From c2e4155296a527e2eb7cbe1300eb5fab1d351865 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Mon, 14 Sep 2026 21:48:56 +0200 Subject: [PATCH 1/5] codex-deployment-localized-presentation: localize deployment presentation --- build/api/index.js | 19 +- build/cli/index.js | 761 +++++++++++----- build/github_action/index.js | 859 +++++++++++++----- docs/issues/deployment-orchestration.mdx | 34 + docs/issues/type/hotfix.mdx | 4 +- docs/issues/type/release.mdx | 4 +- scripts/coverage-budgets.json | 22 + specs/CATALOG.md | 14 +- specs/catalog.json | 15 +- specs/configurable-release-orchestration.md | 72 +- specs/repository-locale-and-localization.md | 26 +- .../github_action_completion.test.ts | 80 ++ src/actions/github_action_completion.ts | 58 +- .../deployment_message_catalog.test.ts | 89 ++ .../__tests__/deployment_plan_policy.test.ts | 11 +- .../deployment_presentation_policy.test.ts | 71 +- .../policies/action_summary_policy.ts | 49 +- .../policies/deployment_message_catalog.ts | 299 ++++++ .../policies/deployment_plan_policy.ts | 3 + .../deployment_presentation_policy.ts | 569 ++++++------ .../ports/deployment_orchestration_ports.ts | 5 +- .../deployment_orchestration_dependencies.ts | 2 + .../deployment_orchestration_runtime.ts | 73 +- .../push_single_action_contexts.test.ts | 7 +- .../deployment_orchestration_use_case.test.ts | 52 +- .../deployment_phase_handlers.test.ts | 2 +- .../accept_promotion_handler.ts | 3 +- .../confirm_publication_handler.ts | 3 +- .../prepare_promotion_handler.ts | 2 + .../reconciliation_handler.ts | 3 +- .../usecases/push_single_action_contexts.ts | 10 +- .../github_publication_boundaries.test.ts | 9 + .../__tests__/deployment_operation.test.ts | 19 + src/domain/__tests__/locale.test.ts | 10 + src/domain/__tests__/message_catalog.test.ts | 1 + src/domain/deployment_operation.ts | 4 + src/domain/locale.ts | 19 + src/domain/message_catalog.ts | 2 +- .../main_run_route_composition_root.ts | 4 +- 39 files changed, 2463 insertions(+), 826 deletions(-) create mode 100644 src/application/policies/__tests__/deployment_message_catalog.test.ts create mode 100644 src/application/policies/deployment_message_catalog.ts diff --git a/build/api/index.js b/build/api/index.js index b4fa64a9a..a3b8a0596 100644 --- a/build/api/index.js +++ b/build/api/index.js @@ -5222,6 +5222,7 @@ exports.canonicalizeLocaleTag = canonicalizeLocaleTag; exports.normalizeLocaleTag = normalizeLocaleTag; exports.resolveLocaleProfile = resolveLocaleProfile; exports.localeForScope = localeForScope; +exports.isLocaleProfile = isLocaleProfile; exports.localeLanguagesMatch = localeLanguagesMatch; exports.baseLanguage = baseLanguage; exports.DEFAULT_REPOSITORY_LOCALE = 'en-US'; @@ -5291,6 +5292,22 @@ function localeForScope(profile, scope) { return profile.pullRequest; return profile.repository; } +function isLocaleProfile(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) + return false; + const candidate = value; + try { + const resolved = resolveLocaleProfile(candidate.repository, candidate.issueOverride ?? '', candidate.pullRequestOverride ?? ''); + return candidate.repository === resolved.repository + && candidate.issue === resolved.issue + && candidate.pullRequest === resolved.pullRequest + && candidate.issueOverride === resolved.issueOverride + && candidate.pullRequestOverride === resolved.pullRequestOverride; + } + catch { + return false; + } +} function localeLanguagesMatch(left, right) { return baseLanguage(canonicalizeLocaleTag(left)) === baseLanguage(canonicalizeLocaleTag(right)); } @@ -5452,7 +5469,7 @@ function validMessageText(value) { function safeDynamicText(value) { return validMessageText(value) && !/[\r\n\u202A-\u202E\u2066-\u2069]/u.test(value) - && !/|<\/?[A-Za-z]|https?:\/\/|```|[`*_[\]~]|(^|\s)\/(?:copilot)(?:\s|$)|@[A-Za-z0-9]/iu.test(value); + && !/|<\/?[A-Za-z]|https?:\/\/|```|[`*_[\]~|]|(^|\s)\/(?:copilot)(?:\s|$)|@[A-Za-z0-9]/iu.test(value); } function pluralPlaceholderParity(message) { const expected = placeholdersForTemplate(message.other); diff --git a/build/cli/index.js b/build/cli/index.js index 2f0fb9d44..39f2518e0 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -43496,6 +43496,244 @@ function projectDeploymentLabels(current, operation, labels) { } +/***/ }), + +/***/ 79364: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEPLOYMENT_CATALOG_DEFINITIONS = exports.SPANISH_DEPLOYMENT_DEFINITION = exports.ENGLISH_DEPLOYMENT_DEFINITION = exports.DEPLOYMENT_MESSAGE_IDS = void 0; +exports.resolveStaticDeploymentCatalog = resolveStaticDeploymentCatalog; +exports.resolveDeploymentCatalog = resolveDeploymentCatalog; +exports.deploymentCopy = deploymentCopy; +const deployment_operation_1 = __nccwpck_require__(92730); +const message_catalog_1 = __nccwpck_require__(27097); +const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); +const SIMPLE_MESSAGE_KEYS = Object.freeze([ + 'release', 'hotfix', 'currentStatus', 'noAction', 'actionRequired', 'progress', + 'currentTransition', 'whatNext', 'links', 'technical', 'alreadyPublished', + 'notPublished', 'productionUpdated', 'developmentSynchronized', 'yes', 'no', + 'from', 'to', 'state', 'compare', 'controlCenter', 'purpose', 'afterMerge', + 'purposePromotion', 'promotionOverview', + 'reconciliationOverview', 'afterPromotion', 'noRepublish', 'origin', + 'preparedSource', 'destination', 'publication', 'productionFact', + 'developmentTarget', 'completionEffect', 'closeIssue', 'keepIssue', + 'readyBeforeReview', 'buildValidation', 'packageSmoke', 'protectedChecks', + 'syncReason', 'protectedFacts', 'cut', 'promotion', 'reconciliation', 'cleanup', + 'jobSummary', 'result', 'externalWait', 'workflowFailure', 'previousPhase', + 'resultingPhase', 'retryable', 'retryAfterCorrection', 'manualIntervention', + 'reviewManagedPr', 'fallback', 'operation', 'strategy', 'pullRequestMode', + 'sourceSha', 'productionSha', 'pending', 'publicationReceipt', 'absent', + 'localization', 'property', 'value', 'repositoryLocaleLabel', + 'issueLocaleLabel', 'pullRequestLocaleLabel', 'catalogResolution', + 'descriptors', 'reason', +]); +const DIAGRAM_KEYS = Object.freeze([ + 'source', 'prepared', 'production', 'accepted', 'publication', + 'reconciliation', 'complete', +]); +const TEMPLATE_MESSAGE_IDS = Object.freeze([ + 'deployment.template.promotionTitle', + 'deployment.template.reconciliationTitle', + 'deployment.template.promotionTechnical', + 'deployment.template.reconciliationTechnical', + 'deployment.template.jobState', + 'deployment.template.admissionScope', + 'deployment.template.sourceBranchLink', + 'deployment.template.originCommitLink', + 'deployment.template.preparedCommitLink', + 'deployment.template.promotionPullRequestLink', + 'deployment.template.reconciliationPullRequestLink', + 'deployment.template.productionCommitLink', + 'deployment.template.releaseLink', + 'deployment.template.actionTagLink', + 'deployment.template.npmLink', + 'deployment.template.workflowRunLink', + 'deployment.template.nextPromotion', + 'deployment.milestone.promotionMerged', + 'deployment.milestone.publicationComplete', + 'deployment.milestone.reconciliationBlocked', + 'deployment.milestone.complete', +]); +exports.DEPLOYMENT_MESSAGE_IDS = Object.freeze([ + ...SIMPLE_MESSAGE_KEYS.map(key => `deployment.${key}`), + ...deployment_operation_1.DEPLOYMENT_PHASES.map(phase => `deployment.phase.${phase}`), + ...DIAGRAM_KEYS.map(key => `deployment.diagram.${key}`), + ...TEMPLATE_MESSAGE_IDS, +]); +const ENGLISH_SIMPLE = Object.freeze({ + release: 'Release', hotfix: 'Hotfix', currentStatus: 'Current status', + noAction: 'No action is required while GitHub owns the pending transition.', actionRequired: 'Action required', + progress: 'Progress', currentTransition: 'Current transition', whatNext: 'What happens next', links: 'Links', + technical: 'Technical details', alreadyPublished: 'Package status: already published', notPublished: 'Package status: not published', + productionUpdated: 'Production updated', developmentSynchronized: 'Development synchronized', yes: 'Yes', no: 'No', + from: 'From', to: 'To', state: 'State', compare: 'Compare changes', controlCenter: 'Release control center', + purpose: 'Purpose', afterMerge: 'After merge', purposePromotion: 'accept the prepared change in production', + promotionOverview: 'Production promotion', reconciliationOverview: 'Development reconciliation', + afterPromotion: 'After merge, Copilot will tag and publish the accepted production commit.', + noRepublish: 'Merging or closing this PR cannot publish the package again.', + origin: 'Origin', preparedSource: 'Prepared source', destination: 'Destination', publication: 'Publication', + productionFact: 'Production fact', developmentTarget: 'Development target', completionEffect: 'Completion effect', + closeIssue: 'Close issue after all targets', keepIssue: 'Keep issue open', readyBeforeReview: 'Ready before review', + buildValidation: 'Build and release validation', packageSmoke: 'Package smoke test', + protectedChecks: 'Protected-branch checks and reviews', + syncReason: 'A dedicated sync branch preserves target-only commits and isolates target-dependent checks.', + protectedFacts: 'What Copilot protected', cut: 'Source cut', promotion: 'Production promotion', + reconciliation: 'Development reconciliation', cleanup: 'Cleanup and issue completion', + jobSummary: 'Deployment orchestration', result: 'Result', externalWait: 'Waiting externally', + workflowFailure: 'Workflow failed', previousPhase: 'Previous phase', resultingPhase: 'Resulting phase', + retryable: 'Retryable', retryAfterCorrection: 'Retry after correcting the cause', + manualIntervention: 'Manual intervention is required', + reviewManagedPr: 'review and merge the managed PR when GitHub reports it ready', + fallback: 'prepared -> production PR -> accepted -> published -> reconciled -> complete', + operation: 'Operation', strategy: 'Strategy', pullRequestMode: 'PR mode', sourceSha: 'Source SHA', + productionSha: 'Production SHA', pending: 'pending', publicationReceipt: 'Publication receipt', absent: 'absent', + localization: 'Localization', property: 'Property', value: 'Value', + repositoryLocaleLabel: 'Repository locale', issueLocaleLabel: 'Issue locale', + pullRequestLocaleLabel: 'Pull-request locale', catalogResolution: 'Catalog resolution', + descriptors: 'descriptors', reason: 'reason', +}); +const SPANISH_SIMPLE = Object.freeze({ + release: 'Release', hotfix: 'Hotfix', currentStatus: 'Estado actual', + noAction: 'No se requiere ninguna acción mientras GitHub gestiona la transición pendiente.', actionRequired: 'Acción necesaria', + progress: 'Progreso', currentTransition: 'Transición actual', whatNext: 'Qué ocurrirá después', links: 'Enlaces', + technical: 'Detalles técnicos', alreadyPublished: 'Estado del paquete: ya publicado', notPublished: 'Estado del paquete: no publicado', + productionUpdated: 'Producción actualizada', developmentSynchronized: 'Desarrollo sincronizado', yes: 'Sí', no: 'No', + from: 'Origen', to: 'Destino', state: 'Estado', compare: 'Comparar cambios', controlCenter: 'Centro de control de la release', + purpose: 'Propósito', afterMerge: 'Después del merge', purposePromotion: 'aceptar en producción el cambio preparado', + promotionOverview: 'Promoción a producción', reconciliationOverview: 'Reconciliación con desarrollo', + afterPromotion: 'Tras el merge, Copilot etiquetará y publicará el commit aceptado en producción.', + noRepublish: 'Mergear o cerrar esta PR no puede volver a publicar el paquete.', + origin: 'Origen', preparedSource: 'Fuente preparada', destination: 'Destino', publication: 'Publicación', + productionFact: 'Estado de producción', developmentTarget: 'Destino de desarrollo', completionEffect: 'Efecto al completar', + closeIssue: 'Cerrar la issue tras todos los destinos', keepIssue: 'Mantener la issue abierta', readyBeforeReview: 'Listo antes de revisar', + buildValidation: 'Build y validación de release', packageSmoke: 'Smoke test del paquete', + protectedChecks: 'Checks y revisiones de la rama protegida', + syncReason: 'Una rama de sincronización dedicada preserva los commits exclusivos del destino y aísla sus checks.', + protectedFacts: 'Qué ha protegido Copilot', cut: 'Corte de la fuente', promotion: 'Promoción a producción', + reconciliation: 'Reconciliación con desarrollo', cleanup: 'Limpieza y cierre de la issue', + jobSummary: 'Orquestación del despliegue', result: 'Resultado', externalWait: 'Esperando fuera del workflow', + workflowFailure: 'Workflow fallido', previousPhase: 'Fase anterior', resultingPhase: 'Fase resultante', + retryable: 'Reintentable', retryAfterCorrection: 'Vuelve a intentarlo después de corregir la causa', + manualIntervention: 'Se requiere intervención manual', + reviewManagedPr: 'revisa y mergea la PR gestionada cuando GitHub indique que está lista', + fallback: 'preparada -> PR de producción -> aceptada -> publicada -> reconciliada -> completada', + operation: 'Operación', strategy: 'Estrategia', pullRequestMode: 'Modo de PR', sourceSha: 'SHA de origen', + productionSha: 'SHA de producción', pending: 'pendiente', publicationReceipt: 'Recibo de publicación', absent: 'ausente', + localization: 'Localización', property: 'Propiedad', value: 'Valor', + repositoryLocaleLabel: 'Locale del repositorio', issueLocaleLabel: 'Locale de la issue', + pullRequestLocaleLabel: 'Locale de la pull request', catalogResolution: 'Resolución del catálogo', + descriptors: 'descriptores', reason: 'motivo', +}); +const ENGLISH_PHASES = Object.freeze({ + preparing: 'preparing the version', promotion_pr_pending: 'waiting for production approval', + promoted: 'accepted in production', publishing: 'publishing artifacts', + published: 'published; preparing development reconciliation', + reconciliation_pending: 'waiting for development reconciliation', completed: 'completed', blocked: 'needs attention', +}); +const SPANISH_PHASES = Object.freeze({ + preparing: 'preparando la versión', promotion_pr_pending: 'esperando aprobación en producción', + promoted: 'aceptada en producción', publishing: 'publicando artefactos', + published: 'publicada; preparando la reconciliación', + reconciliation_pending: 'esperando reconciliación con desarrollo', completed: 'completada', blocked: 'necesita atención', +}); +const ENGLISH_DIAGRAM = Object.freeze({ + source: 'Source snapshot', prepared: 'Version prepared', production: 'Production PR', + accepted: 'Accepted in production', publication: 'Package and release', + reconciliation: 'Development reconciliation', complete: 'Complete', +}); +const SPANISH_DIAGRAM = Object.freeze({ + source: 'Snapshot de origen', prepared: 'Versión preparada', production: 'PR de producción', + accepted: 'Aceptada en producción', publication: 'Paquete y release', + reconciliation: 'Reconciliación con desarrollo', complete: 'Completada', +}); +const ENGLISH_TEMPLATES = Object.freeze({ + 'deployment.template.promotionTitle': '{kind}({version}): promote to {branch}', + 'deployment.template.reconciliationTitle': '{kind}({version}): reconcile {source} into {target}', + 'deployment.template.promotionTechnical': 'Operation {operation}; strategy {strategy}; merge mode {mergeMode}.', + 'deployment.template.reconciliationTechnical': 'Operation {operation}; source SHA {sourceSha}.', + 'deployment.template.jobState': 'State: version {version}, revision {revision}', + 'deployment.template.admissionScope': 'Admission scope: repository + launcher issue #{issue}', + 'deployment.template.sourceBranchLink': '{branch} branch', + 'deployment.template.originCommitLink': '{commit} origin commit', + 'deployment.template.preparedCommitLink': '{commit} prepared commit', + 'deployment.template.promotionPullRequestLink': 'Promotion PR #{number}', + 'deployment.template.reconciliationPullRequestLink': 'Reconciliation PR #{number}', + 'deployment.template.productionCommitLink': '{commit} production commit', + 'deployment.template.releaseLink': '{tag} GitHub Release', + 'deployment.template.actionTagLink': '{tag} Action tag', + 'deployment.template.npmLink': '{package} version {version} on npm', + 'deployment.template.workflowRunLink': 'Workflow run', + 'deployment.template.nextPromotion': '{source} is prepared for promotion to {production}.', + 'deployment.milestone.promotionMerged': '✅ Promotion PR #{number} merged. Publication is starting from production SHA {productionSha}.', + 'deployment.milestone.publicationComplete': '📦 {tag} is published from accepted production SHA {productionSha}.', + 'deployment.milestone.reconciliationBlocked': '❌ Deployment blocked: {reason}', + 'deployment.milestone.complete': '✅ Deployment {tag} and every configured reconciliation target are complete.', +}); +const SPANISH_TEMPLATES = Object.freeze({ + 'deployment.template.promotionTitle': '{kind}({version}): promover a {branch}', + 'deployment.template.reconciliationTitle': '{kind}({version}): reconciliar {source} con {target}', + 'deployment.template.promotionTechnical': 'Operación {operation}; estrategia {strategy}; modo de merge {mergeMode}.', + 'deployment.template.reconciliationTechnical': 'Operación {operation}; SHA de origen {sourceSha}.', + 'deployment.template.jobState': 'Estado: versión {version}, revisión {revision}', + 'deployment.template.admissionScope': 'Ámbito de admisión: repositorio + issue de lanzamiento #{issue}', + 'deployment.template.sourceBranchLink': 'Rama {branch}', + 'deployment.template.originCommitLink': 'Commit de origen {commit}', + 'deployment.template.preparedCommitLink': 'Commit preparado {commit}', + 'deployment.template.promotionPullRequestLink': 'PR de promoción #{number}', + 'deployment.template.reconciliationPullRequestLink': 'PR de reconciliación #{number}', + 'deployment.template.productionCommitLink': 'Commit de producción {commit}', + 'deployment.template.releaseLink': 'Release de GitHub {tag}', + 'deployment.template.actionTagLink': 'Tag de Action {tag}', + 'deployment.template.npmLink': '{package}, versión {version}, en npm', + 'deployment.template.workflowRunLink': 'Ejecución del workflow', + 'deployment.template.nextPromotion': '{source} está preparada para promoverse a {production}.', + 'deployment.milestone.promotionMerged': '✅ La PR de promoción #{number} se ha mergeado. La publicación comienza desde el SHA de producción {productionSha}.', + 'deployment.milestone.publicationComplete': '📦 {tag} se ha publicado desde el SHA de producción aceptado {productionSha}.', + 'deployment.milestone.reconciliationBlocked': '❌ Despliegue bloqueado: {reason}', + 'deployment.milestone.complete': '✅ El despliegue {tag} y todos los destinos de reconciliación configurados se han completado.', +}); +function catalogMessages(simple, phases, diagram, templates) { + return Object.freeze({ + ...Object.fromEntries(SIMPLE_MESSAGE_KEYS.map(key => [`deployment.${key}`, simple[key]])), + ...Object.fromEntries(deployment_operation_1.DEPLOYMENT_PHASES.map(phase => [`deployment.phase.${phase}`, phases[phase]])), + ...Object.fromEntries(DIAGRAM_KEYS.map(key => [`deployment.diagram.${key}`, diagram[key]])), + ...templates, + }); +} +exports.ENGLISH_DEPLOYMENT_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, + locale: 'en-US', + compatibleBaseLanguage: 'en', + messages: catalogMessages(ENGLISH_SIMPLE, ENGLISH_PHASES, ENGLISH_DIAGRAM, ENGLISH_TEMPLATES), +}); +exports.SPANISH_DEPLOYMENT_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, + locale: 'es-ES', + compatibleBaseLanguage: 'es', + messages: catalogMessages(SPANISH_SIMPLE, SPANISH_PHASES, SPANISH_DIAGRAM, SPANISH_TEMPLATES), +}); +exports.DEPLOYMENT_CATALOG_DEFINITIONS = Object.freeze([ + exports.ENGLISH_DEPLOYMENT_DEFINITION, + exports.SPANISH_DEPLOYMENT_DEFINITION, +]); +function resolveStaticDeploymentCatalog(locale) { + return (0, resolved_message_catalog_policy_1.resolveStaticMessageCatalogView)(locale, exports.ENGLISH_DEPLOYMENT_DEFINITION, exports.DEPLOYMENT_CATALOG_DEFINITIONS); +} +function resolveDeploymentCatalog(locale, configuration, resolver) { + return (0, resolved_message_catalog_policy_1.resolveMessageCatalogView)(locale, exports.DEPLOYMENT_MESSAGE_IDS, exports.ENGLISH_DEPLOYMENT_DEFINITION, exports.DEPLOYMENT_CATALOG_DEFINITIONS, configuration, resolver); +} +function deploymentCopy(catalog) { + return Object.freeze({ + ...Object.fromEntries(SIMPLE_MESSAGE_KEYS.map(key => [key, catalog.message(`deployment.${key}`)])), + phase: Object.freeze(Object.fromEntries(deployment_operation_1.DEPLOYMENT_PHASES.map(phase => [phase, catalog.message(`deployment.phase.${phase}`)]))), + diagram: Object.freeze(Object.fromEntries(DIAGRAM_KEYS.map(key => [key, catalog.message(`deployment.diagram.${key}`)]))), + }); +} + + /***/ }), /***/ 8352: @@ -43523,6 +43761,7 @@ function buildInitialDeploymentOperation(input) { stateVersion: deployment_operation_1.DEPLOYMENT_STATE_VERSION, revision: 0, operationId: input.operationId, + locale: Object.freeze({ ...input.locale }), kind: input.kind, version: input.version, title: input.title, @@ -43704,74 +43943,17 @@ exports.renderDeploymentDashboard = renderDeploymentDashboard; exports.renderPromotionPullRequest = renderPromotionPullRequest; exports.renderReconciliationPullRequest = renderReconciliationPullRequest; exports.renderDeploymentJobSummary = renderDeploymentJobSummary; -exports.normalizeLocale = normalizeLocale; +exports.renderDeploymentMilestone = renderDeploymentMilestone; const managed_pull_request_1 = __nccwpck_require__(95914); +const deployment_message_catalog_1 = __nccwpck_require__(79364); const publication_identity_policy_1 = __nccwpck_require__(45403); -const EN = { - release: "Release", hotfix: "Hotfix", currentStatus: "Current status", - noAction: "No action is required while GitHub owns the pending transition.", actionRequired: "Action required", - progress: "Progress", currentTransition: "Current transition", whatNext: "What happens next", links: "Links", - technical: "Technical details", alreadyPublished: "Package status: already published", notPublished: "Package status: not published", - productionUpdated: "Production updated", developmentSynchronized: "Development synchronized", yes: "Yes", no: "No", - from: "From", to: "To", state: "State", compare: "Compare changes", controlCenter: "Release control center", - purpose: "Purpose", afterMerge: "After merge", purposePromotion: "accept the prepared change in production", - purposeReconciliation: "bring the accepted production state back to the development line", - afterPromotion: "After merge, Copilot will tag and publish the accepted production commit.", - noRepublish: "Merging or closing this PR cannot publish the package again.", - origin: "Origin", preparedSource: "Prepared source", destination: "Destination", publication: "Publication", - productionFact: "Production fact", developmentTarget: "Development target", completionEffect: "Completion effect", - closeIssue: "Close issue after all targets", keepIssue: "Keep issue open", readyBeforeReview: "Ready before review", - buildValidation: "Build and release validation", packageSmoke: "Package smoke test", - protectedChecks: "Protected-branch checks and reviews", syncReason: "A dedicated sync branch preserves target-only commits and isolates target-dependent checks.", - protectedFacts: "What Copilot protected", cut: "Source cut", promotion: "Production promotion", reconciliation: "Development reconciliation", - cleanup: "Cleanup and issue completion", jobSummary: "Deployment orchestration", result: "Result", - externalWait: "Waiting externally", workflowFailure: "Workflow failed", previousPhase: "Previous phase", resultingPhase: "Resulting phase", - retryable: "Retryable", createdReused: "Created, reused, or skipped", fallback: "prepared -> production PR -> accepted -> published -> reconciled -> complete", - retryAfterCorrection: "Retry after correcting the cause", manualIntervention: "Manual intervention is required", - reviewManagedPr: "review and merge the managed PR when GitHub reports it ready", - phase: { - preparing: "preparing the version", promotion_pr_pending: "waiting for production approval", promoted: "accepted in production", - publishing: "publishing artifacts", published: "published; preparing development reconciliation", - reconciliation_pending: "waiting for development reconciliation", completed: "completed", blocked: "needs attention", - }, - diagram: ["Source snapshot", "Version prepared", "Production PR", "Accepted in production", "Package and release", "Development reconciliation", "Complete"], -}; -const ES = { - release: "Release", hotfix: "Hotfix", currentStatus: "Estado actual", - noAction: "No se requiere ninguna acción mientras GitHub gestiona la transición pendiente.", actionRequired: "Acción necesaria", - progress: "Progreso", currentTransition: "Transición actual", whatNext: "Qué ocurrirá después", links: "Enlaces", - technical: "Detalles técnicos", alreadyPublished: "Estado del paquete: ya publicado", notPublished: "Estado del paquete: no publicado", - productionUpdated: "Producción actualizada", developmentSynchronized: "Desarrollo sincronizado", yes: "Sí", no: "No", - from: "Origen", to: "Destino", state: "Estado", compare: "Comparar cambios", controlCenter: "Centro de control de la release", - purpose: "Propósito", afterMerge: "Después del merge", purposePromotion: "aceptar en producción el cambio preparado", - purposeReconciliation: "llevar el estado aceptado en producción de vuelta a desarrollo", - afterPromotion: "Tras el merge, Copilot etiquetará y publicará el commit aceptado en producción.", - noRepublish: "Mergear o cerrar esta PR no puede volver a publicar el paquete.", - origin: "Origen", preparedSource: "Fuente preparada", destination: "Destino", publication: "Publicación", - productionFact: "Estado de producción", developmentTarget: "Destino de desarrollo", completionEffect: "Efecto al completar", - closeIssue: "Cerrar la issue tras todos los destinos", keepIssue: "Mantener la issue abierta", readyBeforeReview: "Listo antes de revisar", - buildValidation: "Build y validación de release", packageSmoke: "Smoke test del paquete", - protectedChecks: "Checks y revisiones de la rama protegida", syncReason: "Una rama de sincronización dedicada preserva los commits exclusivos del destino y aísla sus checks.", - protectedFacts: "Qué ha protegido Copilot", cut: "Corte de la fuente", promotion: "Promoción a producción", reconciliation: "Reconciliación con desarrollo", - cleanup: "Limpieza y cierre de la issue", jobSummary: "Orquestación del despliegue", result: "Resultado", - externalWait: "Esperando fuera del workflow", workflowFailure: "Workflow fallido", previousPhase: "Fase anterior", resultingPhase: "Fase resultante", - retryable: "Reintentable", createdReused: "Creado, reutilizado u omitido", fallback: "preparada -> PR de producción -> aceptada -> publicada -> reconciliada -> completada", - retryAfterCorrection: "Vuelve a intentarlo después de corregir la causa", manualIntervention: "Se requiere intervención manual", - reviewManagedPr: "revisa y mergea la PR gestionada cuando GitHub indique que está lista", - phase: { - preparing: "preparando la versión", promotion_pr_pending: "esperando aprobación en producción", promoted: "aceptada en producción", - publishing: "publicando artefactos", published: "publicada; preparando la reconciliación", - reconciliation_pending: "esperando reconciliación con desarrollo", completed: "completada", blocked: "necesita atención", - }, - diagram: ["Snapshot de origen", "Versión preparada", "PR de producción", "Aceptada en producción", "Paquete y release", "Reconciliación con desarrollo", "Completada"], -}; -exports.DEPLOYMENT_DASHBOARD_MARKER = "copilot-deployment-dashboard"; +exports.DEPLOYMENT_DASHBOARD_MARKER = 'copilot-deployment-dashboard'; function deploymentDashboardMarker(operationId, issue) { return ``; } -function renderDeploymentDashboard(operation, context) { - const messages = messagesFor(context.issueLocale); - const title = operation.kind === "release" ? messages.release : messages.hotfix; +function renderDeploymentDashboard(operation, context, catalog = (0, deployment_message_catalog_1.resolveStaticDeploymentCatalog)(context.issueLocale)) { + const messages = (0, deployment_message_catalog_1.deploymentCopy)(catalog); + const title = operation.kind === 'release' ? messages.release : messages.hotfix; const action = deploymentAction(operation, messages); const lines = [ (0, publication_identity_policy_1.buildPublicationMarker)({ @@ -43781,121 +43963,174 @@ function renderDeploymentDashboard(operation, context) { key: `operation:${(0, publication_identity_policy_1.createSemanticDigest)(operation.operationId)}`, }, sourceVersion: `revision:${operation.revision}`, - digest: (0, publication_identity_policy_1.createSemanticDigest)({ locale: context.issueLocale, operation }), + digest: (0, publication_identity_policy_1.createSemanticDigest)({ + requestedLocale: catalog.requestedLocale, + resolvedLocale: catalog.locale, + operation, + }), }), - deploymentDashboardMarker(operation.operationId, context.issue), "", - `# ${operation.phase === "blocked" ? "❌" : operation.phase === "completed" ? "✅" : "🚀"} ${title} ${inline(operation.version)}`, "", + deploymentDashboardMarker(operation.operationId, context.issue), '', + `## ${operation.phase === 'blocked' ? '❌' : operation.phase === 'completed' ? '✅' : '🚀'} ${title} ${inline(operation.version)}`, '', `> **${messages.currentStatus}: ${messages.phase[operation.phase]}.**`, ]; if (action.required) - lines.push("", `## ${messages.actionRequired}`, "", action.message); + lines.push('', `### ${messages.actionRequired}`, '', action.message); else lines.push(`> ${action.message}`); - lines.push(""); - if (operation.phase === "blocked") - lines.push(...factTable(operation, messages), "", `## ${messages.protectedFacts}`, "", protectedFact(operation, messages), ""); - if (operation.presentationMode !== "quiet") - lines.push(`## ${messages.progress}`, "", ...progressLines(operation, messages), ""); - if (operation.presentationMode === "guided" && operation.diagrams) - lines.push(...deploymentDiagram(messages), ""); - if (operation.presentationMode !== "quiet") { - lines.push(`## ${messages.currentTransition}`, "", ...transitionTable(operation, messages), "", `## ${messages.whatNext}`, "", nextDescription(operation, messages), "", `## ${messages.links}`, "", deploymentLinks(operation, context, messages).join(" · "), ""); - } - lines.push("
", `${messages.technical}`, "", `- Operation: ${inline(operation.operationId)}`, `- Strategy: ${inline(operation.strategy)}`, `- PR mode: ${inline(operation.selectedPrMode ?? operation.prMode)}`, `- Source SHA: ${inline(operation.sourceSha)}`, `- Production SHA: ${inline(operation.productionSha ?? "pending")}`, "
"); - return lines.join("\n"); -} -function renderPromotionPullRequest(operation, context) { - const messages = messagesFor(context.pullRequestLocale); - const kind = operation.kind === "release" ? "release" : "hotfix"; - const title = `${kind}(${safeText(operation.version)}): promote to ${safeText(operation.productionBranch)}`; + lines.push(''); + if (operation.phase === 'blocked') { + lines.push(...factTable(operation, messages), '', `### ${messages.protectedFacts}`, '', protectedFact(operation, messages), ''); + } + if (operation.presentationMode !== 'quiet') { + lines.push(`### ${messages.progress}`, '', ...progressLines(operation, messages), ''); + } + if (operation.presentationMode === 'guided' && operation.diagrams) { + lines.push(...deploymentDiagram(messages), ''); + } + if (operation.presentationMode !== 'quiet') { + lines.push(`### ${messages.currentTransition}`, '', ...transitionTable(operation, messages), ''); + if (operation.phase !== 'blocked') { + lines.push(`### ${messages.whatNext}`, '', nextDescription(operation, messages, catalog), ''); + } + lines.push(`### ${messages.links}`, '', deploymentLinks(operation, context, messages, catalog).join(' · '), ''); + } + lines.push('
', `${messages.technical}`, '', `- ${messages.operation}: ${inline(operation.operationId)}`, `- ${messages.strategy}: ${inline(operation.strategy)}`, `- ${messages.pullRequestMode}: ${inline(operation.selectedPrMode ?? operation.prMode)}`, `- ${messages.sourceSha}: ${inline(operation.sourceSha)}`, `- ${messages.productionSha}: ${inline(operation.productionSha ?? messages.pending)}`, '
'); + return lines.join('\n'); +} +function renderPromotionPullRequest(operation, context, catalog = (0, deployment_message_catalog_1.resolveStaticDeploymentCatalog)(context.pullRequestLocale)) { + const messages = (0, deployment_message_catalog_1.deploymentCopy)(catalog); + const kind = operation.kind; + const title = catalog.message('deployment.template.promotionTitle', { + kind, + version: safeText(operation.version), + branch: safeText(operation.productionBranch), + }).slice(0, 240); const body = [ - `# 🚀 ${capitalize(messages.purposePromotion)}`, "", - `> **${messages.purpose}:** ${capitalize(messages.purposePromotion)}.`, - `> **${messages.afterMerge}:** ${messages.afterPromotion}`, "", + `## 🚀 ${messages.promotionOverview}`, '', + `> **${messages.purpose}:** ${messages.purposePromotion}.`, '', `| ${messages.origin} | ${messages.preparedSource} | ${messages.destination} | ${messages.publication} |`, - "|---|---|---|---|", - `| ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)} | ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)} | ${inline(operation.productionBranch)} | ${messages.afterMerge} |`, "", - `## ${messages.readyBeforeReview}`, "", - `- ✅ ${messages.buildValidation}`, `- ✅ ${messages.packageSmoke}`, `- ⏳ ${messages.protectedChecks}`, "", - `## ${messages.afterMerge}`, "", - `- ${messages.afterPromotion}`, `- ${messages.reconciliation}: ${inline(operation.developmentBranch)}.`, "", - `[${messages.compare}](${compareUrl(context, operation.productionBranch, operation.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, "", - "
", `${messages.technical}`, "", - `Operation ${inline(operation.operationId)}; strategy ${inline(operation.strategy)}; merge mode ${inline(operation.prMode)}.`, - "
", "", (0, managed_pull_request_1.buildManagedPullRequestMarker)({ operationId: operation.operationId, phase: "promotion", issue: context.issue }), - ].join("\n"); + '|---|---|---|---|', + `| ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)} | ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)} | ${inline(operation.productionBranch)} | ${messages.notPublished} |`, '', + `### ${messages.readyBeforeReview}`, '', + `- ✅ ${messages.buildValidation}`, `- ✅ ${messages.packageSmoke}`, `- ⏳ ${messages.protectedChecks}`, '', + `### ${messages.afterMerge}`, '', + `- ${messages.afterPromotion}`, `- ${messages.reconciliation}: ${inline(operation.developmentBranch)}.`, '', + `[${messages.compare}](${compareUrl(context, operation.productionBranch, operation.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, '', + '
', `${messages.technical}`, '', + catalog.message('deployment.template.promotionTechnical', { + operation: inline(operation.operationId), + strategy: inline(operation.strategy), + mergeMode: inline(operation.prMode), + }), + '
', '', + (0, managed_pull_request_1.buildManagedPullRequestMarker)({ operationId: operation.operationId, phase: 'promotion', issue: context.issue }), + ].join('\n'); return { title, body }; } -function renderReconciliationPullRequest(operation, target, context) { - const messages = messagesFor(context.pullRequestLocale); - const kind = operation.kind === "release" ? "release" : "hotfix"; - const title = `${kind}(${safeText(operation.version)}): reconcile ${safeText(target.sourceBranch)} into ${safeText(target.targetBranch)}`; +function renderReconciliationPullRequest(operation, target, context, catalog = (0, deployment_message_catalog_1.resolveStaticDeploymentCatalog)(context.pullRequestLocale)) { + const messages = (0, deployment_message_catalog_1.deploymentCopy)(catalog); + const kind = operation.kind; + const title = catalog.message('deployment.template.reconciliationTitle', { + kind, + version: safeText(operation.version), + source: safeText(target.sourceBranch), + target: safeText(target.targetBranch), + }).slice(0, 240); const body = [ - `# 🔄 ${capitalize(messages.purposeReconciliation)}`, "", - `> **${messages.alreadyPublished}.** ${messages.noRepublish}`, "", - `| ${messages.productionFact} | ${messages.developmentTarget} | ${messages.completionEffect} |`, "|---|---|---|", - `| ${inline(`${operation.tag}@${shortSha(operation.productionSha ?? target.sourceSha)}`)} | ${inline(target.targetBranch)} | ${operation.issueCompletion === "close" ? messages.closeIssue : messages.keepIssue} |`, "", - ...(target.syncBranch ? [`${messages.syncReason} ${inline(target.syncBranch)}`, ""] : []), - `${messages.noRepublish}`, "", - `[${messages.compare}](${compareUrl(context, target.targetBranch, target.syncBranch ?? target.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, "", - "
", `${messages.technical}`, "", `Operation ${inline(operation.operationId)}; source SHA ${inline(target.sourceSha)}.`, - "
", "", (0, managed_pull_request_1.buildManagedPullRequestMarker)({ operationId: operation.operationId, phase: "reconciliation", issue: context.issue }), - ].join("\n"); + `## 🔄 ${messages.reconciliationOverview}`, '', + `> **${messages.alreadyPublished}.** ${messages.noRepublish}`, '', + `| ${messages.productionFact} | ${messages.developmentTarget} | ${messages.completionEffect} |`, '|---|---|---|', + `| ${inline(`${operation.tag}@${shortSha(operation.productionSha ?? target.sourceSha)}`)} | ${inline(target.targetBranch)} | ${operation.issueCompletion === 'close' ? messages.closeIssue : messages.keepIssue} |`, '', + ...(target.syncBranch ? [`${messages.syncReason} ${inline(target.syncBranch)}`, ''] : []), + `[${messages.compare}](${compareUrl(context, target.targetBranch, target.syncBranch ?? target.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, '', + '
', `${messages.technical}`, '', + catalog.message('deployment.template.reconciliationTechnical', { + operation: inline(operation.operationId), + sourceSha: inline(target.sourceSha), + }), + '
', '', + (0, managed_pull_request_1.buildManagedPullRequestMarker)({ operationId: operation.operationId, phase: 'reconciliation', issue: context.issue }), + ].join('\n'); return { title, body }; } -function renderDeploymentJobSummary(operation, context, previousPhase, operations = []) { - const messages = messagesFor(context.issueLocale); - const externallyPending = operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending"; - const result = operation.phase === "blocked" ? messages.workflowFailure : externallyPending ? messages.externalWait : messages.phase[operation.phase]; +function renderDeploymentJobSummary(operation, context, previousPhase, catalog = (0, deployment_message_catalog_1.resolveStaticDeploymentCatalog)(context.repositoryLocale)) { + const messages = (0, deployment_message_catalog_1.deploymentCopy)(catalog); + const externallyPending = operation.phase === 'promotion_pr_pending' || operation.phase === 'reconciliation_pending'; + const result = operation.phase === 'blocked' + ? messages.workflowFailure + : externallyPending ? messages.externalWait : messages.phase[operation.phase]; const lines = [ - `# ${operation.phase === "blocked" ? "❌" : externallyPending ? "⏳" : "✅"} ${messages.jobSummary}`, "", - `> **${messages.result}: ${result}.**`, "", - `| ${messages.previousPhase} | ${messages.resultingPhase} | ${messages.retryable} |`, "|---|---|---|", - `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${operation.lastFailure?.retryable ? messages.yes : messages.no} |`, "", - `- Operation: ${inline(operation.operationId)}`, - `- State: version ${operation.stateVersion}, revision ${operation.revision}`, - `- Admission scope: repository + launcher issue #${context.issue}`, + `# ${operation.phase === 'blocked' ? '❌' : externallyPending ? '⏳' : '✅'} ${messages.jobSummary}`, '', + `> **${messages.result}: ${result}.**`, '', + `| ${messages.previousPhase} | ${messages.resultingPhase} | ${messages.retryable} |`, '|---|---|---|', + `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${operation.lastFailure?.retryable ? messages.yes : messages.no} |`, '', + `- ${messages.operation}: ${inline(operation.operationId)}`, + `- ${catalog.message('deployment.template.jobState', { version: operation.stateVersion, revision: operation.revision })}`, + `- ${catalog.message('deployment.template.admissionScope', { issue: context.issue })}`, `- ${messages.origin}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, `- ${messages.preparedSource}: ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)}`, - `- ${messages.productionFact}: ${inline(operation.productionSha ? `${operation.productionBranch}@${shortSha(operation.productionSha)}` : "pending")}`, - `- ${messages.publication}: ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, - `- Publication receipt: ${operation.publicationReceipt ? inline(`${operation.publicationReceipt.tag}@${shortSha(operation.publicationReceipt.productionSha)}`) : "absent"}`, - `- ${messages.createdReused}: ${safeText(operations.join(", ") || "none")}`, "", + `- ${messages.productionFact}: ${inline(operation.productionSha ? `${operation.productionBranch}@${shortSha(operation.productionSha)}` : messages.pending)}`, + `- ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, + `- ${messages.publicationReceipt}: ${operation.publicationReceipt + ? inline(`${operation.publicationReceipt.tag}@${shortSha(operation.publicationReceipt.productionSha)}`) + : messages.absent}`, '', ]; - if (operation.phase === "blocked") { - lines.push(`## ${messages.actionRequired}`, "", `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${operation.lastFailure?.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`, ""); + if (operation.phase === 'blocked') { + lines.push(`## ${messages.actionRequired}`, '', `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${operation.lastFailure?.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`, ''); + } + lines.push(deploymentLinks(operation, context, messages, catalog).join(' · ')); + return lines.join('\n'); +} +function renderDeploymentMilestone(milestone, catalog) { + switch (milestone.kind) { + case 'promotion-merged': + return catalog.message('deployment.milestone.promotionMerged', { + number: milestone.pullRequest, + productionSha: inline(milestone.productionSha), + }); + case 'publication-complete': + return catalog.message('deployment.milestone.publicationComplete', { + tag: inline(milestone.tag), + productionSha: inline(milestone.productionSha), + }); + case 'reconciliation-blocked': + return catalog.message('deployment.milestone.reconciliationBlocked', { reason: safeText(milestone.reason) }); + case 'orchestration-complete': + return catalog.message('deployment.milestone.complete', { tag: inline(milestone.tag) }); } - lines.push(deploymentLinks(operation, context, messages).join(" · ")); - return lines.join("\n"); } function progressLines(operation, messages) { - const phase = operation.phase === "blocked" ? operation.lastFailure?.previousPhase ?? "preparing" : operation.phase; + const phase = operation.phase === 'blocked' ? operation.lastFailure?.previousPhase ?? 'preparing' : operation.phase; const reached = (expected) => phaseRank(phase) >= phaseRank(expected); return [ `- [x] ${messages.cut}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, `- [x] ${messages.buildValidation} + ${messages.packageSmoke}`, - `- [${operation.productionSha || reached("promoted") ? "x" : " "}] ${messages.promotion}: ${inline(operation.productionBranch)}`, - `- [${operation.publicationVerified ? "x" : " "}] ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, - `- [${reconciliationCompleted(operation) ? "x" : " "}] ${messages.reconciliation}: ${inline(operation.developmentBranch)}`, - `- [${operation.phase === "completed" ? "x" : " "}] ${messages.cleanup}`, + `- [${operation.productionSha || reached('promoted') ? 'x' : ' '}] ${messages.promotion}: ${inline(operation.productionBranch)}`, + `- [${operation.publicationVerified ? 'x' : ' '}] ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, + `- [${reconciliationCompleted(operation) ? 'x' : ' '}] ${messages.reconciliation}: ${inline(operation.developmentBranch)}`, + `- [${operation.phase === 'completed' ? 'x' : ' '}] ${messages.cleanup}`, ]; } function deploymentDiagram(messages) { - const [source, prepared, production, accepted, publication, reconciliation, complete] = messages.diagram; + const { source, prepared, production, accepted, publication, reconciliation, complete } = messages.diagram; return [ - "```mermaid", "flowchart LR", ` D[${source}] --> R[${prepared}]`, ` R --> P[${production}]`, - ` P --> A[${accepted}]`, ` A --> N[${publication}]`, ` N --> B[${reconciliation}]`, ` B --> C[${complete}]`, "```", "", messages.fallback, + '```mermaid', 'flowchart LR', ` D[${source}] --> R[${prepared}]`, ` R --> P[${production}]`, + ` P --> A[${accepted}]`, ` A --> N[${publication}]`, ` N --> B[${reconciliation}]`, + ` B --> C[${complete}]`, '```', '', messages.fallback, ]; } function transitionTable(operation, messages) { - const activeTarget = operation.reconciliationTargets.find((target) => target.status !== "completed"); + const activeTarget = operation.reconciliationTargets.find(target => target.status !== 'completed'); const from = activeTarget?.syncBranch ?? activeTarget?.sourceBranch ?? operation.sourceBranch; const to = activeTarget?.targetBranch ?? operation.productionBranch; - return [`| ${messages.from} | ${messages.to} | ${messages.state} |`, "|---|---|---|", `| ${inline(from)} | ${inline(to)} | ${inline(messages.phase[operation.phase])} |`]; + return [ + `| ${messages.from} | ${messages.to} | ${messages.state} |`, '|---|---|---|', + `| ${inline(from)} | ${inline(to)} | ${safeText(messages.phase[operation.phase])} |`, + ]; } function factTable(operation, messages) { return [ - `| ${messages.productionUpdated} | ${messages.alreadyPublished} | ${messages.developmentSynchronized} |`, "|---|---|---|", + `| ${messages.productionUpdated} | ${messages.publication} | ${messages.developmentSynchronized} |`, '|---|---|---|', `| ${operation.productionSha ? messages.yes : messages.no} | ${operation.publicationVerified ? messages.yes : messages.no} | ${reconciliationCompleted(operation) ? messages.yes : messages.no} |`, ]; } @@ -43906,27 +44141,26 @@ function protectedFact(operation, messages) { return `${messages.productionUpdated}: ${messages.yes}. ${messages.notPublished}.`; return `${messages.productionUpdated}: ${messages.no}. ${messages.notPublished}.`; } -function nextDescription(operation, messages) { - if (operation.phase === "blocked") { - const diagnostic = safeText(operation.lastFailure?.message ?? messages.workflowFailure); - return `${diagnostic}. ${messages.actionRequired}: ${operation.lastFailure?.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`; - } - if (operation.phase === "promotion_pr_pending") - return messages.afterPromotion; - if (operation.phase === "publishing" || operation.phase === "promoted") +function nextDescription(operation, messages, catalog) { + if (operation.phase === 'promotion_pr_pending' || operation.phase === 'publishing' || operation.phase === 'promoted') { return messages.afterPromotion; - if (operation.phase === "reconciliation_pending" || operation.phase === "published") + } + if (operation.phase === 'reconciliation_pending' || operation.phase === 'published') return messages.noRepublish; - if (operation.phase === "completed") + if (operation.phase === 'completed') { return `${messages.productionUpdated}: ${messages.yes}. ${messages.developmentSynchronized}: ${messages.yes}.`; - return `${messages.promotion}: ${inline(operation.sourceBranch)} -> ${inline(operation.productionBranch)}.`; + } + return catalog.message('deployment.template.nextPromotion', { + source: inline(operation.sourceBranch), + production: inline(operation.productionBranch), + }); } function deploymentAction(operation, messages) { - const manual = (operation.selectedPrMode ?? operation.prMode) === "create-only" - && (operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending"); + const manual = (operation.selectedPrMode ?? operation.prMode) === 'create-only' + && (operation.phase === 'promotion_pr_pending' || operation.phase === 'reconciliation_pending'); if (manual) return { required: true, message: `${messages.protectedChecks}: ${messages.reviewManagedPr}.` }; - if (operation.phase !== "blocked") + if (operation.phase !== 'blocked') return { required: false, message: messages.noAction }; return { required: true, @@ -43935,52 +44169,106 @@ function deploymentAction(operation, messages) { : `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${messages.manualIntervention}.`, }; } -function deploymentLinks(operation, context, messages) { +function deploymentLinks(operation, context, messages, catalog) { const links = [`[${messages.controlCenter}](${issueUrl(context)})`]; - links.push(`[${safeText(operation.sourceBranch)} branch](${branchUrl(context, operation.sourceBranch)})`); - links.push(`[${shortSha(operation.originSha)} origin commit](${commitUrl(context, operation.originSha)})`); - links.push(`[${shortSha(operation.sourceSha)} prepared commit](${commitUrl(context, operation.sourceSha)})`); - const activeTarget = operation.reconciliationTargets.find((target) => target.status !== "completed"); + links.push(markdownLink(catalog.message('deployment.template.sourceBranchLink', { + branch: safeText(operation.sourceBranch), + }), branchUrl(context, operation.sourceBranch))); + links.push(markdownLink(catalog.message('deployment.template.originCommitLink', { + commit: shortSha(operation.originSha), + }), commitUrl(context, operation.originSha))); + links.push(markdownLink(catalog.message('deployment.template.preparedCommitLink', { + commit: shortSha(operation.sourceSha), + }), commitUrl(context, operation.sourceSha))); + const activeTarget = operation.reconciliationTargets.find(target => target.status !== 'completed'); links.push(`[${messages.compare}](${compareUrl(context, activeTarget?.targetBranch ?? operation.productionBranch, activeTarget?.syncBranch ?? activeTarget?.sourceBranch ?? operation.sourceBranch)})`); - if (operation.promotionPullRequest) - links.push(`[Promotion PR #${operation.promotionPullRequest}](${pullRequestUrl(context, operation.promotionPullRequest)})`); + if (operation.promotionPullRequest) { + links.push(markdownLink(catalog.message('deployment.template.promotionPullRequestLink', { + number: operation.promotionPullRequest, + }), pullRequestUrl(context, operation.promotionPullRequest))); + } for (const target of operation.reconciliationTargets) { - if (target.pullRequest) - links.push(`[Reconciliation PR #${target.pullRequest}](${pullRequestUrl(context, target.pullRequest)})`); + if (target.pullRequest) { + links.push(markdownLink(catalog.message('deployment.template.reconciliationPullRequestLink', { + number: target.pullRequest, + }), pullRequestUrl(context, target.pullRequest))); + } + } + if (operation.productionSha) { + links.push(markdownLink(catalog.message('deployment.template.productionCommitLink', { + commit: shortSha(operation.productionSha), + }), commitUrl(context, operation.productionSha))); } - if (operation.productionSha) - links.push(`[${shortSha(operation.productionSha)} production commit](${commitUrl(context, operation.productionSha)})`); if (operation.publicationVerified) { - links.push(`[${safeText(operation.tag)} GitHub Release](${repositoryUrl(context)}/releases/tag/${encodeURIComponent(operation.tag)})`); - links.push(`[v${safeText(operation.version.split(".")[0])} Action tag](${branchUrl(context, `v${operation.version.split(".")[0]}`)})`); - if (context.packageName) - links.push(`[${safeText(context.packageName)}@${safeText(operation.version)} on npm](${npmVersionUrl(context.packageName, operation.version)})`); + links.push(markdownLink(catalog.message('deployment.template.releaseLink', { + tag: safeText(operation.tag), + }), `${repositoryUrl(context)}/releases/tag/${encodeURIComponent(operation.tag)}`)); + const actionTag = `v${operation.version.split('.')[0]}`; + links.push(markdownLink(catalog.message('deployment.template.actionTagLink', { + tag: actionTag, + }), branchUrl(context, actionTag))); + if (context.packageName) { + links.push(markdownLink(catalog.message('deployment.template.npmLink', { + package: safeText(context.packageName), + version: safeText(operation.version), + }), npmVersionUrl(context.packageName, operation.version))); + } + } + if (context.workflowRunUrl) { + links.push(markdownLink(catalog.message('deployment.template.workflowRunLink'), safeUrl(context.workflowRunUrl))); } - if (context.workflowRunUrl) - links.push(`[Workflow run](${safeUrl(context.workflowRunUrl)})`); return links; } -function messagesFor(locale) { return normalizeLocale(locale) === "es-ES" ? ES : EN; } -function normalizeLocale(locale) { return locale.toLowerCase().startsWith("es") ? "es-ES" : "en-US"; } function reconciliationCompleted(operation) { - return operation.phase === "completed" + return operation.phase === 'completed' || (operation.reconciliationTargets.length > 0 - && operation.reconciliationTargets.every((target) => target.status === "completed")); + && operation.reconciliationTargets.every(target => target.status === 'completed')); +} +function phaseRank(phase) { + return ['preparing', 'promotion_pr_pending', 'promoted', 'publishing', 'published', 'reconciliation_pending', 'completed'].indexOf(phase); +} +function repositoryUrl(context) { + return `https://github.com/${encodeURIComponent(context.owner)}/${encodeURIComponent(context.repository)}`; +} +function issueUrl(context) { + return `${repositoryUrl(context)}/issues/${context.issue}`; +} +function pullRequestUrl(context, number) { + return `${repositoryUrl(context)}/pull/${number}`; +} +function branchUrl(context, branch) { + return `${repositoryUrl(context)}/tree/${encodeURIComponent(branch)}`; +} +function commitUrl(context, sha) { + return `${repositoryUrl(context)}/commit/${encodeURIComponent(sha)}`; +} +function npmVersionUrl(packageName, version) { + return `https://www.npmjs.com/package/${encodeURIComponent(packageName)}/v/${encodeURIComponent(version)}`; +} +function compareUrl(context, base, head) { + return `${repositoryUrl(context)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`; +} +function markdownLink(label, url) { + return `[${safeLinkLabel(label)}](${url})`; +} +function inline(value) { + return `\`${safeText(value)}\``; +} +function safeText(value) { + return value.replace(/[\r\n`<>|]/gu, '').replace(/@/gu, '@\u200b').replace(/::/gu, '﹕﹕').slice(0, 240); +} +function safeLinkLabel(value) { + return safeText(value).replace(/[[\]()]/gu, ''); +} +function safeMarkerValue(value) { + return value.replace(/[^A-Za-z0-9._-]/gu, '').slice(0, 128); +} +function safeUrl(value) { + return /^https:\/\/github\.com\//u.test(value) ? value : 'https://github.com'; +} +function shortSha(value) { + return safeText(value).slice(0, 7); } -function phaseRank(phase) { return ["preparing", "promotion_pr_pending", "promoted", "publishing", "published", "reconciliation_pending", "completed"].indexOf(phase); } -function repositoryUrl(context) { return `https://github.com/${encodeURIComponent(context.owner)}/${encodeURIComponent(context.repository)}`; } -function issueUrl(context) { return `${repositoryUrl(context)}/issues/${context.issue}`; } -function pullRequestUrl(context, number) { return `${repositoryUrl(context)}/pull/${number}`; } -function branchUrl(context, branch) { return `${repositoryUrl(context)}/tree/${encodeURIComponent(branch)}`; } -function commitUrl(context, sha) { return `${repositoryUrl(context)}/commit/${encodeURIComponent(sha)}`; } -function npmVersionUrl(packageName, version) { return `https://www.npmjs.com/package/${encodeURIComponent(packageName)}/v/${encodeURIComponent(version)}`; } -function compareUrl(context, base, head) { return `${repositoryUrl(context)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`; } -function inline(value) { return `\`${safeText(value)}\``; } -function safeText(value) { return value.replace(/[\r\n`<>]/g, "").replace(/@/g, "@\u200b").replace(/::/g, "﹕﹕").slice(0, 240); } -function safeMarkerValue(value) { return value.replace(/[^A-Za-z0-9._-]/g, "").slice(0, 128); } -function safeUrl(value) { return /^https:\/\/github\.com\//.test(value) ? value : "https://github.com"; } -function shortSha(value) { return safeText(value).slice(0, 7); } -function capitalize(value) { return value.charAt(0).toUpperCase() + value.slice(1); } /***/ }), @@ -46398,6 +46686,7 @@ exports.semanticCleanupError = semanticCleanupError; const application_error_1 = __nccwpck_require__(75999); const deployment_plan_policy_1 = __nccwpck_require__(8352); const deployment_presentation_policy_1 = __nccwpck_require__(83221); +const deployment_message_catalog_1 = __nccwpck_require__(79364); const deployment_operation_1 = __nccwpck_require__(92730); const merge_queue_readiness_1 = __nccwpck_require__(12515); const result_1 = __nccwpck_require__(73817); @@ -46406,6 +46695,7 @@ class DeploymentOrchestrationRuntime { constructor(dependencies, stateBoundary) { this.dependencies = dependencies; this.stateBoundary = stateBoundary; + this.presentationCatalogs = new Map(); } async persist(context, operation) { context.currentConfiguration.deploymentOrchestration = operation; @@ -46415,7 +46705,7 @@ class DeploymentOrchestrationRuntime { const blocked = (0, deployment_operation_1.blockDeploymentOperation)(operation, category, message, retryable); await this.persist(context, blocked); await this.publishDashboard(context, blocked); - await this.publishMilestone(context, blocked, "reconciliation-blocked", `❌ Deployment blocked: ${blocked.lastFailure?.message}`); + await this.publishMilestone(context, blocked, { kind: "reconciliation-blocked", reason: blocked.lastFailure?.message ?? message }); return new result_1.Result({ id: exports.DEPLOYMENT_ORCHESTRATION_TASK_ID, success: false, @@ -46426,7 +46716,8 @@ class DeploymentOrchestrationRuntime { } async publishDashboard(context, operation) { const marker = (0, deployment_presentation_policy_1.deploymentDashboardMarker)(operation.operationId, context.singleAction.issue); - const body = (0, deployment_presentation_policy_1.renderDeploymentDashboard)(operation, presentationContext(context)); + const catalog = await this.presentationCatalog("issue", context, operation); + const body = (0, deployment_presentation_policy_1.renderDeploymentDashboard)(operation, presentationContext(context, operation), catalog); const current = await this.dependencies.presentation.findDashboard(context.singleAction.issue, marker); if (current) { await this.dependencies.presentation.updateDashboard(context.singleAction.issue, current.id, body); @@ -46434,11 +46725,12 @@ class DeploymentOrchestrationRuntime { } await this.dependencies.presentation.createDashboard(context.singleAction.issue, body); } - async publishMilestone(context, operation, name, body) { + async publishMilestone(context, operation, milestone) { if (operation.commentMode !== "milestones") return; - const marker = ``; - await this.dependencies.presentation.publishMilestone(context.singleAction.issue, marker, body); + const marker = ``; + const catalog = await this.presentationCatalog("issue", context, operation); + await this.dependencies.presentation.publishMilestone(context.singleAction.issue, marker, (0, deployment_presentation_policy_1.renderDeploymentMilestone)(milestone, catalog)); } async createOrReusePullRequest(context, operation, phase, target) { const headBranch = target?.syncBranch ?? target?.sourceBranch ?? operation.sourceBranch; @@ -46456,10 +46748,11 @@ class DeploymentOrchestrationRuntime { } if (existing[0]) return existing[0]; - const presentation = presentationContext(context); + const presentation = presentationContext(context, operation); + const catalog = await this.presentationCatalog("pull-request", context, operation); const content = phase === "promotion" - ? (0, deployment_presentation_policy_1.renderPromotionPullRequest)(operation, presentation) - : (0, deployment_presentation_policy_1.renderReconciliationPullRequest)(operation, requireTarget(target), presentation); + ? (0, deployment_presentation_policy_1.renderPromotionPullRequest)(operation, presentation, catalog) + : (0, deployment_presentation_policy_1.renderReconciliationPullRequest)(operation, requireTarget(target), presentation, catalog); return await this.dependencies.pullRequests.createManagedPullRequest({ ...query, ...content }); } async configureMergeBehavior(context, operation, pullRequest, category, targetRole) { @@ -46483,7 +46776,7 @@ class DeploymentOrchestrationRuntime { const capabilities = await this.dependencies.targetRules.getTargetCapabilities(targetBranch, { candidateHeadSha, ...(pullRequest === undefined ? {} : { pullRequest }) }); const decision = (0, deployment_plan_policy_1.selectPullRequestMode)(operation.prMode, capabilities); if (decision.kind === "unsupported") { - return this.unsupportedMergeBehavior(context, targetRole, targetBranch, capabilities, decision); + return this.unsupportedMergeBehavior(context, operation, targetRole, targetBranch, capabilities, decision); } if (decision.mode === "merge-queue") { const readiness = (0, merge_queue_readiness_1.evaluateMergeQueueReadiness)({ @@ -46497,7 +46790,7 @@ class DeploymentOrchestrationRuntime { if (readiness.verdict !== "ready") { return { kind: "blocked", - reason: (0, deployment_plan_policy_1.mergeQueueReadinessFailureMessage)(readiness, context.locale.issue), + reason: (0, deployment_plan_policy_1.mergeQueueReadinessFailureMessage)(readiness, effectiveLocale(context, operation).issue), }; } } @@ -46549,7 +46842,7 @@ class DeploymentOrchestrationRuntime { await this.dependencies.pullRequests.enableAutoMerge(pullRequest.nodeId); } } - unsupportedMergeBehavior(context, targetRole, targetBranch, capabilities, decision) { + unsupportedMergeBehavior(context, operation, targetRole, targetBranch, capabilities, decision) { if (capabilities.mergeQueueObservationProblems.length === 0) { return { kind: "blocked", reason: decision.reason }; } @@ -46563,9 +46856,20 @@ class DeploymentOrchestrationRuntime { }); return { kind: "blocked", - reason: (0, deployment_plan_policy_1.mergeQueueReadinessFailureMessage)(readiness, context.locale.issue), + reason: (0, deployment_plan_policy_1.mergeQueueReadinessFailureMessage)(readiness, effectiveLocale(context, operation).issue), }; } + presentationCatalog(scope, context, operation) { + const locale = effectiveLocale(context, operation); + const targetLocale = scope === "issue" ? locale.issue : locale.pullRequest; + const key = `${scope}:${targetLocale}`; + const existing = this.presentationCatalogs.get(key); + if (existing) + return existing; + const resolution = (0, deployment_message_catalog_1.resolveDeploymentCatalog)(targetLocale, context.agentConfiguration, this.dependencies.catalogResolver); + this.presentationCatalogs.set(key, resolution); + return resolution; + } async cleanupSyncBranches(context, operation) { for (const target of operation.reconciliationTargets) { if (!target.syncBranch) @@ -46634,16 +46938,21 @@ function shouldRecordUnexpectedFailure(error) { function semanticCleanupError(error) { return (0, application_error_1.toApplicationError)(error, "workflow.failed", "Deployment cleanup failed."); } -function presentationContext(context) { +function presentationContext(context, operation) { + const locale = effectiveLocale(context, operation); return { owner: context.owner, repository: context.repo, issue: context.singleAction.issue, - issueLocale: context.locale.issue, - pullRequestLocale: context.locale.pullRequest, + repositoryLocale: locale.repository, + issueLocale: locale.issue, + pullRequestLocale: locale.pullRequest, packageName: context.owner === "vypdev" && context.repo === "copilot" ? "@vypdev/copilot" : undefined, }; } +function effectiveLocale(context, operation) { + return operation.locale ?? context.locale; +} function requireTarget(target) { if (!target) throw new application_error_1.ApplicationError("workflow.stale", "Reconciliation target is missing."); @@ -47301,7 +47610,7 @@ class AcceptPromotionHandler { const publishing = { ...promoted, phase: "publishing" }; await this.runtime.persist(context, publishing); await this.runtime.publishDashboard(context, publishing); - await this.runtime.publishMilestone(context, publishing, "promotion-merged", `✅ Promotion PR #${pullRequest.number} merged. Publication is starting from production SHA \`${productionSha}\`.`); + await this.runtime.publishMilestone(context, publishing, { kind: "promotion-merged", pullRequest: pullRequest.number, productionSha }); await this.runtime.dependencies.continuation.dispatch(operation.publicationWorkflow, operation.productionBranch, operation.operationId, context.singleAction.issue, operation.version); return (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Promotion PR #${pullRequest.number} was verified; publication continuation was dispatched from ${operation.productionBranch}.`); } @@ -47374,7 +47683,7 @@ class ConfirmPublicationHandler { lastFailure: null, }; await this.runtime.persist(context, published); - await this.runtime.publishMilestone(context, published, "publication-complete", `📦 ${published.tag} is published from accepted production SHA \`${published.productionSha}\`.`); + await this.runtime.publishMilestone(context, published, { kind: "publication-complete", tag: published.tag, productionSha }); return await this.beginReconciliation(context, published); } async resumeRetryableBlock(context, operation) { @@ -47575,6 +47884,7 @@ class PreparePromotionHandler { const originSha = persistedOrigin ?? await this.runtime.dependencies.git.getMergeBaseSha(originBranch, sourceBranch); const operation = (0, deployment_plan_policy_1.buildInitialDeploymentOperation)({ operationId: this.runtime.dependencies.operationId(), + locale: context.locale, kind, version: context.singleAction.version, title: context.singleAction.title, @@ -47632,6 +47942,7 @@ function selectOriginBranch(context, kind) { function validateOperation(context, operation) { const errors = (0, deployment_plan_policy_1.validateInitialDeploymentInput)({ operationId: operation.operationId, + locale: operation.locale ?? context.locale, kind: operation.kind, version: operation.version, title: operation.title, @@ -47772,7 +48083,7 @@ class ReconciliationHandler { const completed = { ...operation, phase: "completed", lastFailure: null }; await this.runtime.persist(context, completed); await this.runtime.publishDashboard(context, completed); - await this.runtime.publishMilestone(context, completed, "orchestration-complete", `✅ Deployment ${completed.tag} and every configured reconciliation target are complete.`); + await this.runtime.publishMilestone(context, completed, { kind: "orchestration-complete", tag: completed.tag }); return (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Deployment ${completed.tag} completed${completionContext}.`); } async finishOrPresent(context, operation) { @@ -51309,6 +51620,11 @@ function projectDeploymentOrchestrationContext(source) { branches: Object.freeze({ ...source.branches }), workflows: Object.freeze({ ...source.workflows }), locale: Object.freeze({ ...source.locale }), + ...(source.agentConfiguration + ? { agentConfiguration: Object.freeze({ ...source.agentConfiguration }) } + : source.ai + ? { agentConfiguration: Object.freeze({ ...source.ai.getAgentConfiguration('planner') }) } + : {}), labels: Object.freeze({ ...source.labels, lifecycle: Object.freeze({ ...source.labels.lifecycle }), @@ -51498,6 +51814,7 @@ function copyInitialLabels(source) { function copyDeploymentOperation(operation) { return Object.freeze({ ...operation, + ...(operation.locale ? { locale: Object.freeze({ ...operation.locale }) } : {}), reconciliationTargets: Object.freeze((operation.reconciliationTargets ?? []).map(target => Object.freeze({ ...target }))), ...(operation.publicationReceipt ? { publicationReceipt: Object.freeze({ ...operation.publicationReceipt }) } : {}), ...(operation.lastFailure ? { lastFailure: Object.freeze({ ...operation.lastFailure }) } : {}), @@ -72304,6 +72621,7 @@ exports.completeReconciliationTarget = completeReconciliationTarget; exports.sanitizeDeploymentMessage = sanitizeDeploymentMessage; exports.isDeploymentOperationSnapshot = isDeploymentOperationSnapshot; const deployment_configuration_1 = __nccwpck_require__(22495); +const locale_1 = __nccwpck_require__(15386); exports.DEPLOYMENT_PHASES = [ "preparing", "promotion_pr_pending", @@ -72386,6 +72704,7 @@ function isDeploymentOperationSnapshot(value) { && operation.revision > 0 && typeof operation.operationId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operation.operationId) + && (operation.locale === undefined || (0, locale_1.isLocaleProfile)(operation.locale)) && (operation.kind === "release" || operation.kind === "hotfix") && typeof operation.version === "string" && /^[0-9]+\.[0-9]+\.[0-9]+$/.test(operation.version) && typeof operation.title === "string" && operation.title.length <= 1000 @@ -72737,6 +73056,7 @@ exports.canonicalizeLocaleTag = canonicalizeLocaleTag; exports.normalizeLocaleTag = normalizeLocaleTag; exports.resolveLocaleProfile = resolveLocaleProfile; exports.localeForScope = localeForScope; +exports.isLocaleProfile = isLocaleProfile; exports.localeLanguagesMatch = localeLanguagesMatch; exports.baseLanguage = baseLanguage; exports.DEFAULT_REPOSITORY_LOCALE = 'en-US'; @@ -72806,6 +73126,22 @@ function localeForScope(profile, scope) { return profile.pullRequest; return profile.repository; } +function isLocaleProfile(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) + return false; + const candidate = value; + try { + const resolved = resolveLocaleProfile(candidate.repository, candidate.issueOverride ?? '', candidate.pullRequestOverride ?? ''); + return candidate.repository === resolved.repository + && candidate.issue === resolved.issue + && candidate.pullRequest === resolved.pullRequest + && candidate.issueOverride === resolved.issueOverride + && candidate.pullRequestOverride === resolved.pullRequestOverride; + } + catch { + return false; + } +} function localeLanguagesMatch(left, right) { return baseLanguage(canonicalizeLocaleTag(left)) === baseLanguage(canonicalizeLocaleTag(right)); } @@ -73137,7 +73473,7 @@ function validMessageText(value) { function safeDynamicText(value) { return validMessageText(value) && !/[\r\n\u202A-\u202E\u2066-\u2069]/u.test(value) - && !/|<\/?[A-Za-z]|https?:\/\/|```|[`*_[\]~]|(^|\s)\/(?:copilot)(?:\s|$)|@[A-Za-z0-9]/iu.test(value); + && !/|<\/?[A-Za-z]|https?:\/\/|```|[`*_[\]~|]|(^|\s)\/(?:copilot)(?:\s|$)|@[A-Za-z0-9]/iu.test(value); } function pluralPlaceholderParity(message) { const expected = placeholdersForTemplate(message.other); @@ -74994,13 +75330,13 @@ function createSingleActionUseCaseCompositionRoot(surface, binding) { ? new repository_release_publication_repository_1.RepositoryReleasePublicationRepository((0, github_release_client_factory_1.createReleaseClient)()) : undefined; const deploymentOrchestration = surface === "github-workflow" - ? createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, repositoryReleasePort, binding) + ? createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, repositoryReleasePort, binding, catalogResolver) : undefined; return new single_action_use_case_1.SingleActionUseCase(repositoryTagPort && repositoryReleasePort ? new publish_github_action_use_case_1.PublishGithubActionUseCase((0, push_single_action_capability_port_binding_1.bindRepositoryTag)(repositoryTagPort, binding), (0, push_single_action_capability_port_binding_1.bindRepositoryRelease)(repositoryReleasePort, binding)) : undefined, repositoryReleasePort ? new create_release_use_case_1.CreateReleaseUseCase((0, push_single_action_capability_port_binding_1.bindRepositoryRelease)(repositoryReleasePort, binding)) : undefined, repositoryTagPort ? new create_tag_use_case_1.CreateTagUseCase((0, push_single_action_capability_port_binding_1.bindRepositoryTag)(repositoryTagPort, binding)) : undefined, new think_use_case_1.ThinkUseCase((0, shared_capability_port_binding_1.bindIssueDescriptionQuery)(issueDescriptionQueryPort, binding), (0, shared_capability_port_binding_1.bindIssueNotification)((0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, initial_setup_composition_root_1.createInitialSetupCompositionRoot)(binding), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(binding), createDetectPotentialProblemsUseCase(binding), new recommend_steps_use_case_1.RecommendStepsUseCase((0, shared_capability_port_binding_1.bindIssueDescriptionQuery)(issueDescriptionQueryPort, binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, issue_inactivity_composition_root_1.createCloseInactiveIssuesUseCase)(binding), (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), new publish_issue_comment_use_case_1.PublishIssueCommentUseCase((0, push_single_action_capability_port_binding_1.bindIssueCommentPublication)(issueDescriptionQueryPort, binding)), new observe_branch_sync_use_case_1.ObserveBranchSyncUseCase((0, push_single_action_capability_port_binding_1.bindBranchDependencies)(new branch_dependency_repository_1.BranchDependencyRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), binding), (0, push_single_action_capability_port_binding_1.bindBranchComparison)(new branch_compare_repository_1.BranchCompareRepository((0, github_branch_client_factory_1.createBranchComparisonClient)()), binding), (0, push_single_action_capability_port_binding_1.bindBranchSyncNotification)(issueDescriptionQueryPort, binding), catalogResolver), deploymentOrchestration); } -function createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, publication, binding) { +function createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, publication, binding, catalogResolver) { const deploymentClient = new octokit_deployment_adapter_1.OctokitDeploymentClientAdapter(); return new deployment_orchestration_use_case_1.DeploymentOrchestrationUseCase({ pullRequests: (0, push_single_action_capability_port_binding_1.bindManagedPullRequests)(new github_managed_pull_request_repository_1.GithubManagedPullRequestRepository(deploymentClient), binding), @@ -75013,6 +75349,7 @@ function createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, publica labels: (0, push_single_action_capability_port_binding_1.bindDeploymentLabels)((0, issue_labels_composition_root_1.createIssueLabelRepository)(), binding), issues: (0, push_single_action_capability_port_binding_1.bindDeploymentIssues)((0, issue_interaction_composition_root_1.createIssueClosureRepository)(), binding), operationId: node_crypto_1.randomUUID, + catalogResolver, }); } function createIssueCommentUseCaseCompositionRoot(binding) { diff --git a/build/github_action/index.js b/build/github_action/index.js index 3597ec960..0b33dc29f 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -39608,6 +39608,7 @@ const copilot_evidence_policy_1 = __nccwpck_require__(47632); const application_error_1 = __nccwpck_require__(75999); const configuration_persistence_policy_1 = __nccwpck_require__(65388); const deployment_presentation_policy_1 = __nccwpck_require__(83221); +const deployment_message_catalog_1 = __nccwpck_require__(79364); const bugbot_result_finding_state_projection_policy_1 = __nccwpck_require__(98117); const review_state_1 = __nccwpck_require__(79200); async function finishGithubAction(execution, results, issueNotificationPort, configurationStorePort, evidencePort, summaryPort, catalogResolver) { @@ -39658,26 +39659,41 @@ function extractBugbotTelemetry(results) { async function writeActionSummary(execution, summaryPort, catalogResolver) { const operation = execution.currentConfiguration.deploymentOrchestration; const locale = execution.locale ?? { repository: 'en-US', issue: 'en-US', pullRequest: 'en-US' }; - const localizationEvidence = (0, action_summary_policy_1.renderLocalizationSummarySection)({ - repository: locale.repository, - issue: locale.issue, - pullRequest: locale.pullRequest, - }, catalogResolver?.observations?.() ?? []); - const summaryText = execution.singleAction.isDeploymentOrchestrationAction && operation - ? [(0, deployment_presentation_policy_1.renderDeploymentJobSummary)(operation, { - owner: execution.owner, - repository: execution.repo, - issue: execution.singleAction.issue, - issueLocale: locale.issue, - pullRequestLocale: locale.pullRequest, - packageName: execution.owner === 'vypdev' && execution.repo === 'copilot' ? '@vypdev/copilot' : undefined, - workflowRunUrl: process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID - ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` - : undefined, - }, operation.lastFailure?.previousPhase, execution.currentConfiguration.results.flatMap((result) => result.steps ?? [])), localizationEvidence] - .filter(Boolean) - .join('\n\n') - : (0, action_summary_policy_1.buildActionSummary)({ + const summaryLocale = execution.singleAction.isDeploymentOrchestrationAction && operation?.locale + ? operation.locale + : locale; + let body; + let localizationLabels; + if (execution.singleAction.isDeploymentOrchestrationAction && operation) { + const effectiveLocale = operation.locale ?? locale; + const catalog = await (0, deployment_message_catalog_1.resolveDeploymentCatalog)(effectiveLocale.repository, execution.ai.getAgentConfiguration('planner'), catalogResolver); + const messages = (0, deployment_message_catalog_1.deploymentCopy)(catalog); + localizationLabels = { + heading: messages.localization, + property: messages.property, + value: messages.value, + repositoryLocale: messages.repositoryLocaleLabel, + issueLocale: messages.issueLocaleLabel, + pullRequestLocale: messages.pullRequestLocaleLabel, + catalogResolution: messages.catalogResolution, + descriptors: messages.descriptors, + reason: messages.reason, + }; + body = (0, deployment_presentation_policy_1.renderDeploymentJobSummary)(operation, { + owner: execution.owner, + repository: execution.repo, + issue: execution.singleAction.issue, + repositoryLocale: effectiveLocale.repository, + issueLocale: effectiveLocale.issue, + pullRequestLocale: effectiveLocale.pullRequest, + packageName: execution.owner === 'vypdev' && execution.repo === 'copilot' ? '@vypdev/copilot' : undefined, + workflowRunUrl: process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID + ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` + : undefined, + }, operation.lastFailure?.previousPhase, catalog); + } + else { + body = (0, action_summary_policy_1.buildActionSummary)({ owner: execution.owner, repository: execution.repo, eventName: execution.eventName, @@ -39696,6 +39712,13 @@ async function writeActionSummary(execution, summaryPort, catalogResolver) { catalogResolutions: catalogResolver?.observations?.() ?? [], results: execution.currentConfiguration.results, }); + } + const localizationEvidence = (0, action_summary_policy_1.renderLocalizationSummarySection)({ + repository: summaryLocale.repository, + issue: summaryLocale.issue, + pullRequest: summaryLocale.pullRequest, + }, catalogResolver?.observations?.() ?? [], localizationLabels); + const summaryText = [body, localizationEvidence].filter(Boolean).join('\n\n'); if (!summaryPort) return summaryText; try { @@ -41110,6 +41133,17 @@ const application_error_presentation_policy_1 = __nccwpck_require__(95067); const bugbot_telemetry_projection_policy_1 = __nccwpck_require__(43244); const bugbot_result_finding_state_projection_policy_1 = __nccwpck_require__(98117); const review_state_1 = __nccwpck_require__(79200); +const ENGLISH_LOCALIZATION_SUMMARY_LABELS = Object.freeze({ + heading: 'Localization', + property: 'Property', + value: 'Value', + repositoryLocale: 'Repository locale', + issueLocale: 'Issue locale', + pullRequestLocale: 'Pull-request locale', + catalogResolution: 'Catalog resolution', + descriptors: 'descriptors', + reason: 'reason', +}); /** Builds a bounded, publication-safe GitHub Actions Job Summary. */ function buildActionSummary(context) { const failures = context.results.filter(result => !result.success && result.executed); @@ -41155,35 +41189,35 @@ function buildActionSummary(context) { ].join('\n'); } /** Renders the same content-free locale evidence for summaries owned by specialized workflows. */ -function renderLocalizationSummarySection(locale, catalogResolutions = []) { - const rows = localizationSummaryRows(locale, catalogResolutions); +function renderLocalizationSummarySection(locale, catalogResolutions = [], labels = ENGLISH_LOCALIZATION_SUMMARY_LABELS) { + const rows = localizationSummaryRows(locale, catalogResolutions, labels); if (rows.length === 0) return ''; return [ - '## Localization', + `## ${labels.heading}`, '', - '| Property | Value |', + `| ${labels.property} | ${labels.value} |`, '| --- | --- |', ...rows, '', ].join('\n'); } -function localizationSummaryRows(locale, catalogResolutions = []) { +function localizationSummaryRows(locale, catalogResolutions = [], labels = ENGLISH_LOCALIZATION_SUMMARY_LABELS) { return [ ...(locale ? [ - `| Repository locale | \`${escapeTable(locale.repository)}\` |`, - `| Issue locale | \`${escapeTable(locale.issue)}\` |`, - `| Pull-request locale | \`${escapeTable(locale.pullRequest)}\` |`, + `| ${labels.repositoryLocale} | \`${escapeTable(locale.repository)}\` |`, + `| ${labels.issueLocale} | \`${escapeTable(locale.issue)}\` |`, + `| ${labels.pullRequestLocale} | \`${escapeTable(locale.pullRequest)}\` |`, ] : []), ...(catalogResolutions.length ? [ - `| Catalog resolution | ${formatCatalogResolutions(catalogResolutions)} |`, + `| ${labels.catalogResolution} | ${formatCatalogResolutions(catalogResolutions, labels)} |`, ] : []), ]; } -function formatCatalogResolutions(observations) { +function formatCatalogResolutions(observations, labels) { return observations.map(observation => { - const fallback = observation.fallbackReason ? `, reason=${observation.fallbackReason}` : ''; - return `\`${escapeTable(observation.requestedLocale)} -> ${escapeTable(observation.resolvedLocale)} (${observation.source}, descriptors=${observation.descriptorCount}${fallback})\``; + const fallback = observation.fallbackReason ? `, ${labels.reason}=${observation.fallbackReason}` : ''; + return `\`${escapeTable(observation.requestedLocale)} -> ${escapeTable(observation.resolvedLocale)} (${observation.source}, ${labels.descriptors}=${observation.descriptorCount}${fallback})\``; }).join('
'); } function resolveActionSummaryStatus(input) { @@ -44332,6 +44366,244 @@ function projectDeploymentLabels(current, operation, labels) { } +/***/ }), + +/***/ 79364: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEPLOYMENT_CATALOG_DEFINITIONS = exports.SPANISH_DEPLOYMENT_DEFINITION = exports.ENGLISH_DEPLOYMENT_DEFINITION = exports.DEPLOYMENT_MESSAGE_IDS = void 0; +exports.resolveStaticDeploymentCatalog = resolveStaticDeploymentCatalog; +exports.resolveDeploymentCatalog = resolveDeploymentCatalog; +exports.deploymentCopy = deploymentCopy; +const deployment_operation_1 = __nccwpck_require__(92730); +const message_catalog_1 = __nccwpck_require__(27097); +const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); +const SIMPLE_MESSAGE_KEYS = Object.freeze([ + 'release', 'hotfix', 'currentStatus', 'noAction', 'actionRequired', 'progress', + 'currentTransition', 'whatNext', 'links', 'technical', 'alreadyPublished', + 'notPublished', 'productionUpdated', 'developmentSynchronized', 'yes', 'no', + 'from', 'to', 'state', 'compare', 'controlCenter', 'purpose', 'afterMerge', + 'purposePromotion', 'promotionOverview', + 'reconciliationOverview', 'afterPromotion', 'noRepublish', 'origin', + 'preparedSource', 'destination', 'publication', 'productionFact', + 'developmentTarget', 'completionEffect', 'closeIssue', 'keepIssue', + 'readyBeforeReview', 'buildValidation', 'packageSmoke', 'protectedChecks', + 'syncReason', 'protectedFacts', 'cut', 'promotion', 'reconciliation', 'cleanup', + 'jobSummary', 'result', 'externalWait', 'workflowFailure', 'previousPhase', + 'resultingPhase', 'retryable', 'retryAfterCorrection', 'manualIntervention', + 'reviewManagedPr', 'fallback', 'operation', 'strategy', 'pullRequestMode', + 'sourceSha', 'productionSha', 'pending', 'publicationReceipt', 'absent', + 'localization', 'property', 'value', 'repositoryLocaleLabel', + 'issueLocaleLabel', 'pullRequestLocaleLabel', 'catalogResolution', + 'descriptors', 'reason', +]); +const DIAGRAM_KEYS = Object.freeze([ + 'source', 'prepared', 'production', 'accepted', 'publication', + 'reconciliation', 'complete', +]); +const TEMPLATE_MESSAGE_IDS = Object.freeze([ + 'deployment.template.promotionTitle', + 'deployment.template.reconciliationTitle', + 'deployment.template.promotionTechnical', + 'deployment.template.reconciliationTechnical', + 'deployment.template.jobState', + 'deployment.template.admissionScope', + 'deployment.template.sourceBranchLink', + 'deployment.template.originCommitLink', + 'deployment.template.preparedCommitLink', + 'deployment.template.promotionPullRequestLink', + 'deployment.template.reconciliationPullRequestLink', + 'deployment.template.productionCommitLink', + 'deployment.template.releaseLink', + 'deployment.template.actionTagLink', + 'deployment.template.npmLink', + 'deployment.template.workflowRunLink', + 'deployment.template.nextPromotion', + 'deployment.milestone.promotionMerged', + 'deployment.milestone.publicationComplete', + 'deployment.milestone.reconciliationBlocked', + 'deployment.milestone.complete', +]); +exports.DEPLOYMENT_MESSAGE_IDS = Object.freeze([ + ...SIMPLE_MESSAGE_KEYS.map(key => `deployment.${key}`), + ...deployment_operation_1.DEPLOYMENT_PHASES.map(phase => `deployment.phase.${phase}`), + ...DIAGRAM_KEYS.map(key => `deployment.diagram.${key}`), + ...TEMPLATE_MESSAGE_IDS, +]); +const ENGLISH_SIMPLE = Object.freeze({ + release: 'Release', hotfix: 'Hotfix', currentStatus: 'Current status', + noAction: 'No action is required while GitHub owns the pending transition.', actionRequired: 'Action required', + progress: 'Progress', currentTransition: 'Current transition', whatNext: 'What happens next', links: 'Links', + technical: 'Technical details', alreadyPublished: 'Package status: already published', notPublished: 'Package status: not published', + productionUpdated: 'Production updated', developmentSynchronized: 'Development synchronized', yes: 'Yes', no: 'No', + from: 'From', to: 'To', state: 'State', compare: 'Compare changes', controlCenter: 'Release control center', + purpose: 'Purpose', afterMerge: 'After merge', purposePromotion: 'accept the prepared change in production', + promotionOverview: 'Production promotion', reconciliationOverview: 'Development reconciliation', + afterPromotion: 'After merge, Copilot will tag and publish the accepted production commit.', + noRepublish: 'Merging or closing this PR cannot publish the package again.', + origin: 'Origin', preparedSource: 'Prepared source', destination: 'Destination', publication: 'Publication', + productionFact: 'Production fact', developmentTarget: 'Development target', completionEffect: 'Completion effect', + closeIssue: 'Close issue after all targets', keepIssue: 'Keep issue open', readyBeforeReview: 'Ready before review', + buildValidation: 'Build and release validation', packageSmoke: 'Package smoke test', + protectedChecks: 'Protected-branch checks and reviews', + syncReason: 'A dedicated sync branch preserves target-only commits and isolates target-dependent checks.', + protectedFacts: 'What Copilot protected', cut: 'Source cut', promotion: 'Production promotion', + reconciliation: 'Development reconciliation', cleanup: 'Cleanup and issue completion', + jobSummary: 'Deployment orchestration', result: 'Result', externalWait: 'Waiting externally', + workflowFailure: 'Workflow failed', previousPhase: 'Previous phase', resultingPhase: 'Resulting phase', + retryable: 'Retryable', retryAfterCorrection: 'Retry after correcting the cause', + manualIntervention: 'Manual intervention is required', + reviewManagedPr: 'review and merge the managed PR when GitHub reports it ready', + fallback: 'prepared -> production PR -> accepted -> published -> reconciled -> complete', + operation: 'Operation', strategy: 'Strategy', pullRequestMode: 'PR mode', sourceSha: 'Source SHA', + productionSha: 'Production SHA', pending: 'pending', publicationReceipt: 'Publication receipt', absent: 'absent', + localization: 'Localization', property: 'Property', value: 'Value', + repositoryLocaleLabel: 'Repository locale', issueLocaleLabel: 'Issue locale', + pullRequestLocaleLabel: 'Pull-request locale', catalogResolution: 'Catalog resolution', + descriptors: 'descriptors', reason: 'reason', +}); +const SPANISH_SIMPLE = Object.freeze({ + release: 'Release', hotfix: 'Hotfix', currentStatus: 'Estado actual', + noAction: 'No se requiere ninguna acción mientras GitHub gestiona la transición pendiente.', actionRequired: 'Acción necesaria', + progress: 'Progreso', currentTransition: 'Transición actual', whatNext: 'Qué ocurrirá después', links: 'Enlaces', + technical: 'Detalles técnicos', alreadyPublished: 'Estado del paquete: ya publicado', notPublished: 'Estado del paquete: no publicado', + productionUpdated: 'Producción actualizada', developmentSynchronized: 'Desarrollo sincronizado', yes: 'Sí', no: 'No', + from: 'Origen', to: 'Destino', state: 'Estado', compare: 'Comparar cambios', controlCenter: 'Centro de control de la release', + purpose: 'Propósito', afterMerge: 'Después del merge', purposePromotion: 'aceptar en producción el cambio preparado', + promotionOverview: 'Promoción a producción', reconciliationOverview: 'Reconciliación con desarrollo', + afterPromotion: 'Tras el merge, Copilot etiquetará y publicará el commit aceptado en producción.', + noRepublish: 'Mergear o cerrar esta PR no puede volver a publicar el paquete.', + origin: 'Origen', preparedSource: 'Fuente preparada', destination: 'Destino', publication: 'Publicación', + productionFact: 'Estado de producción', developmentTarget: 'Destino de desarrollo', completionEffect: 'Efecto al completar', + closeIssue: 'Cerrar la issue tras todos los destinos', keepIssue: 'Mantener la issue abierta', readyBeforeReview: 'Listo antes de revisar', + buildValidation: 'Build y validación de release', packageSmoke: 'Smoke test del paquete', + protectedChecks: 'Checks y revisiones de la rama protegida', + syncReason: 'Una rama de sincronización dedicada preserva los commits exclusivos del destino y aísla sus checks.', + protectedFacts: 'Qué ha protegido Copilot', cut: 'Corte de la fuente', promotion: 'Promoción a producción', + reconciliation: 'Reconciliación con desarrollo', cleanup: 'Limpieza y cierre de la issue', + jobSummary: 'Orquestación del despliegue', result: 'Resultado', externalWait: 'Esperando fuera del workflow', + workflowFailure: 'Workflow fallido', previousPhase: 'Fase anterior', resultingPhase: 'Fase resultante', + retryable: 'Reintentable', retryAfterCorrection: 'Vuelve a intentarlo después de corregir la causa', + manualIntervention: 'Se requiere intervención manual', + reviewManagedPr: 'revisa y mergea la PR gestionada cuando GitHub indique que está lista', + fallback: 'preparada -> PR de producción -> aceptada -> publicada -> reconciliada -> completada', + operation: 'Operación', strategy: 'Estrategia', pullRequestMode: 'Modo de PR', sourceSha: 'SHA de origen', + productionSha: 'SHA de producción', pending: 'pendiente', publicationReceipt: 'Recibo de publicación', absent: 'ausente', + localization: 'Localización', property: 'Propiedad', value: 'Valor', + repositoryLocaleLabel: 'Locale del repositorio', issueLocaleLabel: 'Locale de la issue', + pullRequestLocaleLabel: 'Locale de la pull request', catalogResolution: 'Resolución del catálogo', + descriptors: 'descriptores', reason: 'motivo', +}); +const ENGLISH_PHASES = Object.freeze({ + preparing: 'preparing the version', promotion_pr_pending: 'waiting for production approval', + promoted: 'accepted in production', publishing: 'publishing artifacts', + published: 'published; preparing development reconciliation', + reconciliation_pending: 'waiting for development reconciliation', completed: 'completed', blocked: 'needs attention', +}); +const SPANISH_PHASES = Object.freeze({ + preparing: 'preparando la versión', promotion_pr_pending: 'esperando aprobación en producción', + promoted: 'aceptada en producción', publishing: 'publicando artefactos', + published: 'publicada; preparando la reconciliación', + reconciliation_pending: 'esperando reconciliación con desarrollo', completed: 'completada', blocked: 'necesita atención', +}); +const ENGLISH_DIAGRAM = Object.freeze({ + source: 'Source snapshot', prepared: 'Version prepared', production: 'Production PR', + accepted: 'Accepted in production', publication: 'Package and release', + reconciliation: 'Development reconciliation', complete: 'Complete', +}); +const SPANISH_DIAGRAM = Object.freeze({ + source: 'Snapshot de origen', prepared: 'Versión preparada', production: 'PR de producción', + accepted: 'Aceptada en producción', publication: 'Paquete y release', + reconciliation: 'Reconciliación con desarrollo', complete: 'Completada', +}); +const ENGLISH_TEMPLATES = Object.freeze({ + 'deployment.template.promotionTitle': '{kind}({version}): promote to {branch}', + 'deployment.template.reconciliationTitle': '{kind}({version}): reconcile {source} into {target}', + 'deployment.template.promotionTechnical': 'Operation {operation}; strategy {strategy}; merge mode {mergeMode}.', + 'deployment.template.reconciliationTechnical': 'Operation {operation}; source SHA {sourceSha}.', + 'deployment.template.jobState': 'State: version {version}, revision {revision}', + 'deployment.template.admissionScope': 'Admission scope: repository + launcher issue #{issue}', + 'deployment.template.sourceBranchLink': '{branch} branch', + 'deployment.template.originCommitLink': '{commit} origin commit', + 'deployment.template.preparedCommitLink': '{commit} prepared commit', + 'deployment.template.promotionPullRequestLink': 'Promotion PR #{number}', + 'deployment.template.reconciliationPullRequestLink': 'Reconciliation PR #{number}', + 'deployment.template.productionCommitLink': '{commit} production commit', + 'deployment.template.releaseLink': '{tag} GitHub Release', + 'deployment.template.actionTagLink': '{tag} Action tag', + 'deployment.template.npmLink': '{package} version {version} on npm', + 'deployment.template.workflowRunLink': 'Workflow run', + 'deployment.template.nextPromotion': '{source} is prepared for promotion to {production}.', + 'deployment.milestone.promotionMerged': '✅ Promotion PR #{number} merged. Publication is starting from production SHA {productionSha}.', + 'deployment.milestone.publicationComplete': '📦 {tag} is published from accepted production SHA {productionSha}.', + 'deployment.milestone.reconciliationBlocked': '❌ Deployment blocked: {reason}', + 'deployment.milestone.complete': '✅ Deployment {tag} and every configured reconciliation target are complete.', +}); +const SPANISH_TEMPLATES = Object.freeze({ + 'deployment.template.promotionTitle': '{kind}({version}): promover a {branch}', + 'deployment.template.reconciliationTitle': '{kind}({version}): reconciliar {source} con {target}', + 'deployment.template.promotionTechnical': 'Operación {operation}; estrategia {strategy}; modo de merge {mergeMode}.', + 'deployment.template.reconciliationTechnical': 'Operación {operation}; SHA de origen {sourceSha}.', + 'deployment.template.jobState': 'Estado: versión {version}, revisión {revision}', + 'deployment.template.admissionScope': 'Ámbito de admisión: repositorio + issue de lanzamiento #{issue}', + 'deployment.template.sourceBranchLink': 'Rama {branch}', + 'deployment.template.originCommitLink': 'Commit de origen {commit}', + 'deployment.template.preparedCommitLink': 'Commit preparado {commit}', + 'deployment.template.promotionPullRequestLink': 'PR de promoción #{number}', + 'deployment.template.reconciliationPullRequestLink': 'PR de reconciliación #{number}', + 'deployment.template.productionCommitLink': 'Commit de producción {commit}', + 'deployment.template.releaseLink': 'Release de GitHub {tag}', + 'deployment.template.actionTagLink': 'Tag de Action {tag}', + 'deployment.template.npmLink': '{package}, versión {version}, en npm', + 'deployment.template.workflowRunLink': 'Ejecución del workflow', + 'deployment.template.nextPromotion': '{source} está preparada para promoverse a {production}.', + 'deployment.milestone.promotionMerged': '✅ La PR de promoción #{number} se ha mergeado. La publicación comienza desde el SHA de producción {productionSha}.', + 'deployment.milestone.publicationComplete': '📦 {tag} se ha publicado desde el SHA de producción aceptado {productionSha}.', + 'deployment.milestone.reconciliationBlocked': '❌ Despliegue bloqueado: {reason}', + 'deployment.milestone.complete': '✅ El despliegue {tag} y todos los destinos de reconciliación configurados se han completado.', +}); +function catalogMessages(simple, phases, diagram, templates) { + return Object.freeze({ + ...Object.fromEntries(SIMPLE_MESSAGE_KEYS.map(key => [`deployment.${key}`, simple[key]])), + ...Object.fromEntries(deployment_operation_1.DEPLOYMENT_PHASES.map(phase => [`deployment.phase.${phase}`, phases[phase]])), + ...Object.fromEntries(DIAGRAM_KEYS.map(key => [`deployment.diagram.${key}`, diagram[key]])), + ...templates, + }); +} +exports.ENGLISH_DEPLOYMENT_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, + locale: 'en-US', + compatibleBaseLanguage: 'en', + messages: catalogMessages(ENGLISH_SIMPLE, ENGLISH_PHASES, ENGLISH_DIAGRAM, ENGLISH_TEMPLATES), +}); +exports.SPANISH_DEPLOYMENT_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, + locale: 'es-ES', + compatibleBaseLanguage: 'es', + messages: catalogMessages(SPANISH_SIMPLE, SPANISH_PHASES, SPANISH_DIAGRAM, SPANISH_TEMPLATES), +}); +exports.DEPLOYMENT_CATALOG_DEFINITIONS = Object.freeze([ + exports.ENGLISH_DEPLOYMENT_DEFINITION, + exports.SPANISH_DEPLOYMENT_DEFINITION, +]); +function resolveStaticDeploymentCatalog(locale) { + return (0, resolved_message_catalog_policy_1.resolveStaticMessageCatalogView)(locale, exports.ENGLISH_DEPLOYMENT_DEFINITION, exports.DEPLOYMENT_CATALOG_DEFINITIONS); +} +function resolveDeploymentCatalog(locale, configuration, resolver) { + return (0, resolved_message_catalog_policy_1.resolveMessageCatalogView)(locale, exports.DEPLOYMENT_MESSAGE_IDS, exports.ENGLISH_DEPLOYMENT_DEFINITION, exports.DEPLOYMENT_CATALOG_DEFINITIONS, configuration, resolver); +} +function deploymentCopy(catalog) { + return Object.freeze({ + ...Object.fromEntries(SIMPLE_MESSAGE_KEYS.map(key => [key, catalog.message(`deployment.${key}`)])), + phase: Object.freeze(Object.fromEntries(deployment_operation_1.DEPLOYMENT_PHASES.map(phase => [phase, catalog.message(`deployment.phase.${phase}`)]))), + diagram: Object.freeze(Object.fromEntries(DIAGRAM_KEYS.map(key => [key, catalog.message(`deployment.diagram.${key}`)]))), + }); +} + + /***/ }), /***/ 8352: @@ -44359,6 +44631,7 @@ function buildInitialDeploymentOperation(input) { stateVersion: deployment_operation_1.DEPLOYMENT_STATE_VERSION, revision: 0, operationId: input.operationId, + locale: Object.freeze({ ...input.locale }), kind: input.kind, version: input.version, title: input.title, @@ -44540,74 +44813,17 @@ exports.renderDeploymentDashboard = renderDeploymentDashboard; exports.renderPromotionPullRequest = renderPromotionPullRequest; exports.renderReconciliationPullRequest = renderReconciliationPullRequest; exports.renderDeploymentJobSummary = renderDeploymentJobSummary; -exports.normalizeLocale = normalizeLocale; +exports.renderDeploymentMilestone = renderDeploymentMilestone; const managed_pull_request_1 = __nccwpck_require__(95914); +const deployment_message_catalog_1 = __nccwpck_require__(79364); const publication_identity_policy_1 = __nccwpck_require__(45403); -const EN = { - release: "Release", hotfix: "Hotfix", currentStatus: "Current status", - noAction: "No action is required while GitHub owns the pending transition.", actionRequired: "Action required", - progress: "Progress", currentTransition: "Current transition", whatNext: "What happens next", links: "Links", - technical: "Technical details", alreadyPublished: "Package status: already published", notPublished: "Package status: not published", - productionUpdated: "Production updated", developmentSynchronized: "Development synchronized", yes: "Yes", no: "No", - from: "From", to: "To", state: "State", compare: "Compare changes", controlCenter: "Release control center", - purpose: "Purpose", afterMerge: "After merge", purposePromotion: "accept the prepared change in production", - purposeReconciliation: "bring the accepted production state back to the development line", - afterPromotion: "After merge, Copilot will tag and publish the accepted production commit.", - noRepublish: "Merging or closing this PR cannot publish the package again.", - origin: "Origin", preparedSource: "Prepared source", destination: "Destination", publication: "Publication", - productionFact: "Production fact", developmentTarget: "Development target", completionEffect: "Completion effect", - closeIssue: "Close issue after all targets", keepIssue: "Keep issue open", readyBeforeReview: "Ready before review", - buildValidation: "Build and release validation", packageSmoke: "Package smoke test", - protectedChecks: "Protected-branch checks and reviews", syncReason: "A dedicated sync branch preserves target-only commits and isolates target-dependent checks.", - protectedFacts: "What Copilot protected", cut: "Source cut", promotion: "Production promotion", reconciliation: "Development reconciliation", - cleanup: "Cleanup and issue completion", jobSummary: "Deployment orchestration", result: "Result", - externalWait: "Waiting externally", workflowFailure: "Workflow failed", previousPhase: "Previous phase", resultingPhase: "Resulting phase", - retryable: "Retryable", createdReused: "Created, reused, or skipped", fallback: "prepared -> production PR -> accepted -> published -> reconciled -> complete", - retryAfterCorrection: "Retry after correcting the cause", manualIntervention: "Manual intervention is required", - reviewManagedPr: "review and merge the managed PR when GitHub reports it ready", - phase: { - preparing: "preparing the version", promotion_pr_pending: "waiting for production approval", promoted: "accepted in production", - publishing: "publishing artifacts", published: "published; preparing development reconciliation", - reconciliation_pending: "waiting for development reconciliation", completed: "completed", blocked: "needs attention", - }, - diagram: ["Source snapshot", "Version prepared", "Production PR", "Accepted in production", "Package and release", "Development reconciliation", "Complete"], -}; -const ES = { - release: "Release", hotfix: "Hotfix", currentStatus: "Estado actual", - noAction: "No se requiere ninguna acción mientras GitHub gestiona la transición pendiente.", actionRequired: "Acción necesaria", - progress: "Progreso", currentTransition: "Transición actual", whatNext: "Qué ocurrirá después", links: "Enlaces", - technical: "Detalles técnicos", alreadyPublished: "Estado del paquete: ya publicado", notPublished: "Estado del paquete: no publicado", - productionUpdated: "Producción actualizada", developmentSynchronized: "Desarrollo sincronizado", yes: "Sí", no: "No", - from: "Origen", to: "Destino", state: "Estado", compare: "Comparar cambios", controlCenter: "Centro de control de la release", - purpose: "Propósito", afterMerge: "Después del merge", purposePromotion: "aceptar en producción el cambio preparado", - purposeReconciliation: "llevar el estado aceptado en producción de vuelta a desarrollo", - afterPromotion: "Tras el merge, Copilot etiquetará y publicará el commit aceptado en producción.", - noRepublish: "Mergear o cerrar esta PR no puede volver a publicar el paquete.", - origin: "Origen", preparedSource: "Fuente preparada", destination: "Destino", publication: "Publicación", - productionFact: "Estado de producción", developmentTarget: "Destino de desarrollo", completionEffect: "Efecto al completar", - closeIssue: "Cerrar la issue tras todos los destinos", keepIssue: "Mantener la issue abierta", readyBeforeReview: "Listo antes de revisar", - buildValidation: "Build y validación de release", packageSmoke: "Smoke test del paquete", - protectedChecks: "Checks y revisiones de la rama protegida", syncReason: "Una rama de sincronización dedicada preserva los commits exclusivos del destino y aísla sus checks.", - protectedFacts: "Qué ha protegido Copilot", cut: "Corte de la fuente", promotion: "Promoción a producción", reconciliation: "Reconciliación con desarrollo", - cleanup: "Limpieza y cierre de la issue", jobSummary: "Orquestación del despliegue", result: "Resultado", - externalWait: "Esperando fuera del workflow", workflowFailure: "Workflow fallido", previousPhase: "Fase anterior", resultingPhase: "Fase resultante", - retryable: "Reintentable", createdReused: "Creado, reutilizado u omitido", fallback: "preparada -> PR de producción -> aceptada -> publicada -> reconciliada -> completada", - retryAfterCorrection: "Vuelve a intentarlo después de corregir la causa", manualIntervention: "Se requiere intervención manual", - reviewManagedPr: "revisa y mergea la PR gestionada cuando GitHub indique que está lista", - phase: { - preparing: "preparando la versión", promotion_pr_pending: "esperando aprobación en producción", promoted: "aceptada en producción", - publishing: "publicando artefactos", published: "publicada; preparando la reconciliación", - reconciliation_pending: "esperando reconciliación con desarrollo", completed: "completada", blocked: "necesita atención", - }, - diagram: ["Snapshot de origen", "Versión preparada", "PR de producción", "Aceptada en producción", "Paquete y release", "Reconciliación con desarrollo", "Completada"], -}; -exports.DEPLOYMENT_DASHBOARD_MARKER = "copilot-deployment-dashboard"; +exports.DEPLOYMENT_DASHBOARD_MARKER = 'copilot-deployment-dashboard'; function deploymentDashboardMarker(operationId, issue) { return ``; } -function renderDeploymentDashboard(operation, context) { - const messages = messagesFor(context.issueLocale); - const title = operation.kind === "release" ? messages.release : messages.hotfix; +function renderDeploymentDashboard(operation, context, catalog = (0, deployment_message_catalog_1.resolveStaticDeploymentCatalog)(context.issueLocale)) { + const messages = (0, deployment_message_catalog_1.deploymentCopy)(catalog); + const title = operation.kind === 'release' ? messages.release : messages.hotfix; const action = deploymentAction(operation, messages); const lines = [ (0, publication_identity_policy_1.buildPublicationMarker)({ @@ -44617,121 +44833,174 @@ function renderDeploymentDashboard(operation, context) { key: `operation:${(0, publication_identity_policy_1.createSemanticDigest)(operation.operationId)}`, }, sourceVersion: `revision:${operation.revision}`, - digest: (0, publication_identity_policy_1.createSemanticDigest)({ locale: context.issueLocale, operation }), + digest: (0, publication_identity_policy_1.createSemanticDigest)({ + requestedLocale: catalog.requestedLocale, + resolvedLocale: catalog.locale, + operation, + }), }), - deploymentDashboardMarker(operation.operationId, context.issue), "", - `# ${operation.phase === "blocked" ? "❌" : operation.phase === "completed" ? "✅" : "🚀"} ${title} ${inline(operation.version)}`, "", + deploymentDashboardMarker(operation.operationId, context.issue), '', + `## ${operation.phase === 'blocked' ? '❌' : operation.phase === 'completed' ? '✅' : '🚀'} ${title} ${inline(operation.version)}`, '', `> **${messages.currentStatus}: ${messages.phase[operation.phase]}.**`, ]; if (action.required) - lines.push("", `## ${messages.actionRequired}`, "", action.message); + lines.push('', `### ${messages.actionRequired}`, '', action.message); else lines.push(`> ${action.message}`); - lines.push(""); - if (operation.phase === "blocked") - lines.push(...factTable(operation, messages), "", `## ${messages.protectedFacts}`, "", protectedFact(operation, messages), ""); - if (operation.presentationMode !== "quiet") - lines.push(`## ${messages.progress}`, "", ...progressLines(operation, messages), ""); - if (operation.presentationMode === "guided" && operation.diagrams) - lines.push(...deploymentDiagram(messages), ""); - if (operation.presentationMode !== "quiet") { - lines.push(`## ${messages.currentTransition}`, "", ...transitionTable(operation, messages), "", `## ${messages.whatNext}`, "", nextDescription(operation, messages), "", `## ${messages.links}`, "", deploymentLinks(operation, context, messages).join(" · "), ""); - } - lines.push("
", `${messages.technical}`, "", `- Operation: ${inline(operation.operationId)}`, `- Strategy: ${inline(operation.strategy)}`, `- PR mode: ${inline(operation.selectedPrMode ?? operation.prMode)}`, `- Source SHA: ${inline(operation.sourceSha)}`, `- Production SHA: ${inline(operation.productionSha ?? "pending")}`, "
"); - return lines.join("\n"); -} -function renderPromotionPullRequest(operation, context) { - const messages = messagesFor(context.pullRequestLocale); - const kind = operation.kind === "release" ? "release" : "hotfix"; - const title = `${kind}(${safeText(operation.version)}): promote to ${safeText(operation.productionBranch)}`; + lines.push(''); + if (operation.phase === 'blocked') { + lines.push(...factTable(operation, messages), '', `### ${messages.protectedFacts}`, '', protectedFact(operation, messages), ''); + } + if (operation.presentationMode !== 'quiet') { + lines.push(`### ${messages.progress}`, '', ...progressLines(operation, messages), ''); + } + if (operation.presentationMode === 'guided' && operation.diagrams) { + lines.push(...deploymentDiagram(messages), ''); + } + if (operation.presentationMode !== 'quiet') { + lines.push(`### ${messages.currentTransition}`, '', ...transitionTable(operation, messages), ''); + if (operation.phase !== 'blocked') { + lines.push(`### ${messages.whatNext}`, '', nextDescription(operation, messages, catalog), ''); + } + lines.push(`### ${messages.links}`, '', deploymentLinks(operation, context, messages, catalog).join(' · '), ''); + } + lines.push('
', `${messages.technical}`, '', `- ${messages.operation}: ${inline(operation.operationId)}`, `- ${messages.strategy}: ${inline(operation.strategy)}`, `- ${messages.pullRequestMode}: ${inline(operation.selectedPrMode ?? operation.prMode)}`, `- ${messages.sourceSha}: ${inline(operation.sourceSha)}`, `- ${messages.productionSha}: ${inline(operation.productionSha ?? messages.pending)}`, '
'); + return lines.join('\n'); +} +function renderPromotionPullRequest(operation, context, catalog = (0, deployment_message_catalog_1.resolveStaticDeploymentCatalog)(context.pullRequestLocale)) { + const messages = (0, deployment_message_catalog_1.deploymentCopy)(catalog); + const kind = operation.kind; + const title = catalog.message('deployment.template.promotionTitle', { + kind, + version: safeText(operation.version), + branch: safeText(operation.productionBranch), + }).slice(0, 240); const body = [ - `# 🚀 ${capitalize(messages.purposePromotion)}`, "", - `> **${messages.purpose}:** ${capitalize(messages.purposePromotion)}.`, - `> **${messages.afterMerge}:** ${messages.afterPromotion}`, "", + `## 🚀 ${messages.promotionOverview}`, '', + `> **${messages.purpose}:** ${messages.purposePromotion}.`, '', `| ${messages.origin} | ${messages.preparedSource} | ${messages.destination} | ${messages.publication} |`, - "|---|---|---|---|", - `| ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)} | ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)} | ${inline(operation.productionBranch)} | ${messages.afterMerge} |`, "", - `## ${messages.readyBeforeReview}`, "", - `- ✅ ${messages.buildValidation}`, `- ✅ ${messages.packageSmoke}`, `- ⏳ ${messages.protectedChecks}`, "", - `## ${messages.afterMerge}`, "", - `- ${messages.afterPromotion}`, `- ${messages.reconciliation}: ${inline(operation.developmentBranch)}.`, "", - `[${messages.compare}](${compareUrl(context, operation.productionBranch, operation.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, "", - "
", `${messages.technical}`, "", - `Operation ${inline(operation.operationId)}; strategy ${inline(operation.strategy)}; merge mode ${inline(operation.prMode)}.`, - "
", "", (0, managed_pull_request_1.buildManagedPullRequestMarker)({ operationId: operation.operationId, phase: "promotion", issue: context.issue }), - ].join("\n"); + '|---|---|---|---|', + `| ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)} | ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)} | ${inline(operation.productionBranch)} | ${messages.notPublished} |`, '', + `### ${messages.readyBeforeReview}`, '', + `- ✅ ${messages.buildValidation}`, `- ✅ ${messages.packageSmoke}`, `- ⏳ ${messages.protectedChecks}`, '', + `### ${messages.afterMerge}`, '', + `- ${messages.afterPromotion}`, `- ${messages.reconciliation}: ${inline(operation.developmentBranch)}.`, '', + `[${messages.compare}](${compareUrl(context, operation.productionBranch, operation.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, '', + '
', `${messages.technical}`, '', + catalog.message('deployment.template.promotionTechnical', { + operation: inline(operation.operationId), + strategy: inline(operation.strategy), + mergeMode: inline(operation.prMode), + }), + '
', '', + (0, managed_pull_request_1.buildManagedPullRequestMarker)({ operationId: operation.operationId, phase: 'promotion', issue: context.issue }), + ].join('\n'); return { title, body }; } -function renderReconciliationPullRequest(operation, target, context) { - const messages = messagesFor(context.pullRequestLocale); - const kind = operation.kind === "release" ? "release" : "hotfix"; - const title = `${kind}(${safeText(operation.version)}): reconcile ${safeText(target.sourceBranch)} into ${safeText(target.targetBranch)}`; +function renderReconciliationPullRequest(operation, target, context, catalog = (0, deployment_message_catalog_1.resolveStaticDeploymentCatalog)(context.pullRequestLocale)) { + const messages = (0, deployment_message_catalog_1.deploymentCopy)(catalog); + const kind = operation.kind; + const title = catalog.message('deployment.template.reconciliationTitle', { + kind, + version: safeText(operation.version), + source: safeText(target.sourceBranch), + target: safeText(target.targetBranch), + }).slice(0, 240); const body = [ - `# 🔄 ${capitalize(messages.purposeReconciliation)}`, "", - `> **${messages.alreadyPublished}.** ${messages.noRepublish}`, "", - `| ${messages.productionFact} | ${messages.developmentTarget} | ${messages.completionEffect} |`, "|---|---|---|", - `| ${inline(`${operation.tag}@${shortSha(operation.productionSha ?? target.sourceSha)}`)} | ${inline(target.targetBranch)} | ${operation.issueCompletion === "close" ? messages.closeIssue : messages.keepIssue} |`, "", - ...(target.syncBranch ? [`${messages.syncReason} ${inline(target.syncBranch)}`, ""] : []), - `${messages.noRepublish}`, "", - `[${messages.compare}](${compareUrl(context, target.targetBranch, target.syncBranch ?? target.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, "", - "
", `${messages.technical}`, "", `Operation ${inline(operation.operationId)}; source SHA ${inline(target.sourceSha)}.`, - "
", "", (0, managed_pull_request_1.buildManagedPullRequestMarker)({ operationId: operation.operationId, phase: "reconciliation", issue: context.issue }), - ].join("\n"); + `## 🔄 ${messages.reconciliationOverview}`, '', + `> **${messages.alreadyPublished}.** ${messages.noRepublish}`, '', + `| ${messages.productionFact} | ${messages.developmentTarget} | ${messages.completionEffect} |`, '|---|---|---|', + `| ${inline(`${operation.tag}@${shortSha(operation.productionSha ?? target.sourceSha)}`)} | ${inline(target.targetBranch)} | ${operation.issueCompletion === 'close' ? messages.closeIssue : messages.keepIssue} |`, '', + ...(target.syncBranch ? [`${messages.syncReason} ${inline(target.syncBranch)}`, ''] : []), + `[${messages.compare}](${compareUrl(context, target.targetBranch, target.syncBranch ?? target.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, '', + '
', `${messages.technical}`, '', + catalog.message('deployment.template.reconciliationTechnical', { + operation: inline(operation.operationId), + sourceSha: inline(target.sourceSha), + }), + '
', '', + (0, managed_pull_request_1.buildManagedPullRequestMarker)({ operationId: operation.operationId, phase: 'reconciliation', issue: context.issue }), + ].join('\n'); return { title, body }; } -function renderDeploymentJobSummary(operation, context, previousPhase, operations = []) { - const messages = messagesFor(context.issueLocale); - const externallyPending = operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending"; - const result = operation.phase === "blocked" ? messages.workflowFailure : externallyPending ? messages.externalWait : messages.phase[operation.phase]; +function renderDeploymentJobSummary(operation, context, previousPhase, catalog = (0, deployment_message_catalog_1.resolveStaticDeploymentCatalog)(context.repositoryLocale)) { + const messages = (0, deployment_message_catalog_1.deploymentCopy)(catalog); + const externallyPending = operation.phase === 'promotion_pr_pending' || operation.phase === 'reconciliation_pending'; + const result = operation.phase === 'blocked' + ? messages.workflowFailure + : externallyPending ? messages.externalWait : messages.phase[operation.phase]; const lines = [ - `# ${operation.phase === "blocked" ? "❌" : externallyPending ? "⏳" : "✅"} ${messages.jobSummary}`, "", - `> **${messages.result}: ${result}.**`, "", - `| ${messages.previousPhase} | ${messages.resultingPhase} | ${messages.retryable} |`, "|---|---|---|", - `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${operation.lastFailure?.retryable ? messages.yes : messages.no} |`, "", - `- Operation: ${inline(operation.operationId)}`, - `- State: version ${operation.stateVersion}, revision ${operation.revision}`, - `- Admission scope: repository + launcher issue #${context.issue}`, + `# ${operation.phase === 'blocked' ? '❌' : externallyPending ? '⏳' : '✅'} ${messages.jobSummary}`, '', + `> **${messages.result}: ${result}.**`, '', + `| ${messages.previousPhase} | ${messages.resultingPhase} | ${messages.retryable} |`, '|---|---|---|', + `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${operation.lastFailure?.retryable ? messages.yes : messages.no} |`, '', + `- ${messages.operation}: ${inline(operation.operationId)}`, + `- ${catalog.message('deployment.template.jobState', { version: operation.stateVersion, revision: operation.revision })}`, + `- ${catalog.message('deployment.template.admissionScope', { issue: context.issue })}`, `- ${messages.origin}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, `- ${messages.preparedSource}: ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)}`, - `- ${messages.productionFact}: ${inline(operation.productionSha ? `${operation.productionBranch}@${shortSha(operation.productionSha)}` : "pending")}`, - `- ${messages.publication}: ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, - `- Publication receipt: ${operation.publicationReceipt ? inline(`${operation.publicationReceipt.tag}@${shortSha(operation.publicationReceipt.productionSha)}`) : "absent"}`, - `- ${messages.createdReused}: ${safeText(operations.join(", ") || "none")}`, "", + `- ${messages.productionFact}: ${inline(operation.productionSha ? `${operation.productionBranch}@${shortSha(operation.productionSha)}` : messages.pending)}`, + `- ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, + `- ${messages.publicationReceipt}: ${operation.publicationReceipt + ? inline(`${operation.publicationReceipt.tag}@${shortSha(operation.publicationReceipt.productionSha)}`) + : messages.absent}`, '', ]; - if (operation.phase === "blocked") { - lines.push(`## ${messages.actionRequired}`, "", `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${operation.lastFailure?.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`, ""); + if (operation.phase === 'blocked') { + lines.push(`## ${messages.actionRequired}`, '', `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${operation.lastFailure?.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`, ''); + } + lines.push(deploymentLinks(operation, context, messages, catalog).join(' · ')); + return lines.join('\n'); +} +function renderDeploymentMilestone(milestone, catalog) { + switch (milestone.kind) { + case 'promotion-merged': + return catalog.message('deployment.milestone.promotionMerged', { + number: milestone.pullRequest, + productionSha: inline(milestone.productionSha), + }); + case 'publication-complete': + return catalog.message('deployment.milestone.publicationComplete', { + tag: inline(milestone.tag), + productionSha: inline(milestone.productionSha), + }); + case 'reconciliation-blocked': + return catalog.message('deployment.milestone.reconciliationBlocked', { reason: safeText(milestone.reason) }); + case 'orchestration-complete': + return catalog.message('deployment.milestone.complete', { tag: inline(milestone.tag) }); } - lines.push(deploymentLinks(operation, context, messages).join(" · ")); - return lines.join("\n"); } function progressLines(operation, messages) { - const phase = operation.phase === "blocked" ? operation.lastFailure?.previousPhase ?? "preparing" : operation.phase; + const phase = operation.phase === 'blocked' ? operation.lastFailure?.previousPhase ?? 'preparing' : operation.phase; const reached = (expected) => phaseRank(phase) >= phaseRank(expected); return [ `- [x] ${messages.cut}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, `- [x] ${messages.buildValidation} + ${messages.packageSmoke}`, - `- [${operation.productionSha || reached("promoted") ? "x" : " "}] ${messages.promotion}: ${inline(operation.productionBranch)}`, - `- [${operation.publicationVerified ? "x" : " "}] ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, - `- [${reconciliationCompleted(operation) ? "x" : " "}] ${messages.reconciliation}: ${inline(operation.developmentBranch)}`, - `- [${operation.phase === "completed" ? "x" : " "}] ${messages.cleanup}`, + `- [${operation.productionSha || reached('promoted') ? 'x' : ' '}] ${messages.promotion}: ${inline(operation.productionBranch)}`, + `- [${operation.publicationVerified ? 'x' : ' '}] ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, + `- [${reconciliationCompleted(operation) ? 'x' : ' '}] ${messages.reconciliation}: ${inline(operation.developmentBranch)}`, + `- [${operation.phase === 'completed' ? 'x' : ' '}] ${messages.cleanup}`, ]; } function deploymentDiagram(messages) { - const [source, prepared, production, accepted, publication, reconciliation, complete] = messages.diagram; + const { source, prepared, production, accepted, publication, reconciliation, complete } = messages.diagram; return [ - "```mermaid", "flowchart LR", ` D[${source}] --> R[${prepared}]`, ` R --> P[${production}]`, - ` P --> A[${accepted}]`, ` A --> N[${publication}]`, ` N --> B[${reconciliation}]`, ` B --> C[${complete}]`, "```", "", messages.fallback, + '```mermaid', 'flowchart LR', ` D[${source}] --> R[${prepared}]`, ` R --> P[${production}]`, + ` P --> A[${accepted}]`, ` A --> N[${publication}]`, ` N --> B[${reconciliation}]`, + ` B --> C[${complete}]`, '```', '', messages.fallback, ]; } function transitionTable(operation, messages) { - const activeTarget = operation.reconciliationTargets.find((target) => target.status !== "completed"); + const activeTarget = operation.reconciliationTargets.find(target => target.status !== 'completed'); const from = activeTarget?.syncBranch ?? activeTarget?.sourceBranch ?? operation.sourceBranch; const to = activeTarget?.targetBranch ?? operation.productionBranch; - return [`| ${messages.from} | ${messages.to} | ${messages.state} |`, "|---|---|---|", `| ${inline(from)} | ${inline(to)} | ${inline(messages.phase[operation.phase])} |`]; + return [ + `| ${messages.from} | ${messages.to} | ${messages.state} |`, '|---|---|---|', + `| ${inline(from)} | ${inline(to)} | ${safeText(messages.phase[operation.phase])} |`, + ]; } function factTable(operation, messages) { return [ - `| ${messages.productionUpdated} | ${messages.alreadyPublished} | ${messages.developmentSynchronized} |`, "|---|---|---|", + `| ${messages.productionUpdated} | ${messages.publication} | ${messages.developmentSynchronized} |`, '|---|---|---|', `| ${operation.productionSha ? messages.yes : messages.no} | ${operation.publicationVerified ? messages.yes : messages.no} | ${reconciliationCompleted(operation) ? messages.yes : messages.no} |`, ]; } @@ -44742,27 +45011,26 @@ function protectedFact(operation, messages) { return `${messages.productionUpdated}: ${messages.yes}. ${messages.notPublished}.`; return `${messages.productionUpdated}: ${messages.no}. ${messages.notPublished}.`; } -function nextDescription(operation, messages) { - if (operation.phase === "blocked") { - const diagnostic = safeText(operation.lastFailure?.message ?? messages.workflowFailure); - return `${diagnostic}. ${messages.actionRequired}: ${operation.lastFailure?.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`; - } - if (operation.phase === "promotion_pr_pending") - return messages.afterPromotion; - if (operation.phase === "publishing" || operation.phase === "promoted") +function nextDescription(operation, messages, catalog) { + if (operation.phase === 'promotion_pr_pending' || operation.phase === 'publishing' || operation.phase === 'promoted') { return messages.afterPromotion; - if (operation.phase === "reconciliation_pending" || operation.phase === "published") + } + if (operation.phase === 'reconciliation_pending' || operation.phase === 'published') return messages.noRepublish; - if (operation.phase === "completed") + if (operation.phase === 'completed') { return `${messages.productionUpdated}: ${messages.yes}. ${messages.developmentSynchronized}: ${messages.yes}.`; - return `${messages.promotion}: ${inline(operation.sourceBranch)} -> ${inline(operation.productionBranch)}.`; + } + return catalog.message('deployment.template.nextPromotion', { + source: inline(operation.sourceBranch), + production: inline(operation.productionBranch), + }); } function deploymentAction(operation, messages) { - const manual = (operation.selectedPrMode ?? operation.prMode) === "create-only" - && (operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending"); + const manual = (operation.selectedPrMode ?? operation.prMode) === 'create-only' + && (operation.phase === 'promotion_pr_pending' || operation.phase === 'reconciliation_pending'); if (manual) return { required: true, message: `${messages.protectedChecks}: ${messages.reviewManagedPr}.` }; - if (operation.phase !== "blocked") + if (operation.phase !== 'blocked') return { required: false, message: messages.noAction }; return { required: true, @@ -44771,52 +45039,106 @@ function deploymentAction(operation, messages) { : `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${messages.manualIntervention}.`, }; } -function deploymentLinks(operation, context, messages) { +function deploymentLinks(operation, context, messages, catalog) { const links = [`[${messages.controlCenter}](${issueUrl(context)})`]; - links.push(`[${safeText(operation.sourceBranch)} branch](${branchUrl(context, operation.sourceBranch)})`); - links.push(`[${shortSha(operation.originSha)} origin commit](${commitUrl(context, operation.originSha)})`); - links.push(`[${shortSha(operation.sourceSha)} prepared commit](${commitUrl(context, operation.sourceSha)})`); - const activeTarget = operation.reconciliationTargets.find((target) => target.status !== "completed"); + links.push(markdownLink(catalog.message('deployment.template.sourceBranchLink', { + branch: safeText(operation.sourceBranch), + }), branchUrl(context, operation.sourceBranch))); + links.push(markdownLink(catalog.message('deployment.template.originCommitLink', { + commit: shortSha(operation.originSha), + }), commitUrl(context, operation.originSha))); + links.push(markdownLink(catalog.message('deployment.template.preparedCommitLink', { + commit: shortSha(operation.sourceSha), + }), commitUrl(context, operation.sourceSha))); + const activeTarget = operation.reconciliationTargets.find(target => target.status !== 'completed'); links.push(`[${messages.compare}](${compareUrl(context, activeTarget?.targetBranch ?? operation.productionBranch, activeTarget?.syncBranch ?? activeTarget?.sourceBranch ?? operation.sourceBranch)})`); - if (operation.promotionPullRequest) - links.push(`[Promotion PR #${operation.promotionPullRequest}](${pullRequestUrl(context, operation.promotionPullRequest)})`); + if (operation.promotionPullRequest) { + links.push(markdownLink(catalog.message('deployment.template.promotionPullRequestLink', { + number: operation.promotionPullRequest, + }), pullRequestUrl(context, operation.promotionPullRequest))); + } for (const target of operation.reconciliationTargets) { - if (target.pullRequest) - links.push(`[Reconciliation PR #${target.pullRequest}](${pullRequestUrl(context, target.pullRequest)})`); + if (target.pullRequest) { + links.push(markdownLink(catalog.message('deployment.template.reconciliationPullRequestLink', { + number: target.pullRequest, + }), pullRequestUrl(context, target.pullRequest))); + } + } + if (operation.productionSha) { + links.push(markdownLink(catalog.message('deployment.template.productionCommitLink', { + commit: shortSha(operation.productionSha), + }), commitUrl(context, operation.productionSha))); } - if (operation.productionSha) - links.push(`[${shortSha(operation.productionSha)} production commit](${commitUrl(context, operation.productionSha)})`); if (operation.publicationVerified) { - links.push(`[${safeText(operation.tag)} GitHub Release](${repositoryUrl(context)}/releases/tag/${encodeURIComponent(operation.tag)})`); - links.push(`[v${safeText(operation.version.split(".")[0])} Action tag](${branchUrl(context, `v${operation.version.split(".")[0]}`)})`); - if (context.packageName) - links.push(`[${safeText(context.packageName)}@${safeText(operation.version)} on npm](${npmVersionUrl(context.packageName, operation.version)})`); + links.push(markdownLink(catalog.message('deployment.template.releaseLink', { + tag: safeText(operation.tag), + }), `${repositoryUrl(context)}/releases/tag/${encodeURIComponent(operation.tag)}`)); + const actionTag = `v${operation.version.split('.')[0]}`; + links.push(markdownLink(catalog.message('deployment.template.actionTagLink', { + tag: actionTag, + }), branchUrl(context, actionTag))); + if (context.packageName) { + links.push(markdownLink(catalog.message('deployment.template.npmLink', { + package: safeText(context.packageName), + version: safeText(operation.version), + }), npmVersionUrl(context.packageName, operation.version))); + } + } + if (context.workflowRunUrl) { + links.push(markdownLink(catalog.message('deployment.template.workflowRunLink'), safeUrl(context.workflowRunUrl))); } - if (context.workflowRunUrl) - links.push(`[Workflow run](${safeUrl(context.workflowRunUrl)})`); return links; } -function messagesFor(locale) { return normalizeLocale(locale) === "es-ES" ? ES : EN; } -function normalizeLocale(locale) { return locale.toLowerCase().startsWith("es") ? "es-ES" : "en-US"; } function reconciliationCompleted(operation) { - return operation.phase === "completed" + return operation.phase === 'completed' || (operation.reconciliationTargets.length > 0 - && operation.reconciliationTargets.every((target) => target.status === "completed")); + && operation.reconciliationTargets.every(target => target.status === 'completed')); +} +function phaseRank(phase) { + return ['preparing', 'promotion_pr_pending', 'promoted', 'publishing', 'published', 'reconciliation_pending', 'completed'].indexOf(phase); +} +function repositoryUrl(context) { + return `https://github.com/${encodeURIComponent(context.owner)}/${encodeURIComponent(context.repository)}`; +} +function issueUrl(context) { + return `${repositoryUrl(context)}/issues/${context.issue}`; +} +function pullRequestUrl(context, number) { + return `${repositoryUrl(context)}/pull/${number}`; +} +function branchUrl(context, branch) { + return `${repositoryUrl(context)}/tree/${encodeURIComponent(branch)}`; +} +function commitUrl(context, sha) { + return `${repositoryUrl(context)}/commit/${encodeURIComponent(sha)}`; +} +function npmVersionUrl(packageName, version) { + return `https://www.npmjs.com/package/${encodeURIComponent(packageName)}/v/${encodeURIComponent(version)}`; +} +function compareUrl(context, base, head) { + return `${repositoryUrl(context)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`; +} +function markdownLink(label, url) { + return `[${safeLinkLabel(label)}](${url})`; +} +function inline(value) { + return `\`${safeText(value)}\``; +} +function safeText(value) { + return value.replace(/[\r\n`<>|]/gu, '').replace(/@/gu, '@\u200b').replace(/::/gu, '﹕﹕').slice(0, 240); +} +function safeLinkLabel(value) { + return safeText(value).replace(/[[\]()]/gu, ''); +} +function safeMarkerValue(value) { + return value.replace(/[^A-Za-z0-9._-]/gu, '').slice(0, 128); +} +function safeUrl(value) { + return /^https:\/\/github\.com\//u.test(value) ? value : 'https://github.com'; +} +function shortSha(value) { + return safeText(value).slice(0, 7); } -function phaseRank(phase) { return ["preparing", "promotion_pr_pending", "promoted", "publishing", "published", "reconciliation_pending", "completed"].indexOf(phase); } -function repositoryUrl(context) { return `https://github.com/${encodeURIComponent(context.owner)}/${encodeURIComponent(context.repository)}`; } -function issueUrl(context) { return `${repositoryUrl(context)}/issues/${context.issue}`; } -function pullRequestUrl(context, number) { return `${repositoryUrl(context)}/pull/${number}`; } -function branchUrl(context, branch) { return `${repositoryUrl(context)}/tree/${encodeURIComponent(branch)}`; } -function commitUrl(context, sha) { return `${repositoryUrl(context)}/commit/${encodeURIComponent(sha)}`; } -function npmVersionUrl(packageName, version) { return `https://www.npmjs.com/package/${encodeURIComponent(packageName)}/v/${encodeURIComponent(version)}`; } -function compareUrl(context, base, head) { return `${repositoryUrl(context)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`; } -function inline(value) { return `\`${safeText(value)}\``; } -function safeText(value) { return value.replace(/[\r\n`<>]/g, "").replace(/@/g, "@\u200b").replace(/::/g, "﹕﹕").slice(0, 240); } -function safeMarkerValue(value) { return value.replace(/[^A-Za-z0-9._-]/g, "").slice(0, 128); } -function safeUrl(value) { return /^https:\/\/github\.com\//.test(value) ? value : "https://github.com"; } -function shortSha(value) { return safeText(value).slice(0, 7); } -function capitalize(value) { return value.charAt(0).toUpperCase() + value.slice(1); } /***/ }), @@ -47113,6 +47435,7 @@ exports.semanticCleanupError = semanticCleanupError; const application_error_1 = __nccwpck_require__(75999); const deployment_plan_policy_1 = __nccwpck_require__(8352); const deployment_presentation_policy_1 = __nccwpck_require__(83221); +const deployment_message_catalog_1 = __nccwpck_require__(79364); const deployment_operation_1 = __nccwpck_require__(92730); const merge_queue_readiness_1 = __nccwpck_require__(12515); const result_1 = __nccwpck_require__(73817); @@ -47121,6 +47444,7 @@ class DeploymentOrchestrationRuntime { constructor(dependencies, stateBoundary) { this.dependencies = dependencies; this.stateBoundary = stateBoundary; + this.presentationCatalogs = new Map(); } async persist(context, operation) { context.currentConfiguration.deploymentOrchestration = operation; @@ -47130,7 +47454,7 @@ class DeploymentOrchestrationRuntime { const blocked = (0, deployment_operation_1.blockDeploymentOperation)(operation, category, message, retryable); await this.persist(context, blocked); await this.publishDashboard(context, blocked); - await this.publishMilestone(context, blocked, "reconciliation-blocked", `❌ Deployment blocked: ${blocked.lastFailure?.message}`); + await this.publishMilestone(context, blocked, { kind: "reconciliation-blocked", reason: blocked.lastFailure?.message ?? message }); return new result_1.Result({ id: exports.DEPLOYMENT_ORCHESTRATION_TASK_ID, success: false, @@ -47141,7 +47465,8 @@ class DeploymentOrchestrationRuntime { } async publishDashboard(context, operation) { const marker = (0, deployment_presentation_policy_1.deploymentDashboardMarker)(operation.operationId, context.singleAction.issue); - const body = (0, deployment_presentation_policy_1.renderDeploymentDashboard)(operation, presentationContext(context)); + const catalog = await this.presentationCatalog("issue", context, operation); + const body = (0, deployment_presentation_policy_1.renderDeploymentDashboard)(operation, presentationContext(context, operation), catalog); const current = await this.dependencies.presentation.findDashboard(context.singleAction.issue, marker); if (current) { await this.dependencies.presentation.updateDashboard(context.singleAction.issue, current.id, body); @@ -47149,11 +47474,12 @@ class DeploymentOrchestrationRuntime { } await this.dependencies.presentation.createDashboard(context.singleAction.issue, body); } - async publishMilestone(context, operation, name, body) { + async publishMilestone(context, operation, milestone) { if (operation.commentMode !== "milestones") return; - const marker = ``; - await this.dependencies.presentation.publishMilestone(context.singleAction.issue, marker, body); + const marker = ``; + const catalog = await this.presentationCatalog("issue", context, operation); + await this.dependencies.presentation.publishMilestone(context.singleAction.issue, marker, (0, deployment_presentation_policy_1.renderDeploymentMilestone)(milestone, catalog)); } async createOrReusePullRequest(context, operation, phase, target) { const headBranch = target?.syncBranch ?? target?.sourceBranch ?? operation.sourceBranch; @@ -47171,10 +47497,11 @@ class DeploymentOrchestrationRuntime { } if (existing[0]) return existing[0]; - const presentation = presentationContext(context); + const presentation = presentationContext(context, operation); + const catalog = await this.presentationCatalog("pull-request", context, operation); const content = phase === "promotion" - ? (0, deployment_presentation_policy_1.renderPromotionPullRequest)(operation, presentation) - : (0, deployment_presentation_policy_1.renderReconciliationPullRequest)(operation, requireTarget(target), presentation); + ? (0, deployment_presentation_policy_1.renderPromotionPullRequest)(operation, presentation, catalog) + : (0, deployment_presentation_policy_1.renderReconciliationPullRequest)(operation, requireTarget(target), presentation, catalog); return await this.dependencies.pullRequests.createManagedPullRequest({ ...query, ...content }); } async configureMergeBehavior(context, operation, pullRequest, category, targetRole) { @@ -47198,7 +47525,7 @@ class DeploymentOrchestrationRuntime { const capabilities = await this.dependencies.targetRules.getTargetCapabilities(targetBranch, { candidateHeadSha, ...(pullRequest === undefined ? {} : { pullRequest }) }); const decision = (0, deployment_plan_policy_1.selectPullRequestMode)(operation.prMode, capabilities); if (decision.kind === "unsupported") { - return this.unsupportedMergeBehavior(context, targetRole, targetBranch, capabilities, decision); + return this.unsupportedMergeBehavior(context, operation, targetRole, targetBranch, capabilities, decision); } if (decision.mode === "merge-queue") { const readiness = (0, merge_queue_readiness_1.evaluateMergeQueueReadiness)({ @@ -47212,7 +47539,7 @@ class DeploymentOrchestrationRuntime { if (readiness.verdict !== "ready") { return { kind: "blocked", - reason: (0, deployment_plan_policy_1.mergeQueueReadinessFailureMessage)(readiness, context.locale.issue), + reason: (0, deployment_plan_policy_1.mergeQueueReadinessFailureMessage)(readiness, effectiveLocale(context, operation).issue), }; } } @@ -47264,7 +47591,7 @@ class DeploymentOrchestrationRuntime { await this.dependencies.pullRequests.enableAutoMerge(pullRequest.nodeId); } } - unsupportedMergeBehavior(context, targetRole, targetBranch, capabilities, decision) { + unsupportedMergeBehavior(context, operation, targetRole, targetBranch, capabilities, decision) { if (capabilities.mergeQueueObservationProblems.length === 0) { return { kind: "blocked", reason: decision.reason }; } @@ -47278,9 +47605,20 @@ class DeploymentOrchestrationRuntime { }); return { kind: "blocked", - reason: (0, deployment_plan_policy_1.mergeQueueReadinessFailureMessage)(readiness, context.locale.issue), + reason: (0, deployment_plan_policy_1.mergeQueueReadinessFailureMessage)(readiness, effectiveLocale(context, operation).issue), }; } + presentationCatalog(scope, context, operation) { + const locale = effectiveLocale(context, operation); + const targetLocale = scope === "issue" ? locale.issue : locale.pullRequest; + const key = `${scope}:${targetLocale}`; + const existing = this.presentationCatalogs.get(key); + if (existing) + return existing; + const resolution = (0, deployment_message_catalog_1.resolveDeploymentCatalog)(targetLocale, context.agentConfiguration, this.dependencies.catalogResolver); + this.presentationCatalogs.set(key, resolution); + return resolution; + } async cleanupSyncBranches(context, operation) { for (const target of operation.reconciliationTargets) { if (!target.syncBranch) @@ -47349,16 +47687,21 @@ function shouldRecordUnexpectedFailure(error) { function semanticCleanupError(error) { return (0, application_error_1.toApplicationError)(error, "workflow.failed", "Deployment cleanup failed."); } -function presentationContext(context) { +function presentationContext(context, operation) { + const locale = effectiveLocale(context, operation); return { owner: context.owner, repository: context.repo, issue: context.singleAction.issue, - issueLocale: context.locale.issue, - pullRequestLocale: context.locale.pullRequest, + repositoryLocale: locale.repository, + issueLocale: locale.issue, + pullRequestLocale: locale.pullRequest, packageName: context.owner === "vypdev" && context.repo === "copilot" ? "@vypdev/copilot" : undefined, }; } +function effectiveLocale(context, operation) { + return operation.locale ?? context.locale; +} function requireTarget(target) { if (!target) throw new application_error_1.ApplicationError("workflow.stale", "Reconciliation target is missing."); @@ -48016,7 +48359,7 @@ class AcceptPromotionHandler { const publishing = { ...promoted, phase: "publishing" }; await this.runtime.persist(context, publishing); await this.runtime.publishDashboard(context, publishing); - await this.runtime.publishMilestone(context, publishing, "promotion-merged", `✅ Promotion PR #${pullRequest.number} merged. Publication is starting from production SHA \`${productionSha}\`.`); + await this.runtime.publishMilestone(context, publishing, { kind: "promotion-merged", pullRequest: pullRequest.number, productionSha }); await this.runtime.dependencies.continuation.dispatch(operation.publicationWorkflow, operation.productionBranch, operation.operationId, context.singleAction.issue, operation.version); return (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Promotion PR #${pullRequest.number} was verified; publication continuation was dispatched from ${operation.productionBranch}.`); } @@ -48089,7 +48432,7 @@ class ConfirmPublicationHandler { lastFailure: null, }; await this.runtime.persist(context, published); - await this.runtime.publishMilestone(context, published, "publication-complete", `📦 ${published.tag} is published from accepted production SHA \`${published.productionSha}\`.`); + await this.runtime.publishMilestone(context, published, { kind: "publication-complete", tag: published.tag, productionSha }); return await this.beginReconciliation(context, published); } async resumeRetryableBlock(context, operation) { @@ -48290,6 +48633,7 @@ class PreparePromotionHandler { const originSha = persistedOrigin ?? await this.runtime.dependencies.git.getMergeBaseSha(originBranch, sourceBranch); const operation = (0, deployment_plan_policy_1.buildInitialDeploymentOperation)({ operationId: this.runtime.dependencies.operationId(), + locale: context.locale, kind, version: context.singleAction.version, title: context.singleAction.title, @@ -48347,6 +48691,7 @@ function selectOriginBranch(context, kind) { function validateOperation(context, operation) { const errors = (0, deployment_plan_policy_1.validateInitialDeploymentInput)({ operationId: operation.operationId, + locale: operation.locale ?? context.locale, kind: operation.kind, version: operation.version, title: operation.title, @@ -48487,7 +48832,7 @@ class ReconciliationHandler { const completed = { ...operation, phase: "completed", lastFailure: null }; await this.runtime.persist(context, completed); await this.runtime.publishDashboard(context, completed); - await this.runtime.publishMilestone(context, completed, "orchestration-complete", `✅ Deployment ${completed.tag} and every configured reconciliation target are complete.`); + await this.runtime.publishMilestone(context, completed, { kind: "orchestration-complete", tag: completed.tag }); return (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Deployment ${completed.tag} completed${completionContext}.`); } async finishOrPresent(context, operation) { @@ -52165,6 +52510,11 @@ function projectDeploymentOrchestrationContext(source) { branches: Object.freeze({ ...source.branches }), workflows: Object.freeze({ ...source.workflows }), locale: Object.freeze({ ...source.locale }), + ...(source.agentConfiguration + ? { agentConfiguration: Object.freeze({ ...source.agentConfiguration }) } + : source.ai + ? { agentConfiguration: Object.freeze({ ...source.ai.getAgentConfiguration('planner') }) } + : {}), labels: Object.freeze({ ...source.labels, lifecycle: Object.freeze({ ...source.labels.lifecycle }), @@ -52354,6 +52704,7 @@ function copyInitialLabels(source) { function copyDeploymentOperation(operation) { return Object.freeze({ ...operation, + ...(operation.locale ? { locale: Object.freeze({ ...operation.locale }) } : {}), reconciliationTargets: Object.freeze((operation.reconciliationTargets ?? []).map(target => Object.freeze({ ...target }))), ...(operation.publicationReceipt ? { publicationReceipt: Object.freeze({ ...operation.publicationReceipt }) } : {}), ...(operation.lastFailure ? { lastFailure: Object.freeze({ ...operation.lastFailure }) } : {}), @@ -70608,6 +70959,7 @@ exports.completeReconciliationTarget = completeReconciliationTarget; exports.sanitizeDeploymentMessage = sanitizeDeploymentMessage; exports.isDeploymentOperationSnapshot = isDeploymentOperationSnapshot; const deployment_configuration_1 = __nccwpck_require__(22495); +const locale_1 = __nccwpck_require__(15386); exports.DEPLOYMENT_PHASES = [ "preparing", "promotion_pr_pending", @@ -70690,6 +71042,7 @@ function isDeploymentOperationSnapshot(value) { && operation.revision > 0 && typeof operation.operationId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operation.operationId) + && (operation.locale === undefined || (0, locale_1.isLocaleProfile)(operation.locale)) && (operation.kind === "release" || operation.kind === "hotfix") && typeof operation.version === "string" && /^[0-9]+\.[0-9]+\.[0-9]+$/.test(operation.version) && typeof operation.title === "string" && operation.title.length <= 1000 @@ -71041,6 +71394,7 @@ exports.canonicalizeLocaleTag = canonicalizeLocaleTag; exports.normalizeLocaleTag = normalizeLocaleTag; exports.resolveLocaleProfile = resolveLocaleProfile; exports.localeForScope = localeForScope; +exports.isLocaleProfile = isLocaleProfile; exports.localeLanguagesMatch = localeLanguagesMatch; exports.baseLanguage = baseLanguage; exports.DEFAULT_REPOSITORY_LOCALE = 'en-US'; @@ -71110,6 +71464,22 @@ function localeForScope(profile, scope) { return profile.pullRequest; return profile.repository; } +function isLocaleProfile(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) + return false; + const candidate = value; + try { + const resolved = resolveLocaleProfile(candidate.repository, candidate.issueOverride ?? '', candidate.pullRequestOverride ?? ''); + return candidate.repository === resolved.repository + && candidate.issue === resolved.issue + && candidate.pullRequest === resolved.pullRequest + && candidate.issueOverride === resolved.issueOverride + && candidate.pullRequestOverride === resolved.pullRequestOverride; + } + catch { + return false; + } +} function localeLanguagesMatch(left, right) { return baseLanguage(canonicalizeLocaleTag(left)) === baseLanguage(canonicalizeLocaleTag(right)); } @@ -71441,7 +71811,7 @@ function validMessageText(value) { function safeDynamicText(value) { return validMessageText(value) && !/[\r\n\u202A-\u202E\u2066-\u2069]/u.test(value) - && !/|<\/?[A-Za-z]|https?:\/\/|```|[`*_[\]~]|(^|\s)\/(?:copilot)(?:\s|$)|@[A-Za-z0-9]/iu.test(value); + && !/|<\/?[A-Za-z]|https?:\/\/|```|[`*_[\]~|]|(^|\s)\/(?:copilot)(?:\s|$)|@[A-Za-z0-9]/iu.test(value); } function pluralPlaceholderParity(message) { const expected = placeholdersForTemplate(message.other); @@ -73112,13 +73482,13 @@ function createSingleActionUseCaseCompositionRoot(surface, binding) { ? new repository_release_publication_repository_1.RepositoryReleasePublicationRepository((0, github_release_client_factory_1.createReleaseClient)()) : undefined; const deploymentOrchestration = surface === "github-workflow" - ? createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, repositoryReleasePort, binding) + ? createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, repositoryReleasePort, binding, catalogResolver) : undefined; return new single_action_use_case_1.SingleActionUseCase(repositoryTagPort && repositoryReleasePort ? new publish_github_action_use_case_1.PublishGithubActionUseCase((0, push_single_action_capability_port_binding_1.bindRepositoryTag)(repositoryTagPort, binding), (0, push_single_action_capability_port_binding_1.bindRepositoryRelease)(repositoryReleasePort, binding)) : undefined, repositoryReleasePort ? new create_release_use_case_1.CreateReleaseUseCase((0, push_single_action_capability_port_binding_1.bindRepositoryRelease)(repositoryReleasePort, binding)) : undefined, repositoryTagPort ? new create_tag_use_case_1.CreateTagUseCase((0, push_single_action_capability_port_binding_1.bindRepositoryTag)(repositoryTagPort, binding)) : undefined, new think_use_case_1.ThinkUseCase((0, shared_capability_port_binding_1.bindIssueDescriptionQuery)(issueDescriptionQueryPort, binding), (0, shared_capability_port_binding_1.bindIssueNotification)((0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, initial_setup_composition_root_1.createInitialSetupCompositionRoot)(binding), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(binding), createDetectPotentialProblemsUseCase(binding), new recommend_steps_use_case_1.RecommendStepsUseCase((0, shared_capability_port_binding_1.bindIssueDescriptionQuery)(issueDescriptionQueryPort, binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, issue_inactivity_composition_root_1.createCloseInactiveIssuesUseCase)(binding), (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), new publish_issue_comment_use_case_1.PublishIssueCommentUseCase((0, push_single_action_capability_port_binding_1.bindIssueCommentPublication)(issueDescriptionQueryPort, binding)), new observe_branch_sync_use_case_1.ObserveBranchSyncUseCase((0, push_single_action_capability_port_binding_1.bindBranchDependencies)(new branch_dependency_repository_1.BranchDependencyRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), binding), (0, push_single_action_capability_port_binding_1.bindBranchComparison)(new branch_compare_repository_1.BranchCompareRepository((0, github_branch_client_factory_1.createBranchComparisonClient)()), binding), (0, push_single_action_capability_port_binding_1.bindBranchSyncNotification)(issueDescriptionQueryPort, binding), catalogResolver), deploymentOrchestration); } -function createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, publication, binding) { +function createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, publication, binding, catalogResolver) { const deploymentClient = new octokit_deployment_adapter_1.OctokitDeploymentClientAdapter(); return new deployment_orchestration_use_case_1.DeploymentOrchestrationUseCase({ pullRequests: (0, push_single_action_capability_port_binding_1.bindManagedPullRequests)(new github_managed_pull_request_repository_1.GithubManagedPullRequestRepository(deploymentClient), binding), @@ -73131,6 +73501,7 @@ function createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, publica labels: (0, push_single_action_capability_port_binding_1.bindDeploymentLabels)((0, issue_labels_composition_root_1.createIssueLabelRepository)(), binding), issues: (0, push_single_action_capability_port_binding_1.bindDeploymentIssues)((0, issue_interaction_composition_root_1.createIssueClosureRepository)(), binding), operationId: node_crypto_1.randomUUID, + catalogResolver, }); } function createIssueCommentUseCaseCompositionRoot(binding) { diff --git a/docs/issues/deployment-orchestration.mdx b/docs/issues/deployment-orchestration.mdx index 2bdbe9a4a..32bd4de23 100644 --- a/docs/issues/deployment-orchestration.mdx +++ b/docs/issues/deployment-orchestration.mdx @@ -99,6 +99,40 @@ already be present when a managed promotion PR closes. The immutable facts are visible in one issue control-center comment. Duplicate events and retries update that comment instead of creating another operation. +## Language and presentation + +Deployment UI follows the repository locale profile. English (`en-US`) is the +default when no locale is configured. + +- The issue control center and the four optional milestone notifications use the + effective issue locale. +- Managed promotion and reconciliation PR titles and descriptions use the + effective pull-request locale. The stable `release(...)` or `hotfix(...)` + prefix, refs, SHAs, tags, IDs, and marker fields are not translated. +- The GitHub Actions Job Summary uses the repository locale because it belongs to + the run, not to the issue or PR conversation. + +Empty `issues-locale` and `pull-requests-locale` values inherit +`repository-locale`. Complete reviewed catalogs ship for English and Spanish. +Any other valid BCP-47 locale uses the configured planning agent to resolve one +bounded catalog for the destination. If that response is unavailable, incomplete, +or unsafe, the whole artifact falls back to English; Copilot never mixes +languages or creates a separate fallback comment. + +The dashboard leads with current status and a required action only when a person +must act. Progress, transition, and links follow, while operation IDs and other +technical facts stay collapsed. Managed PR descriptions explain their single +purpose and consequence without repeating the dashboard. The Job Summary shows +the durable revision, branch/publication facts, retryability, and descriptive +links; it does not replay internal workflow step logs such as “created” or +“reused”. + +New operations snapshot the complete canonical locale profile when they start, +so a later Variable change cannot switch an in-flight release between languages. +A version-1 operation created before locale snapshots were added remains valid +and uses the current profile until it completes. Start the next deployment from +a fresh launcher issue to guarantee a newly configured profile. + ## Serialization, durable revisions, and safe retries All release, hotfix, and managed-PR mutation routes for the same launcher issue diff --git a/docs/issues/type/hotfix.mdx b/docs/issues/type/hotfix.mdx index cd2f5aaa5..9c1705698 100644 --- a/docs/issues/type/hotfix.mdx +++ b/docs/issues/type/hotfix.mdx @@ -146,7 +146,9 @@ jobs: The durable hotfix dashboard owns the issue presentation. Routine orchestration does not create an “Actions” summary or a decorative image. The legacy `images-issue-hotfix` input remains accepted only for configuration -compatibility. +compatibility. The dashboard uses the effective issue locale, managed PRs use +the pull-request locale, and the Actions summary uses the English-default +repository locale; see the orchestration guide for inheritance and fallback. ## Template You can use this template for hotfix issues that helps capture all the necessary information. The template includes fields for: diff --git a/docs/issues/type/release.mdx b/docs/issues/type/release.mdx index 84eacf0ff..3a26c937e 100644 --- a/docs/issues/type/release.mdx +++ b/docs/issues/type/release.mdx @@ -142,7 +142,9 @@ jobs: The durable release dashboard owns the issue presentation. Routine orchestration does not create an “Actions” summary or a decorative image. The legacy `images-issue-release` input remains accepted only for configuration -compatibility. +compatibility. The dashboard uses the effective issue locale, managed PRs use +the pull-request locale, and the Actions summary uses the English-default +repository locale; see the orchestration guide for inheritance and fallback. ## Template You can use this template for release issues that helps capture all the necessary information. The template includes fields for: diff --git a/scripts/coverage-budgets.json b/scripts/coverage-budgets.json index 4f6c2c043..36d7dc6cb 100644 --- a/scripts/coverage-budgets.json +++ b/scripts/coverage-budgets.json @@ -14,6 +14,28 @@ ], "successMessage": "deployment handler coverage: PASS (95% lines/statements, 90% branches/functions)" }, + { + "name": "Deployment presentation", + "missingEntryLabel": "deployment presentation", + "rules": [ + { + "files": [ + "src/application/policies/deployment_message_catalog.ts" + ], + "mode": "each", + "thresholdProfile": "exhaustive" + }, + { + "files": [ + "src/application/policies/deployment_presentation_policy.ts", + "src/application/services/deployment_orchestration_runtime.ts" + ], + "mode": "aggregate", + "thresholdProfile": "default" + } + ], + "successMessage": "deployment presentation coverage: PASS (catalog 100%; rendered path 95% lines/statements, 90% branches/functions)" + }, { "name": "Bugbot context", "missingEntryLabel": "Bugbot context", diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 8cdd3e9d0..f5e8bea8c 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -10,8 +10,8 @@ debt or convert unknown historic intent into a design decision. | Capability ID | Status | Scope | Primary SDD | Evidence | |---|---|---|---|---| -| `github-communication-experience` | Proposed | English-default, localized, semantic, bounded, and idempotent product messages across GitHub and repository-aware operator surfaces | [Semantic GitHub communication and repository localization](./semantic-github-publication-and-notification.md) + 1 companion | 101 paths · 2026-09-14 | -| `release-orchestration` | Implemented | Release and hotfix promotion, publication, reconciliation, and durable recovery | [Configurable production-first release orchestration](./configurable-release-orchestration.md) + 2 companion | 41 paths · 2026-09-12 | +| `github-communication-experience` | Proposed | English-default, localized, semantic, bounded, and idempotent product messages across GitHub and repository-aware operator surfaces | [Semantic GitHub communication and repository localization](./semantic-github-publication-and-notification.md) + 1 companion | 103 paths · 2026-09-14 | +| `release-orchestration` | Implemented | Release and hotfix promotion, publication, reconciliation, and durable recovery | [Configurable production-first release orchestration](./configurable-release-orchestration.md) + 2 companion | 52 paths · 2026-09-14 | | `merge-queue-readiness` | Implemented | Fail-closed validation of required checks and merge-group workflow support | [Merge queue readiness and effective target rules](./merge-queue-readiness.md) | 15 paths · 2026-09-11 | | `bugbot-review-state-reconciliation` | Implemented | Reconcile review snapshots, findings, threads, comments, and check conclusions | [Bugbot review-state reconciliation](./bugbot-review-state-reconciliation.md) | 56 paths · 2026-09-14 | | `execution-lifecycle` | Implemented | Shared GitHub Action lifecycle from event admission through durable user-facing results | [Execution admission, queueing, routing, and result publication](./execution-admission-queue-and-publication.md) + 3 companion | 82 paths · 2026-09-14 | @@ -34,19 +34,19 @@ debt or convert unknown historic intent into a design decision. - Specifications: [`specs/semantic-github-publication-and-notification.md`](./semantic-github-publication-and-notification.md) · [`specs/repository-locale-and-localization.md`](./repository-locale-and-localization.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/copilot_issue_comment.yml`](../.github/workflows/copilot_issue_comment.yml) · [`.github/workflows/copilot_pull_request.yml`](../.github/workflows/copilot_pull_request.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) · [`.github/workflows/copilot_deployment_orchestration.yml`](../.github/workflows/copilot_deployment_orchestration.yml) - Entrypoints: [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`src/actions/github_action_completion.ts`](../src/actions/github_action_completion.ts) · [`src/api.ts`](../src/api.ts) · [`src/cli.ts`](../src/cli.ts) -- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`src/domain/locale.ts`](../src/domain/locale.ts) · [`src/domain/message_catalog.ts`](../src/domain/message_catalog.ts) · [`src/data/model/locale.ts`](../src/data/model/locale.ts) · [`src/actions/github_action_locale_inputs.ts`](../src/actions/github_action_locale_inputs.ts) · [`src/application/ports/message_catalog_ports.ts`](../src/application/ports/message_catalog_ports.ts) · [`src/application/policies/resolved_message_catalog_policy.ts`](../src/application/policies/resolved_message_catalog_policy.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/usecases/localization/resolve_message_catalog_use_case.ts`](../src/application/usecases/localization/resolve_message_catalog_use_case.ts) · [`src/application/usecases/steps/common/comment_language_translation_workflow.ts`](../src/application/usecases/steps/common/comment_language_translation_workflow.ts) · [`src/application/policies/comment_translation_policy.ts`](../src/application/policies/comment_translation_policy.ts) · [`src/application/usecases/comment_automation_use_case.ts`](../src/application/usecases/comment_automation_use_case.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/domain/github_publication.ts`](../src/domain/github_publication.ts) · [`src/application/policies/publication_identity_policy.ts`](../src/application/policies/publication_identity_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/application/usecases/steps/common/status_card_publication_workflow.ts`](../src/application/usecases/steps/common/status_card_publication_workflow.ts) · [`src/application/usecases/steps/common/reply_publication_workflow.ts`](../src/application/usecases/steps/common/reply_publication_workflow.ts) · [`src/architecture/github_publication_mutation_baseline.json`](../src/architecture/github_publication_mutation_baseline.json) · [`src/application/policies/action_summary_policy.ts`](../src/application/policies/action_summary_policy.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/policies/bugbot_message_catalog.ts`](../src/application/policies/bugbot_message_catalog.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/policies/bugbot_review_presentation_policy.ts`](../src/application/policies/bugbot_review_presentation_policy.ts) · [`src/application/usecases/steps/commit/detect_potential_problems_workflow.ts`](../src/application/usecases/steps/commit/detect_potential_problems_workflow.ts) · [`src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts`](../src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts) · [`src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts`](../src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts) · [`src/application/policies/deployment_presentation_policy.ts`](../src/application/policies/deployment_presentation_policy.ts) · [`src/application/usecases/actions/recommend_steps_workflow.ts`](../src/application/usecases/actions/recommend_steps_workflow.ts) · [`src/application/usecases/actions/check_progress_workflow.ts`](../src/application/usecases/actions/check_progress_workflow.ts) · [`src/data/repository/issue/issue_content_repository.ts`](../src/data/repository/issue/issue_content_repository.ts) -- Tests: [`src/domain/__tests__/locale.test.ts`](../src/domain/__tests__/locale.test.ts) · [`src/domain/__tests__/message_catalog.test.ts`](../src/domain/__tests__/message_catalog.test.ts) · [`src/actions/__tests__/configuration_builders.test.ts`](../src/actions/__tests__/configuration_builders.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/application/policies/__tests__/comment_translation_policy.test.ts`](../src/application/policies/__tests__/comment_translation_policy.test.ts) · [`src/application/usecases/localization/__tests__/resolve_message_catalog_use_case.test.ts`](../src/application/usecases/localization/__tests__/resolve_message_catalog_use_case.test.ts) · [`src/prompts/__tests__/localize_message_catalog.test.ts`](../src/prompts/__tests__/localize_message_catalog.test.ts) · [`src/domain/__tests__/github_publication.test.ts`](../src/domain/__tests__/github_publication.test.ts) · [`src/application/policies/__tests__/publication_identity_policy.test.ts`](../src/application/policies/__tests__/publication_identity_policy.test.ts) · [`src/application/policies/__tests__/publication_message_catalog.test.ts`](../src/application/policies/__tests__/publication_message_catalog.test.ts) · [`src/application/policies/__tests__/semantic_result_publication_policy.test.ts`](../src/application/policies/__tests__/semantic_result_publication_policy.test.ts) · [`src/application/policies/__tests__/action_summary_policy.test.ts`](../src/application/policies/__tests__/action_summary_policy.test.ts) · [`src/application/policies/__tests__/branch_sync_notification_policy.test.ts`](../src/application/policies/__tests__/branch_sync_notification_policy.test.ts) · [`src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts) · [`src/application/policies/__tests__/bugbot_message_catalog.test.ts`](../src/application/policies/__tests__/bugbot_message_catalog.test.ts) · [`src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts`](../src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts) · [`src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts`](../src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/dismiss_bugbot_findings_use_case.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/dismiss_bugbot_findings_use_case.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/synchronize_bugbot_review_presentation_use_case.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/synchronize_bugbot_review_presentation_use_case.test.ts) · [`src/application/policies/__tests__/deployment_presentation_policy.test.ts`](../src/application/policies/__tests__/deployment_presentation_policy.test.ts) · [`src/application/usecases/steps/common/__tests__/comment_language_translation_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/comment_language_translation_workflow.test.ts) · [`src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts) · [`src/application/usecases/steps/common/__tests__/status_card_publication_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/status_card_publication_workflow.test.ts) · [`src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts) · [`src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts`](../src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts) · [`src/architecture/__tests__/github_publication_boundaries.test.ts`](../src/architecture/__tests__/github_publication_boundaries.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts`](../src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts) · [`src/application/usecases/actions/__tests__/check_progress_use_case.test.ts`](../src/application/usecases/actions/__tests__/check_progress_use_case.test.ts) · [`src/application/usecases/__tests__/comment_automation_use_case.test.ts`](../src/application/usecases/__tests__/comment_automation_use_case.test.ts) +- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`src/domain/locale.ts`](../src/domain/locale.ts) · [`src/domain/message_catalog.ts`](../src/domain/message_catalog.ts) · [`src/data/model/locale.ts`](../src/data/model/locale.ts) · [`src/actions/github_action_locale_inputs.ts`](../src/actions/github_action_locale_inputs.ts) · [`src/application/ports/message_catalog_ports.ts`](../src/application/ports/message_catalog_ports.ts) · [`src/application/policies/resolved_message_catalog_policy.ts`](../src/application/policies/resolved_message_catalog_policy.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/usecases/localization/resolve_message_catalog_use_case.ts`](../src/application/usecases/localization/resolve_message_catalog_use_case.ts) · [`src/application/usecases/steps/common/comment_language_translation_workflow.ts`](../src/application/usecases/steps/common/comment_language_translation_workflow.ts) · [`src/application/policies/comment_translation_policy.ts`](../src/application/policies/comment_translation_policy.ts) · [`src/application/usecases/comment_automation_use_case.ts`](../src/application/usecases/comment_automation_use_case.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/domain/github_publication.ts`](../src/domain/github_publication.ts) · [`src/application/policies/publication_identity_policy.ts`](../src/application/policies/publication_identity_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/application/usecases/steps/common/status_card_publication_workflow.ts`](../src/application/usecases/steps/common/status_card_publication_workflow.ts) · [`src/application/usecases/steps/common/reply_publication_workflow.ts`](../src/application/usecases/steps/common/reply_publication_workflow.ts) · [`src/architecture/github_publication_mutation_baseline.json`](../src/architecture/github_publication_mutation_baseline.json) · [`src/application/policies/action_summary_policy.ts`](../src/application/policies/action_summary_policy.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/policies/bugbot_message_catalog.ts`](../src/application/policies/bugbot_message_catalog.ts) · [`src/application/policies/deployment_message_catalog.ts`](../src/application/policies/deployment_message_catalog.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/policies/bugbot_review_presentation_policy.ts`](../src/application/policies/bugbot_review_presentation_policy.ts) · [`src/application/usecases/steps/commit/detect_potential_problems_workflow.ts`](../src/application/usecases/steps/commit/detect_potential_problems_workflow.ts) · [`src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts`](../src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts) · [`src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts`](../src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts) · [`src/application/policies/deployment_presentation_policy.ts`](../src/application/policies/deployment_presentation_policy.ts) · [`src/application/usecases/actions/recommend_steps_workflow.ts`](../src/application/usecases/actions/recommend_steps_workflow.ts) · [`src/application/usecases/actions/check_progress_workflow.ts`](../src/application/usecases/actions/check_progress_workflow.ts) · [`src/data/repository/issue/issue_content_repository.ts`](../src/data/repository/issue/issue_content_repository.ts) +- Tests: [`src/domain/__tests__/locale.test.ts`](../src/domain/__tests__/locale.test.ts) · [`src/domain/__tests__/message_catalog.test.ts`](../src/domain/__tests__/message_catalog.test.ts) · [`src/actions/__tests__/configuration_builders.test.ts`](../src/actions/__tests__/configuration_builders.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/application/policies/__tests__/comment_translation_policy.test.ts`](../src/application/policies/__tests__/comment_translation_policy.test.ts) · [`src/application/usecases/localization/__tests__/resolve_message_catalog_use_case.test.ts`](../src/application/usecases/localization/__tests__/resolve_message_catalog_use_case.test.ts) · [`src/prompts/__tests__/localize_message_catalog.test.ts`](../src/prompts/__tests__/localize_message_catalog.test.ts) · [`src/domain/__tests__/github_publication.test.ts`](../src/domain/__tests__/github_publication.test.ts) · [`src/application/policies/__tests__/publication_identity_policy.test.ts`](../src/application/policies/__tests__/publication_identity_policy.test.ts) · [`src/application/policies/__tests__/publication_message_catalog.test.ts`](../src/application/policies/__tests__/publication_message_catalog.test.ts) · [`src/application/policies/__tests__/semantic_result_publication_policy.test.ts`](../src/application/policies/__tests__/semantic_result_publication_policy.test.ts) · [`src/application/policies/__tests__/action_summary_policy.test.ts`](../src/application/policies/__tests__/action_summary_policy.test.ts) · [`src/application/policies/__tests__/branch_sync_notification_policy.test.ts`](../src/application/policies/__tests__/branch_sync_notification_policy.test.ts) · [`src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts) · [`src/application/policies/__tests__/bugbot_message_catalog.test.ts`](../src/application/policies/__tests__/bugbot_message_catalog.test.ts) · [`src/application/policies/__tests__/deployment_message_catalog.test.ts`](../src/application/policies/__tests__/deployment_message_catalog.test.ts) · [`src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts`](../src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts) · [`src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts`](../src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/dismiss_bugbot_findings_use_case.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/dismiss_bugbot_findings_use_case.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/synchronize_bugbot_review_presentation_use_case.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/synchronize_bugbot_review_presentation_use_case.test.ts) · [`src/application/policies/__tests__/deployment_presentation_policy.test.ts`](../src/application/policies/__tests__/deployment_presentation_policy.test.ts) · [`src/application/usecases/steps/common/__tests__/comment_language_translation_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/comment_language_translation_workflow.test.ts) · [`src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts) · [`src/application/usecases/steps/common/__tests__/status_card_publication_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/status_card_publication_workflow.test.ts) · [`src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts) · [`src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts`](../src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts) · [`src/architecture/__tests__/github_publication_boundaries.test.ts`](../src/architecture/__tests__/github_publication_boundaries.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts`](../src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts) · [`src/application/usecases/actions/__tests__/check_progress_use_case.test.ts`](../src/application/usecases/actions/__tests__/check_progress_use_case.test.ts) · [`src/application/usecases/__tests__/comment_automation_use_case.test.ts`](../src/application/usecases/__tests__/comment_automation_use_case.test.ts) - User documentation: [`docs/configuration.mdx`](../docs/configuration.mdx) · [`docs/configuration-checklist.mdx`](../docs/configuration-checklist.mdx) · [`docs/features.mdx`](../docs/features.mdx) · [`docs/issues/configuration.mdx`](../docs/issues/configuration.mdx) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) · [`docs/issues/notifications-and-auto-close.mdx`](../docs/issues/notifications-and-auto-close.mdx) · [`docs/issues/branch-synchronization.mdx`](../docs/issues/branch-synchronization.mdx) · [`docs/issues/type/feature.mdx`](../docs/issues/type/feature.mdx) · [`docs/issues/type/bugfix.mdx`](../docs/issues/type/bugfix.mdx) · [`docs/issues/type/docs.mdx`](../docs/issues/type/docs.mdx) · [`docs/issues/type/chore.mdx`](../docs/issues/type/chore.mdx) · [`docs/issues/type/hotfix.mdx`](../docs/issues/type/hotfix.mdx) · [`docs/issues/type/release.mdx`](../docs/issues/type/release.mdx) · [`docs/issues/deployment-orchestration.mdx`](../docs/issues/deployment-orchestration.mdx) · [`docs/pull-requests/configuration.mdx`](../docs/pull-requests/configuration.mdx) · [`docs/pull-requests/capabilities.mdx`](../docs/pull-requests/capabilities.mdx) · [`docs/pull-requests/workflow-setup.mdx`](../docs/pull-requests/workflow-setup.mdx) · [`docs/pull-requests/examples.mdx`](../docs/pull-requests/examples.mdx) · [`docs/bugbot/configuration.mdx`](../docs/bugbot/configuration.mdx) · [`docs/bugbot/finding-publication.mdx`](../docs/bugbot/finding-publication.mdx) · [`docs/bugbot/detection.mdx`](../docs/bugbot/detection.mdx) · [`docs/bugbot/how-it-works.mdx`](../docs/bugbot/how-it-works.mdx) · [`docs/bugbot/programmatic-api.mdx`](../docs/bugbot/programmatic-api.mdx) · [`docs/bugbot/quality-observability.mdx`](../docs/bugbot/quality-observability.mdx) · [`docs/bugbot/failure-scenarios.mdx`](../docs/bugbot/failure-scenarios.mdx) · [`docs/bugbot/examples.mdx`](../docs/bugbot/examples.mdx) · [`docs/single-actions/configuration.mdx`](../docs/single-actions/configuration.mdx) · [`docs/single-actions/available-actions.mdx`](../docs/single-actions/available-actions.mdx) · [`docs/development/architecture.mdx`](../docs/development/architecture.mdx) ### `release-orchestration` — Configurable production-first release orchestration - Owner: Copilot maintainers -- Last verified: 2026-09-12 +- Last verified: 2026-09-14 - Specifications: [`specs/configurable-release-orchestration.md`](./configurable-release-orchestration.md) · [`specs/configurable-release-orchestration-traceability.md`](./configurable-release-orchestration-traceability.md) · [`specs/deployment-concurrency-and-state-fencing.md`](./deployment-concurrency-and-state-fencing.md) - Workflows: [`.github/workflows/release_workflow.yml`](../.github/workflows/release_workflow.yml) · [`.github/workflows/hotfix_workflow.yml`](../.github/workflows/hotfix_workflow.yml) · [`.github/workflows/copilot_deployment_orchestration.yml`](../.github/workflows/copilot_deployment_orchestration.yml) · [`setup/workflows/release_workflow.yml`](../setup/workflows/release_workflow.yml) · [`setup/workflows/hotfix_workflow.yml`](../setup/workflows/hotfix_workflow.yml) · [`setup/workflows/copilot_deployment_orchestration.yml`](../setup/workflows/copilot_deployment_orchestration.yml) - Entrypoints: [`src/application/usecases/actions/deployment_orchestration_use_case.ts`](../src/application/usecases/actions/deployment_orchestration_use_case.ts) -- Core code: [`src/domain/deployment_configuration.ts`](../src/domain/deployment_configuration.ts) · [`src/domain/deployment_operation.ts`](../src/domain/deployment_operation.ts) · [`src/domain/deployment_state_fence.ts`](../src/domain/deployment_state_fence.ts) · [`src/domain/deployment_publication.ts`](../src/domain/deployment_publication.ts) · [`src/application/policies/deployment_lifecycle_policy.ts`](../src/application/policies/deployment_lifecycle_policy.ts) · [`src/application/policies/deployment_plan_policy.ts`](../src/application/policies/deployment_plan_policy.ts) · [`src/application/services/deployment_orchestration_dependencies.ts`](../src/application/services/deployment_orchestration_dependencies.ts) · [`src/application/services/deployment_orchestration_runtime.ts`](../src/application/services/deployment_orchestration_runtime.ts) · [`src/application/services/deployment_state_boundary.ts`](../src/application/services/deployment_state_boundary.ts) · [`src/application/usecases/actions/deployment_handlers/accept_promotion_handler.ts`](../src/application/usecases/actions/deployment_handlers/accept_promotion_handler.ts) · [`src/application/usecases/actions/deployment_handlers/confirm_publication_handler.ts`](../src/application/usecases/actions/deployment_handlers/confirm_publication_handler.ts) · [`src/application/usecases/actions/deployment_handlers/continue_deployment_handler.ts`](../src/application/usecases/actions/deployment_handlers/continue_deployment_handler.ts) · [`src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts`](../src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts) · [`src/application/usecases/actions/deployment_handlers/reconciliation_handler.ts`](../src/application/usecases/actions/deployment_handlers/reconciliation_handler.ts) · [`src/application/usecases/actions/deployment_handlers/record_failure_handler.ts`](../src/application/usecases/actions/deployment_handlers/record_failure_handler.ts) · [`src/data/repository/deployment/github_deployment_git_repository.ts`](../src/data/repository/deployment/github_deployment_git_repository.ts) · [`src/data/repository/deployment/github_managed_pull_request_repository.ts`](../src/data/repository/deployment/github_managed_pull_request_repository.ts) · [`src/data/repository/deployment/github_target_merge_capabilities_inspector.ts`](../src/data/repository/deployment/github_target_merge_capabilities_inspector.ts) · [`src/data/repository/deployment/deployment_state_repository.ts`](../src/data/repository/deployment/deployment_state_repository.ts) · [`src/data/repository/release/repository_release_publication_repository.ts`](../src/data/repository/release/repository_release_publication_repository.ts) · [`src/infrastructure/github/octokit_deployment_adapter.ts`](../src/infrastructure/github/octokit_deployment_adapter.ts) -- Tests: [`src/domain/__tests__/deployment_operation.test.ts`](../src/domain/__tests__/deployment_operation.test.ts) · [`src/domain/__tests__/deployment_state_fence.test.ts`](../src/domain/__tests__/deployment_state_fence.test.ts) · [`src/domain/__tests__/deployment_publication.test.ts`](../src/domain/__tests__/deployment_publication.test.ts) · [`src/application/services/__tests__/deployment_state_boundary.test.ts`](../src/application/services/__tests__/deployment_state_boundary.test.ts) · [`src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts`](../src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts) · [`src/application/usecases/actions/__tests__/deployment_phase_handlers.test.ts`](../src/application/usecases/actions/__tests__/deployment_phase_handlers.test.ts) · [`src/data/repository/deployment/__tests__/github_deployment_adapters.test.ts`](../src/data/repository/deployment/__tests__/github_deployment_adapters.test.ts) · [`src/application/__tests__/architecture_boundaries.test.ts`](../src/application/__tests__/architecture_boundaries.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) +- Core code: [`src/domain/deployment_configuration.ts`](../src/domain/deployment_configuration.ts) · [`src/domain/deployment_operation.ts`](../src/domain/deployment_operation.ts) · [`src/domain/locale.ts`](../src/domain/locale.ts) · [`src/domain/message_catalog.ts`](../src/domain/message_catalog.ts) · [`src/domain/deployment_state_fence.ts`](../src/domain/deployment_state_fence.ts) · [`src/domain/deployment_publication.ts`](../src/domain/deployment_publication.ts) · [`src/application/policies/deployment_lifecycle_policy.ts`](../src/application/policies/deployment_lifecycle_policy.ts) · [`src/application/policies/deployment_plan_policy.ts`](../src/application/policies/deployment_plan_policy.ts) · [`src/application/policies/deployment_message_catalog.ts`](../src/application/policies/deployment_message_catalog.ts) · [`src/application/policies/deployment_presentation_policy.ts`](../src/application/policies/deployment_presentation_policy.ts) · [`src/application/ports/message_catalog_ports.ts`](../src/application/ports/message_catalog_ports.ts) · [`src/application/services/deployment_orchestration_dependencies.ts`](../src/application/services/deployment_orchestration_dependencies.ts) · [`src/application/services/deployment_orchestration_runtime.ts`](../src/application/services/deployment_orchestration_runtime.ts) · [`src/application/services/deployment_state_boundary.ts`](../src/application/services/deployment_state_boundary.ts) · [`src/application/usecases/actions/deployment_handlers/accept_promotion_handler.ts`](../src/application/usecases/actions/deployment_handlers/accept_promotion_handler.ts) · [`src/application/usecases/actions/deployment_handlers/confirm_publication_handler.ts`](../src/application/usecases/actions/deployment_handlers/confirm_publication_handler.ts) · [`src/application/usecases/actions/deployment_handlers/continue_deployment_handler.ts`](../src/application/usecases/actions/deployment_handlers/continue_deployment_handler.ts) · [`src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts`](../src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts) · [`src/application/usecases/actions/deployment_handlers/reconciliation_handler.ts`](../src/application/usecases/actions/deployment_handlers/reconciliation_handler.ts) · [`src/application/usecases/actions/deployment_handlers/record_failure_handler.ts`](../src/application/usecases/actions/deployment_handlers/record_failure_handler.ts) · [`src/data/repository/deployment/github_deployment_git_repository.ts`](../src/data/repository/deployment/github_deployment_git_repository.ts) · [`src/data/repository/deployment/github_managed_pull_request_repository.ts`](../src/data/repository/deployment/github_managed_pull_request_repository.ts) · [`src/data/repository/deployment/github_target_merge_capabilities_inspector.ts`](../src/data/repository/deployment/github_target_merge_capabilities_inspector.ts) · [`src/data/repository/deployment/deployment_state_repository.ts`](../src/data/repository/deployment/deployment_state_repository.ts) · [`src/data/repository/release/repository_release_publication_repository.ts`](../src/data/repository/release/repository_release_publication_repository.ts) · [`src/infrastructure/github/octokit_deployment_adapter.ts`](../src/infrastructure/github/octokit_deployment_adapter.ts) +- Tests: [`src/domain/__tests__/deployment_operation.test.ts`](../src/domain/__tests__/deployment_operation.test.ts) · [`src/domain/__tests__/locale.test.ts`](../src/domain/__tests__/locale.test.ts) · [`src/domain/__tests__/message_catalog.test.ts`](../src/domain/__tests__/message_catalog.test.ts) · [`src/domain/__tests__/deployment_state_fence.test.ts`](../src/domain/__tests__/deployment_state_fence.test.ts) · [`src/domain/__tests__/deployment_publication.test.ts`](../src/domain/__tests__/deployment_publication.test.ts) · [`src/application/services/__tests__/deployment_state_boundary.test.ts`](../src/application/services/__tests__/deployment_state_boundary.test.ts) · [`src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts`](../src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts) · [`src/application/usecases/actions/__tests__/deployment_phase_handlers.test.ts`](../src/application/usecases/actions/__tests__/deployment_phase_handlers.test.ts) · [`src/application/policies/__tests__/deployment_message_catalog.test.ts`](../src/application/policies/__tests__/deployment_message_catalog.test.ts) · [`src/application/policies/__tests__/deployment_presentation_policy.test.ts`](../src/application/policies/__tests__/deployment_presentation_policy.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/data/repository/deployment/__tests__/github_deployment_adapters.test.ts`](../src/data/repository/deployment/__tests__/github_deployment_adapters.test.ts) · [`src/application/__tests__/architecture_boundaries.test.ts`](../src/application/__tests__/architecture_boundaries.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) - User documentation: [`docs/issues/deployment-orchestration.mdx`](../docs/issues/deployment-orchestration.mdx) · [`docs/issues/type/release.mdx`](../docs/issues/type/release.mdx) · [`docs/issues/type/hotfix.mdx`](../docs/issues/type/hotfix.mdx) · [`docs/development/release-process.mdx`](../docs/development/release-process.mdx) ### `merge-queue-readiness` — Merge queue readiness and effective target rules diff --git a/specs/catalog.json b/specs/catalog.json index 2635ce6c6..b27a53478 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -50,6 +50,7 @@ "src/application/policies/action_summary_policy.ts", "src/application/policies/branch_sync_notification_policy.ts", "src/application/policies/bugbot_message_catalog.ts", + "src/application/policies/deployment_message_catalog.ts", "src/application/usecases/actions/observe_branch_sync_use_case.ts", "src/application/policies/bugbot_review_presentation_policy.ts", "src/application/usecases/steps/commit/detect_potential_problems_workflow.ts", @@ -76,6 +77,7 @@ "src/application/policies/__tests__/branch_sync_notification_policy.test.ts", "src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts", "src/application/policies/__tests__/bugbot_message_catalog.test.ts", + "src/application/policies/__tests__/deployment_message_catalog.test.ts", "src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts", "src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts", "src/application/usecases/steps/commit/bugbot/__tests__/dismiss_bugbot_findings_use_case.test.ts", @@ -130,7 +132,7 @@ "status": "implemented", "scope": "Release and hotfix promotion, publication, reconciliation, and durable recovery", "owner": "Copilot maintainers", - "lastVerified": "2026-09-12", + "lastVerified": "2026-09-14", "specs": [ "specs/configurable-release-orchestration.md", "specs/configurable-release-orchestration-traceability.md", @@ -150,10 +152,15 @@ "code": [ "src/domain/deployment_configuration.ts", "src/domain/deployment_operation.ts", + "src/domain/locale.ts", + "src/domain/message_catalog.ts", "src/domain/deployment_state_fence.ts", "src/domain/deployment_publication.ts", "src/application/policies/deployment_lifecycle_policy.ts", "src/application/policies/deployment_plan_policy.ts", + "src/application/policies/deployment_message_catalog.ts", + "src/application/policies/deployment_presentation_policy.ts", + "src/application/ports/message_catalog_ports.ts", "src/application/services/deployment_orchestration_dependencies.ts", "src/application/services/deployment_orchestration_runtime.ts", "src/application/services/deployment_state_boundary.ts", @@ -172,11 +179,17 @@ ], "tests": [ "src/domain/__tests__/deployment_operation.test.ts", + "src/domain/__tests__/locale.test.ts", + "src/domain/__tests__/message_catalog.test.ts", "src/domain/__tests__/deployment_state_fence.test.ts", "src/domain/__tests__/deployment_publication.test.ts", "src/application/services/__tests__/deployment_state_boundary.test.ts", "src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts", "src/application/usecases/actions/__tests__/deployment_phase_handlers.test.ts", + "src/application/policies/__tests__/deployment_message_catalog.test.ts", + "src/application/policies/__tests__/deployment_presentation_policy.test.ts", + "src/actions/__tests__/github_action_completion.test.ts", + "src/application/usecases/__tests__/push_single_action_contexts.test.ts", "src/data/repository/deployment/__tests__/github_deployment_adapters.test.ts", "src/application/__tests__/architecture_boundaries.test.ts", "src/tooling/__tests__/validate_workflow_contract.test.ts" diff --git a/specs/configurable-release-orchestration.md b/specs/configurable-release-orchestration.md index 58d543cd3..5af57e672 100644 --- a/specs/configurable-release-orchestration.md +++ b/specs/configurable-release-orchestration.md @@ -3,7 +3,7 @@ - Status: Implemented; live rollout, human UX validation, and concurrency conformance hardening pending - Date: 2026-09-09 -- Last updated: 2026-09-11 +- Last updated: 2026-09-14 - Owners: Copilot maintainers - Scope: release and hotfix promotion, publication, reconciliation, and cleanup - Related proposals: @@ -359,8 +359,18 @@ The following inputs retain their current meaning: - `hotfix-tree` - `release-workflow` - `hotfix-workflow` -- `issues-locale`, used for the issue control center and issue comments -- `pull-requests-locale`, used for managed PR titles and descriptions +- `repository-locale`, default `en-US`, used for repository/run surfaces such as + the deployment Job Summary +- `issues-locale`, an optional override for the issue control center and issue + milestones; empty inherits `repository-locale` +- `pull-requests-locale`, an optional override for managed PR titles and + descriptions; empty inherits `repository-locale` + +New operations snapshot the complete canonical locale profile with the branch +and strategy facts, so changing repository Variables cannot switch language +mid-operation. Existing version-1 state created before this field was introduced +remains valid and uses the current effective profile until it completes; it is +not rewritten solely to add locale metadata. ### 6.5 Project publication timing @@ -802,13 +812,13 @@ The guided presentation follows this information hierarchy: ```markdown -# 🚀 Release 3.4.0 +## 🚀 Release 3.4.0 > **Current status: waiting for production approval** > > No action is required while GitHub checks are running. -## Progress +### Progress - [x] Release cut from `develop` at [`abc1234`](...) - [x] Version files, build, validation, and smoke test @@ -817,18 +827,18 @@ The guided presentation follows this information hierarchy: - [ ] Reconciliation into `develop` - [ ] Cleanup and issue completion -## Current transition +### Current transition | From | To | State | |---|---|---| | `release/3.4.0` | `master` | ⏳ Checks and review | -## What happens next +### What happens next After [PR #401](...) is merged, Copilot will tag the accepted `master` commit, publish `@vypdev/copilot@3.4.0`, and start development reconciliation. -## Links +### Links [Promotion PR](...) · [Compare changes](...) · [Workflow run](...) @@ -864,22 +874,22 @@ instruction with a direct link or copyable command. A blocked presentation uses the following order: ```markdown -# ❌ Release 3.4.0 needs attention +## ❌ Release 3.4.0 needs attention > **Published package:** No > **Production updated:** No > **Development synchronized:** No -## What happened +### What happened The production PR was closed without merging, so publication was stopped. -## Action required +### Action required Reopen [PR #401](...) or run `/copilot retry-release` after correcting the problem. -## What Copilot protected +### What Copilot protected No npm version, GitHub Release, or version tag was created. @@ -1036,14 +1046,24 @@ automation. Labels supplement, but never replace, the PR body status. ### 16.10 Localization -Issue-facing presentation uses `issues-locale`; PR-facing presentation uses -`pull-requests-locale`. +Issue-facing presentation uses the effective issue locale; PR-facing +presentation uses the effective pull-request locale; the Job Summary uses the +repository locale. Empty issue and PR overrides inherit `repository-locale`, +whose default is `en-US`. All orchestration-owned headings, status sentences, instructions, table labels, and failure guidance MUST come from a typed message catalog rather than scattered -string literals. The first implementation MUST provide complete `en-US` and -`es-ES` catalogs. An unsupported locale falls back to `en-US` and emits one -visible, non-blocking warning in technical details. +string literals. The implementation provides complete `en-US` and `es-ES` +catalogs. Any other valid BCP-47 locale uses one schema-constrained dynamic +catalog resolution for the required slice. Missing, invalid, unsafe, or +unavailable dynamic output falls back atomically to `en-US`; fallback is recorded +in the Job Summary and never creates a second timeline comment or a mixed-language +artifact. + +The localization-evidence heading, table labels, descriptor count, and +fallback-reason labels in that Job Summary use the same repository-locale +catalog. Locale tags, catalog source names, descriptor counts, and +machine-readable fallback reasons remain provider facts and are not translated. Branch names, package names, tag names, GitHub check names, and copied provider facts are not translated. @@ -1069,16 +1089,23 @@ facts are not translated. ### 16.12 Job Summary -The Job Summary mirrors the issue's current facts but is optimized for operators: +The Job Summary projects the operation's current semantic facts for operators; +it does not replay internal use-case steps: - result and phase at the top; - transition performed by this invocation; - verified input/output SHAs; -- API operations created/reused/skipped; +- durable operation identity, revision, origin, prepared source, and accepted + production fact; - publication evidence; - pending external dependency; - sanitized failure classification and retryability; and -- a final artifact/PR link table. +- a final set of descriptive artifact and PR links. + +Raw `Result.steps`, provider narration, and duplicate “created/reused/skipped” +logs remain in machine-searchable logs. They are not copied into the Job Summary +because the operation state and descriptive links already communicate the useful +result. It MUST distinguish `waiting externally` from `workflow failure`. A workflow that successfully creates a pending auto-merge PR finishes green and reports the @@ -1395,8 +1422,9 @@ deviation. Deviations require an explicit update to this specification. configuration-controlled fallback. - Build every URL from verified repository entities and sanitize all untrusted values before Markdown publication. -- Keep generic `Result.steps` for short generic feedback only; deployment - orchestration uses its dedicated structured presentation boundary. +- Keep `Result.steps` as internal execution evidence; deployment orchestration + uses its dedicated structured presentation boundary and never replays those + steps into GitHub conversation UI or its Job Summary. ### 20.6 Documentation diff --git a/specs/repository-locale-and-localization.md b/specs/repository-locale-and-localization.md index 3cf613182..b0a72967f 100644 --- a/specs/repository-locale-and-localization.md +++ b/specs/repository-locale-and-localization.md @@ -3,7 +3,9 @@ - Status: In implementation - Date: 2026-09-14 - Catalog capability ID: github-communication-experience -- Last verified: 2026-09-14 for the delivered foundation, shared publication, branch-sync, and Bugbot slices; remaining clauses are prospective +- Last verified: 2026-09-14 for the delivered foundation, shared publication, + branch-sync, Bugbot, and deployment-presentation slices; remaining clauses + are prospective - Owners: Copilot maintainers - Scope: Define one English-default repository locale profile and apply it generically to deterministic UI, agent-generated content, and safe interpretation of addressed comments. - Related issues/PRs: [issue #334](https://github.com/vypdev/copilot/issues/334), [PR #363](https://github.com/vypdev/copilot/pull/363), [PR #365](https://github.com/vypdev/copilot/pull/365) @@ -60,9 +62,11 @@ participant's message is a poor authorship and trust boundary: the timeline no longer shows exactly what that person wrote as the primary body, and downstream automation can observe model-generated text under the human comment identity. -### 2.2 Current behavior +### 2.2 Baseline behavior before rollout -The following facts are verified in the 2026-09-14 repository snapshot: +The following facts describe the pre-PR-#366 baseline audited on 2026-09-14. +They are retained as migration evidence and are not claims about the current +implementation: 1. `Locale` is a mutable data model with only `issue` and `pullRequest` strings; `Locale.DEFAULT` is `en-US`. @@ -804,6 +808,7 @@ I couldn't safely interpret this request in `fr-FR`, so no repository change was | Addressed request translation fails | no repository mutation | original human comment | user retry only | rephrase or restore provider | one fallback reply | | Catalog misses one required ID | build fails for bundled catalog; runtime dynamic slice falls back atomically | English catalog | no | fix catalog manifest | no partial catalog publication | | Locale changes during durable release | in-flight operation remains in snapshot locale | operation state and cards | next operation uses new profile | none | no historical rewrite | +| Legacy version-1 release state has no locale snapshot | in-flight operation uses the currently resolved profile | legacy operation and irreversible facts | each continuation remains safe | finish the operation, then use a fresh issue for a frozen profile | no state rewrite | | Locale changes for normal status card | old language remains until next semantic update | card identity/state | rerender on next update | optional explicit status command | history untouched | | Legacy v2 translated human comment | historic authorship surface remains as-is | translated body plus embedded original | no automatic rewrite | user may edit own comment manually | reader remains tolerant | @@ -932,18 +937,23 @@ pull-requests-locale: "" 7. Update all named docs, generated workflows/action bundles, and related SDD clauses before announcing support. -Implementation evidence as of 2026-09-14: PRs #366–#372 deliver the canonical +Implementation evidence as of 2026-09-14: PRs #366–#373 deliver the canonical locale profile, typed and validated catalog resolution, English-default shared publication, non-mutating addressed-language adaptation, localized agent response contracts, branch synchronization, and bounded review context. The -current Bugbot slice adds one typed catalog resolution per publishing operation +Bugbot slice adds one typed catalog resolution per publishing operation and reuses it across status cards, review snapshots, inline and issue findings, overflow, and resolution notes. Exact/base Spanish resolution, arbitrary BCP-47 dynamic resolution, atomic English fallback, issue-versus-PR scope selection, target-locale cardinal plural completeness, and skip/dry-run/no-mutation no-call -behavior are executable tests. Deployment, merge -readiness, lifecycle, setup/doctor, and the remaining public surfaces are not -claimed complete by this evidence. +behavior are executable tests. The deployment-presentation slice snapshots the +effective locale for new durable operations, resolves one catalog per issue or +managed-PR destination, renders repository-locale Job Summaries, localizes the +four bounded milestones, and removes internal `Result.steps` from deployment +operator UI. Legacy version-1 operations without a locale remain readable and +use the current effective profile until completion. Merge-readiness diagnostics, +lifecycle, setup/doctor, and the remaining public surfaces are not claimed +complete by this evidence. ### 13.5 Rollback diff --git a/src/actions/__tests__/github_action_completion.test.ts b/src/actions/__tests__/github_action_completion.test.ts index 8efe3e603..32e5a0be7 100644 --- a/src/actions/__tests__/github_action_completion.test.ts +++ b/src/actions/__tests__/github_action_completion.test.ts @@ -4,6 +4,8 @@ import { Result } from '../../data/model/result'; import { finishGithubAction } from '../github_action_completion'; import * as core from '@actions/core'; import { ApplicationError } from '../../application/errors/application_error'; +import type { DeploymentOperationSnapshot } from '../../domain/deployment_operation'; +import { ResolveMessageCatalogUseCase } from '../../application/usecases/localization/resolve_message_catalog_use_case'; jest.mock('@actions/core', () => ({ setOutput: jest.fn(), setFailed: jest.fn() })); @@ -29,6 +31,43 @@ const recommendationState = { recommendation: '1. Add tests', }; +function deploymentOperation(): DeploymentOperationSnapshot { + return { + stateVersion: 1, + revision: 3, + operationId: 'operation-12345678', + locale: { repository: 'es-ES', issue: 'fr-FR', pullRequest: 'de-DE', issueOverride: 'fr-FR', pullRequestOverride: 'de-DE' }, + kind: 'release', + version: '3.4.0', + title: 'Release', + changelog: 'Changes', + phase: 'promotion_pr_pending', + strategy: 'production-lineage', + prMode: 'auto', + selectedPrMode: 'auto-merge', + backmergeMode: 'auto', + hotfixActiveReleasePolicy: 'prefer-release', + cleanup: 'all', + issueCompletion: 'close', + presentationMode: 'guided', + diagrams: true, + commentMode: 'update', + sourceBranch: 'release/3.4.0', + sourceSha: 'a'.repeat(40), + originBranch: 'develop', + originSha: 'b'.repeat(40), + productionBranch: 'master', + developmentBranch: 'develop', + reconciliationTree: 'sync', + promotionPullRequest: 40, + tag: 'v3.4.0', + publicationWorkflow: 'release_workflow.yml', + publicationVerified: false, + reconciliationTargets: [], + lastFailure: null, + }; +} + function execution(): Execution { return { owner: 'test-owner', @@ -215,6 +254,47 @@ describe('finishGithubAction', () => { expect(mockSummaryPublish).toHaveBeenCalledWith(expect.stringContaining('test-owner/test-repo')); }); + it('uses the durable repository locale for deployment summaries without replaying internal steps', async () => { + const action = Object.assign(singleActionExecution(), { + singleAction: { + ...singleActionExecution().singleAction, + issue: 11, + isDeploymentOrchestrationAction: true, + }, + currentConfiguration: { results: [], deploymentOrchestration: deploymentOperation() }, + }) as Execution; + const resolver = new ResolveMessageCatalogUseCase(); + const resolve = jest.spyOn(resolver, 'resolve'); + const results = [new Result({ + id: 'DeploymentOrchestrationUseCase', + success: true, + executed: true, + steps: ['Created promotion PR #40'], + })]; + + await finishGithubAction( + action, + results, + {} as never, + {} as never, + undefined, + { publish: mockSummaryPublish }, + resolver, + ); + + const summary = mockSummaryPublish.mock.calls[0][0] as string; + expect(summary).toContain('# ⏳ Orquestación del despliegue'); + expect(summary).not.toContain('Created promotion PR #40'); + expect(summary).toContain('## Localización'); + expect(summary).toContain('| Propiedad | Valor |'); + expect(summary).toContain('| Locale del repositorio | `es-ES` |'); + expect(summary).not.toContain('## Localization'); + expect(summary).toContain('`es-ES -> es-ES (exact'); + expect(resolve).toHaveBeenCalledTimes(1); + expect(resolve).toHaveBeenCalledWith(expect.objectContaining({ targetLocale: 'es-ES' })); + expect(mockPublishInvoke).not.toHaveBeenCalled(); + }); + it('uses the short-lived evidence token only for the native Check Run', async () => { process.env.COPILOT_EVIDENCE_TOKEN = 'github-actions-token'; const action = Object.assign(execution(), { diff --git a/src/actions/github_action_completion.ts b/src/actions/github_action_completion.ts index 020bf30fd..8a2295108 100644 --- a/src/actions/github_action_completion.ts +++ b/src/actions/github_action_completion.ts @@ -10,7 +10,11 @@ import { projectPublishResultContext } from '../application/usecases/steps/commo import { projectConfigurationPersistenceContext } from '../application/usecases/steps/common/store_configuration_use_case'; import { logInfo } from '../utils/logger'; -import { buildActionSummary, renderLocalizationSummarySection } from '../application/policies/action_summary_policy'; +import { + buildActionSummary, + renderLocalizationSummarySection, + type LocalizationSummaryLabels, +} from '../application/policies/action_summary_policy'; import { lifecycleStateFromLabels } from '../domain/copilot_lifecycle'; import type { CopilotEvidencePort } from '../application/ports/copilot_evidence_ports'; import { buildCopilotEvidence } from '../application/policies/copilot_evidence_policy'; @@ -18,6 +22,7 @@ import type { ActionSummaryPort } from '../application/ports/action_summary_port import { toApplicationError } from '../application/errors/application_error'; import { shouldPersistConfiguration } from '../application/policies/configuration_persistence_policy'; import { renderDeploymentJobSummary } from '../application/policies/deployment_presentation_policy'; +import { deploymentCopy, resolveDeploymentCatalog } from '../application/policies/deployment_message_catalog'; import { projectBugbotResultFindingStates } from '../application/policies/bugbot_result_finding_state_projection_policy'; import { countActionableBugbotFindings } from '../domain/bugbot/review_state'; import type { MessageCatalogResolutionPort } from '../application/ports/message_catalog_ports'; @@ -82,26 +87,44 @@ async function writeActionSummary( ): Promise { const operation = execution.currentConfiguration.deploymentOrchestration; const locale = execution.locale ?? { repository: 'en-US', issue: 'en-US', pullRequest: 'en-US' }; - const localizationEvidence = renderLocalizationSummarySection({ - repository: locale.repository, - issue: locale.issue, - pullRequest: locale.pullRequest, - }, catalogResolver?.observations?.() ?? []); - const summaryText = execution.singleAction.isDeploymentOrchestrationAction && operation - ? [renderDeploymentJobSummary(operation, { + const summaryLocale = execution.singleAction.isDeploymentOrchestrationAction && operation?.locale + ? operation.locale + : locale; + let body: string; + let localizationLabels: LocalizationSummaryLabels | undefined; + if (execution.singleAction.isDeploymentOrchestrationAction && operation) { + const effectiveLocale = operation.locale ?? locale; + const catalog = await resolveDeploymentCatalog( + effectiveLocale.repository, + execution.ai.getAgentConfiguration('planner'), + catalogResolver, + ); + const messages = deploymentCopy(catalog); + localizationLabels = { + heading: messages.localization, + property: messages.property, + value: messages.value, + repositoryLocale: messages.repositoryLocaleLabel, + issueLocale: messages.issueLocaleLabel, + pullRequestLocale: messages.pullRequestLocaleLabel, + catalogResolution: messages.catalogResolution, + descriptors: messages.descriptors, + reason: messages.reason, + }; + body = renderDeploymentJobSummary(operation, { owner: execution.owner, repository: execution.repo, issue: execution.singleAction.issue, - issueLocale: locale.issue, - pullRequestLocale: locale.pullRequest, + repositoryLocale: effectiveLocale.repository, + issueLocale: effectiveLocale.issue, + pullRequestLocale: effectiveLocale.pullRequest, packageName: execution.owner === 'vypdev' && execution.repo === 'copilot' ? '@vypdev/copilot' : undefined, workflowRunUrl: process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` : undefined, - }, operation.lastFailure?.previousPhase, execution.currentConfiguration.results.flatMap((result) => result.steps ?? [])), localizationEvidence] - .filter(Boolean) - .join('\n\n') - : buildActionSummary({ + }, operation.lastFailure?.previousPhase, catalog); + } else { + body = buildActionSummary({ owner: execution.owner, repository: execution.repo, eventName: execution.eventName, @@ -123,6 +146,13 @@ async function writeActionSummary( catalogResolutions: catalogResolver?.observations?.() ?? [], results: execution.currentConfiguration.results, }); + } + const localizationEvidence = renderLocalizationSummarySection({ + repository: summaryLocale.repository, + issue: summaryLocale.issue, + pullRequest: summaryLocale.pullRequest, + }, catalogResolver?.observations?.() ?? [], localizationLabels); + const summaryText = [body, localizationEvidence].filter(Boolean).join('\n\n'); if (!summaryPort) return summaryText; try { await summaryPort.publish(summaryText); diff --git a/src/application/policies/__tests__/deployment_message_catalog.test.ts b/src/application/policies/__tests__/deployment_message_catalog.test.ts new file mode 100644 index 000000000..068922fec --- /dev/null +++ b/src/application/policies/__tests__/deployment_message_catalog.test.ts @@ -0,0 +1,89 @@ +import type { CatalogMessage } from '../../../domain/message_catalog'; +import { catalogPlaceholders, validateCatalogDefinition } from '../../../domain/message_catalog'; +import { ResolveMessageCatalogUseCase } from '../../usecases/localization/resolve_message_catalog_use_case'; +import { + DEPLOYMENT_CATALOG_DEFINITIONS, + DEPLOYMENT_MESSAGE_IDS, + ENGLISH_DEPLOYMENT_DEFINITION, + SPANISH_DEPLOYMENT_DEFINITION, + deploymentCopy, + resolveDeploymentCatalog, + resolveStaticDeploymentCatalog, +} from '../deployment_message_catalog'; + +const configuration = { provider: 'codex' as const, model: 'model' }; + +function translatedMessages(): Record { + return Object.fromEntries(Object.entries(ENGLISH_DEPLOYMENT_DEFINITION.messages).map(([id, message]) => [ + id, + `FR ${message as string}`, + ])); +} + +describe('deployment message catalog', () => { + it('ships complete, unique, immutable English and Spanish definitions', () => { + expect(new Set(DEPLOYMENT_MESSAGE_IDS).size).toBe(DEPLOYMENT_MESSAGE_IDS.length); + for (const definition of DEPLOYMENT_CATALOG_DEFINITIONS) { + expect(validateCatalogDefinition(definition, DEPLOYMENT_MESSAGE_IDS)).toEqual([]); + expect(Object.isFrozen(definition)).toBe(true); + expect(Object.isFrozen(definition.messages)).toBe(true); + } + for (const id of DEPLOYMENT_MESSAGE_IDS) { + expect(catalogPlaceholders(SPANISH_DEPLOYMENT_DEFINITION.messages[id])) + .toEqual(catalogPlaceholders(ENGLISH_DEPLOYMENT_DEFINITION.messages[id])); + } + }); + + it.each([ + ['en-US', 'en-US', 'exact'], + ['en-GB', 'en-US', 'base'], + ['es-ES', 'es-ES', 'exact'], + ['es-MX', 'es-ES', 'base'], + ['fr-FR', 'en-US', 'fallback'], + ] as const)('resolves %s deterministically as %s', (requested, resolved, source) => { + const catalog = resolveStaticDeploymentCatalog(requested); + expect(catalog).toMatchObject({ requestedLocale: requested, locale: resolved, resolutionSource: source }); + }); + + it('projects a deeply immutable typed copy', () => { + const copy = deploymentCopy(resolveStaticDeploymentCatalog('es-ES')); + expect(copy.currentStatus).toBe('Estado actual'); + expect(copy.phase.publishing).toBe('publicando artefactos'); + expect(copy.diagram.complete).toBe('Completada'); + expect(Object.isFrozen(copy)).toBe(true); + expect(Object.isFrozen(copy.phase)).toBe(true); + expect(Object.isFrozen(copy.diagram)).toBe(true); + }); + + it('renders one complete cached dynamic catalog for an arbitrary BCP-47 locale', async () => { + const query = jest.fn().mockResolvedValue({ targetLocale: 'fr-FR', messages: translatedMessages() }); + const resolver = new ResolveMessageCatalogUseCase({ query }); + const catalog = await resolveDeploymentCatalog('fr-FR', configuration, resolver); + const replay = await resolveDeploymentCatalog('fr-FR', configuration, resolver); + + expect(catalog).toMatchObject({ requestedLocale: 'fr-FR', locale: 'fr-FR', resolutionSource: 'dynamic' }); + expect(catalog.message('deployment.template.promotionTitle', { + kind: 'Release', version: '3.4.0', branch: 'master', + })).toBe('FR Release(3.4.0): promote to master'); + expect(replay.message('deployment.currentStatus')).toBe('FR Current status'); + expect(query).toHaveBeenCalledTimes(1); + }); + + it('falls back atomically to English when one dynamic descriptor is missing', async () => { + const messages = translatedMessages(); + delete messages['deployment.currentStatus']; + const resolver = new ResolveMessageCatalogUseCase({ + query: jest.fn().mockResolvedValue({ targetLocale: 'fr-FR', messages }), + }); + const catalog = await resolveDeploymentCatalog('fr-FR', configuration, resolver); + + expect(catalog).toMatchObject({ + requestedLocale: 'fr-FR', + locale: 'en-US', + resolutionSource: 'fallback', + fallbackReason: 'dynamic-response-invalid', + }); + expect(catalog.message('deployment.currentStatus')).toBe('Current status'); + expect(catalog.message('deployment.progress')).not.toContain('FR '); + }); +}); diff --git a/src/application/policies/__tests__/deployment_plan_policy.test.ts b/src/application/policies/__tests__/deployment_plan_policy.test.ts index 11b7bdca8..d638a5c0d 100644 --- a/src/application/policies/__tests__/deployment_plan_policy.test.ts +++ b/src/application/policies/__tests__/deployment_plan_policy.test.ts @@ -14,6 +14,11 @@ import { } from "../deployment_plan_policy"; const sha = (letter: string) => letter.repeat(40); +const locale = Object.freeze({ + repository: "en-US", + issue: "en-US", + pullRequest: "en-US", +}); const capabilities = (overrides: Partial = {}): TargetMergeCapabilities => ({ autoMergeAllowed: true, mergeQueueRequired: false, @@ -67,6 +72,7 @@ describe("deployment plan policy", () => { it("snapshots a release plan from the development cut", () => { const value = buildInitialDeploymentOperation({ operationId: "operation-12345678", + locale, kind: "release", version: "3.4.0", title: "Release", @@ -80,12 +86,14 @@ describe("deployment plan policy", () => { configuration: { ...DEFAULT_DEPLOYMENT_CONFIGURATION }, publicationWorkflow: "release_workflow.yml", }); - expect(value).toEqual(expect.objectContaining({ phase: "preparing", originBranch: "develop", originSha: sha("b"), tag: "v3.4.0" })); + expect(value).toEqual(expect.objectContaining({ phase: "preparing", originBranch: "develop", originSha: sha("b"), tag: "v3.4.0", locale })); + expect(Object.isFrozen(value.locale)).toBe(true); }); it("selects the hotfix strategy independently", () => { const value = buildInitialDeploymentOperation({ operationId: "operation-12345678", kind: "hotfix", version: "3.4.1", title: "Hotfix", changelog: "Fix", + locale, sourceBranch: "hotfix/3.4.1", sourceSha: sha("a"), originBranch: "v3.4.0", originSha: sha("b"), productionBranch: "master", developmentBranch: "develop", publicationWorkflow: "hotfix_workflow.yml", configuration: { ...DEFAULT_DEPLOYMENT_CONFIGURATION, hotfixReconciliationStrategy: "canonical-gitflow" }, @@ -240,6 +248,7 @@ describe("deployment plan policy", () => { it("rejects invalid initial deployment facts", () => { const input = { operationId: "operation-12345678", kind: "release" as const, version: "v3", title: "", changelog: "", + locale, sourceBranch: "master", sourceSha: "bad", originBranch: "develop", originSha: "bad", productionBranch: "master", developmentBranch: "develop", publicationWorkflow: "release_workflow.yml", configuration: { ...DEFAULT_DEPLOYMENT_CONFIGURATION }, diff --git a/src/application/policies/__tests__/deployment_presentation_policy.test.ts b/src/application/policies/__tests__/deployment_presentation_policy.test.ts index 0b4c2b7cb..377260db5 100644 --- a/src/application/policies/__tests__/deployment_presentation_policy.test.ts +++ b/src/application/policies/__tests__/deployment_presentation_policy.test.ts @@ -1,9 +1,10 @@ import type { DeploymentOperationSnapshot, DeploymentPhase } from "../../../domain/deployment_operation"; +import { resolveStaticDeploymentCatalog } from "../deployment_message_catalog"; import { deploymentDashboardMarker, - normalizeLocale, renderDeploymentDashboard, renderDeploymentJobSummary, + renderDeploymentMilestone, renderPromotionPullRequest, renderReconciliationPullRequest, } from "../deployment_presentation_policy"; @@ -48,6 +49,7 @@ const context = { owner: "vypdev", repository: "copilot", issue: 355, + repositoryLocale: "en-US", issueLocale: "en-US", pullRequestLocale: "en-US", workflowRunUrl: "https://github.com/vypdev/copilot/actions/runs/1", @@ -62,8 +64,8 @@ describe("deployment presentation policy", () => { it("orders guided status before progress and technical detail", () => { const body = renderDeploymentDashboard(operation(), context); - expect(body.indexOf("Current status")).toBeLessThan(body.indexOf("## Progress")); - expect(body.indexOf("## Progress")).toBeLessThan(body.indexOf("Technical details")); + expect(body.indexOf("Current status")).toBeLessThan(body.indexOf("### Progress")); + expect(body.indexOf("### Progress")).toBeLessThan(body.indexOf("Technical details")); }); it("renders a fixed-label Mermaid diagram plus text fallback", () => { @@ -98,7 +100,7 @@ describe("deployment presentation policy", () => { it("suppresses secondary sections in quiet mode but preserves state", () => { const body = renderDeploymentDashboard(operation("publishing", { presentationMode: "quiet" }), context); expect(body).toContain("publishing artifacts"); - expect(body).not.toContain("## Progress"); + expect(body).not.toContain("### Progress"); expect(body).toContain("Technical details"); }); @@ -115,10 +117,21 @@ describe("deployment presentation policy", () => { expect(body).toContain("❌"); expect(body).toContain("needs attention"); expect(body).toContain("PR was closed"); - expect(body.indexOf("## Action required")).toBeLessThan(body.indexOf("## Progress")); + expect(body.indexOf("### Action required")).toBeLessThan(body.indexOf("### Progress")); expect(body).toContain("Production updated"); }); + it("renders safe fallback guidance when legacy blocked state has no failure payload", () => { + const blocked = operation("blocked", { lastFailure: null }); + const dashboard = renderDeploymentDashboard(blocked, context); + const summary = renderDeploymentJobSummary(blocked, context); + + expect(dashboard).toContain("Workflow failed"); + expect(dashboard).toContain("Manual intervention is required"); + expect(dashboard).not.toContain("### What happens next"); + expect(summary).toContain("Workflow failed. Manual intervention is required."); + }); + it("distinguishes published-but-not-reconciled state", () => { const body = renderDeploymentDashboard(operation("reconciliation_pending", { publicationVerified: true, productionSha: "c".repeat(40) }), context); expect(body).toContain("Package status: already published"); @@ -156,14 +169,15 @@ describe("deployment presentation policy", () => { expect(dashboard).toContain("Reconciliación con desarrollo"); expect(promotion.body).toContain("Listo antes de revisar"); expect(promotion.body).toContain("Después del merge"); + expect(promotion.title).toBe("release(3.4.0): promover a master"); }); - it("falls unsupported locales back to English", () => { - expect(normalizeLocale("fr-FR")).toBe("en-US"); + it("falls unsupported static locales back to English", () => { + expect(renderDeploymentDashboard(operation(), { ...context, issueLocale: "fr-FR" })).toContain("Current status"); }); - it("normalizes regional Spanish locales", () => { - expect(normalizeLocale("es-MX")).toBe("es-ES"); + it("uses the bundled base-language catalog for regional Spanish locales", () => { + expect(renderDeploymentDashboard(operation(), { ...context, issueLocale: "es-MX" })).toContain("Estado actual"); }); it("renders a deterministic promotion PR with ownership marker", () => { @@ -171,6 +185,7 @@ describe("deployment presentation policy", () => { expect(value.title).toBe("release(3.4.0): promote to master"); expect(value.body).toContain('phase="promotion" issue="355"'); expect(value.body).toContain("After merge"); + expect(value.body.match(/After merge, Copilot will tag and publish the accepted production commit\./gu)).toHaveLength(1); }); it("renders a reconciliation PR that cannot republish", () => { @@ -216,30 +231,56 @@ describe("deployment presentation policy", () => { }); it("renders a pending external transition as a successful wait in the Job Summary", () => { - const summary = renderDeploymentJobSummary(operation(), context, "preparing", ["Created promotion PR #401"]); + const summary = renderDeploymentJobSummary(operation(), context, "preparing"); expect(summary).toContain("Waiting externally"); expect(summary).not.toContain("Workflow failed"); - expect(summary).toContain("Created promotion PR #401"); + expect(summary).not.toContain("Created promotion PR #401"); }); it("renders blocked retryability and sanitizes Job Summary operations", () => { const summary = renderDeploymentJobSummary(operation("blocked", { lastFailure: { category: "publication", message: "bad", retryable: true, previousPhase: "publishing" }, - }), context, "publishing", ["::error:: @team\n# forged"]); + }), context, "publishing"); expect(summary).toContain("Workflow failed"); expect(summary).toContain("| `publishing` | `blocked` | Yes |"); expect(summary).toContain("## Action required"); expect(summary).toContain("bad. Retry after correcting the cause."); - expect(summary).not.toContain("::error::"); - expect(summary).not.toContain("@team"); }); it("renders equivalent Spanish recovery guidance in dashboards and Job Summaries", () => { const blocked = operation("blocked", { lastFailure: { category: "promotion", message: "Falta merge_group", retryable: true, previousPhase: "preparing" }, }); - const localized = { ...context, issueLocale: "es-ES" }; + const localized = { ...context, repositoryLocale: "es-ES", issueLocale: "es-ES" }; expect(renderDeploymentDashboard(blocked, localized)).toContain("Vuelve a intentarlo después de corregir la causa"); expect(renderDeploymentJobSummary(blocked, localized)).toContain("## Acción necesaria"); }); + + it("localizes semantic milestones without exposing workflow step logs", () => { + const catalog = resolveStaticDeploymentCatalog("es-ES"); + expect(renderDeploymentMilestone({ + kind: "promotion-merged", + pullRequest: 401, + productionSha: "c".repeat(40), + }, catalog)).toContain("La PR de promoción #401 se ha mergeado"); + expect(renderDeploymentMilestone({ + kind: "publication-complete", + tag: "v3.4.0", + productionSha: "c".repeat(40), + }, catalog)).toContain("se ha publicado desde el SHA de producción aceptado"); + expect(renderDeploymentMilestone({ + kind: "reconciliation-blocked", + reason: "No se puede reconciliar", + }, catalog)).toBe("❌ Despliegue bloqueado: No se puede reconciliar"); + expect(renderDeploymentMilestone({ + kind: "orchestration-complete", + tag: "v3.4.0", + }, catalog)).toContain("todos los destinos de reconciliación configurados"); + }); + + it("bounds managed PR titles after catalog rendering", () => { + const long = "x".repeat(500); + const value = renderPromotionPullRequest(operation(undefined, { productionBranch: long }), context); + expect(value.title).toHaveLength(240); + }); }); diff --git a/src/application/policies/action_summary_policy.ts b/src/application/policies/action_summary_policy.ts index be7439c16..00f17338b 100644 --- a/src/application/policies/action_summary_policy.ts +++ b/src/application/policies/action_summary_policy.ts @@ -27,6 +27,30 @@ export interface ActionSummaryContext { readonly catalogResolutions?: readonly CatalogResolutionObservation[]; } +export interface LocalizationSummaryLabels { + readonly heading: string; + readonly property: string; + readonly value: string; + readonly repositoryLocale: string; + readonly issueLocale: string; + readonly pullRequestLocale: string; + readonly catalogResolution: string; + readonly descriptors: string; + readonly reason: string; +} + +const ENGLISH_LOCALIZATION_SUMMARY_LABELS: LocalizationSummaryLabels = Object.freeze({ + heading: 'Localization', + property: 'Property', + value: 'Value', + repositoryLocale: 'Repository locale', + issueLocale: 'Issue locale', + pullRequestLocale: 'Pull-request locale', + catalogResolution: 'Catalog resolution', + descriptors: 'descriptors', + reason: 'reason', +}); + /** Builds a bounded, publication-safe GitHub Actions Job Summary. */ export function buildActionSummary(context: ActionSummaryContext): string { const failures = context.results.filter(result => !result.success && result.executed); @@ -77,13 +101,14 @@ export function buildActionSummary(context: ActionSummaryContext): string { export function renderLocalizationSummarySection( locale: ActionSummaryContext['locale'], catalogResolutions: ActionSummaryContext['catalogResolutions'] = [], + labels: LocalizationSummaryLabels = ENGLISH_LOCALIZATION_SUMMARY_LABELS, ): string { - const rows = localizationSummaryRows(locale, catalogResolutions); + const rows = localizationSummaryRows(locale, catalogResolutions, labels); if (rows.length === 0) return ''; return [ - '## Localization', + `## ${labels.heading}`, '', - '| Property | Value |', + `| ${labels.property} | ${labels.value} |`, '| --- | --- |', ...rows, '', @@ -93,23 +118,27 @@ export function renderLocalizationSummarySection( function localizationSummaryRows( locale: ActionSummaryContext['locale'], catalogResolutions: ActionSummaryContext['catalogResolutions'] = [], + labels: LocalizationSummaryLabels = ENGLISH_LOCALIZATION_SUMMARY_LABELS, ): string[] { return [ ...(locale ? [ - `| Repository locale | \`${escapeTable(locale.repository)}\` |`, - `| Issue locale | \`${escapeTable(locale.issue)}\` |`, - `| Pull-request locale | \`${escapeTable(locale.pullRequest)}\` |`, + `| ${labels.repositoryLocale} | \`${escapeTable(locale.repository)}\` |`, + `| ${labels.issueLocale} | \`${escapeTable(locale.issue)}\` |`, + `| ${labels.pullRequestLocale} | \`${escapeTable(locale.pullRequest)}\` |`, ] : []), ...(catalogResolutions.length ? [ - `| Catalog resolution | ${formatCatalogResolutions(catalogResolutions)} |`, + `| ${labels.catalogResolution} | ${formatCatalogResolutions(catalogResolutions, labels)} |`, ] : []), ]; } -function formatCatalogResolutions(observations: readonly CatalogResolutionObservation[]): string { +function formatCatalogResolutions( + observations: readonly CatalogResolutionObservation[], + labels: LocalizationSummaryLabels, +): string { return observations.map(observation => { - const fallback = observation.fallbackReason ? `, reason=${observation.fallbackReason}` : ''; - return `\`${escapeTable(observation.requestedLocale)} -> ${escapeTable(observation.resolvedLocale)} (${observation.source}, descriptors=${observation.descriptorCount}${fallback})\``; + const fallback = observation.fallbackReason ? `, ${labels.reason}=${observation.fallbackReason}` : ''; + return `\`${escapeTable(observation.requestedLocale)} -> ${escapeTable(observation.resolvedLocale)} (${observation.source}, ${labels.descriptors}=${observation.descriptorCount}${fallback})\``; }).join('
'); } diff --git a/src/application/policies/deployment_message_catalog.ts b/src/application/policies/deployment_message_catalog.ts new file mode 100644 index 000000000..74d23b811 --- /dev/null +++ b/src/application/policies/deployment_message_catalog.ts @@ -0,0 +1,299 @@ +import type { AgentConfiguration } from '../../domain/agent'; +import { + DEPLOYMENT_PHASES, + type DeploymentPhase, +} from '../../domain/deployment_operation'; +import { + MESSAGE_CATALOG_VERSION, + type CatalogMessage, + type MessageCatalogDefinition, +} from '../../domain/message_catalog'; +import type { MessageCatalogResolutionPort } from '../ports/message_catalog_ports'; +import { + resolveMessageCatalogView, + resolveStaticMessageCatalogView, + type ResolvedMessageCatalogView, +} from './resolved_message_catalog_policy'; + +const SIMPLE_MESSAGE_KEYS = Object.freeze([ + 'release', 'hotfix', 'currentStatus', 'noAction', 'actionRequired', 'progress', + 'currentTransition', 'whatNext', 'links', 'technical', 'alreadyPublished', + 'notPublished', 'productionUpdated', 'developmentSynchronized', 'yes', 'no', + 'from', 'to', 'state', 'compare', 'controlCenter', 'purpose', 'afterMerge', + 'purposePromotion', 'promotionOverview', + 'reconciliationOverview', 'afterPromotion', 'noRepublish', 'origin', + 'preparedSource', 'destination', 'publication', 'productionFact', + 'developmentTarget', 'completionEffect', 'closeIssue', 'keepIssue', + 'readyBeforeReview', 'buildValidation', 'packageSmoke', 'protectedChecks', + 'syncReason', 'protectedFacts', 'cut', 'promotion', 'reconciliation', 'cleanup', + 'jobSummary', 'result', 'externalWait', 'workflowFailure', 'previousPhase', + 'resultingPhase', 'retryable', 'retryAfterCorrection', 'manualIntervention', + 'reviewManagedPr', 'fallback', 'operation', 'strategy', 'pullRequestMode', + 'sourceSha', 'productionSha', 'pending', 'publicationReceipt', 'absent', + 'localization', 'property', 'value', 'repositoryLocaleLabel', + 'issueLocaleLabel', 'pullRequestLocaleLabel', 'catalogResolution', + 'descriptors', 'reason', +] as const); + +const DIAGRAM_KEYS = Object.freeze([ + 'source', 'prepared', 'production', 'accepted', 'publication', + 'reconciliation', 'complete', +] as const); + +const TEMPLATE_MESSAGE_IDS = Object.freeze([ + 'deployment.template.promotionTitle', + 'deployment.template.reconciliationTitle', + 'deployment.template.promotionTechnical', + 'deployment.template.reconciliationTechnical', + 'deployment.template.jobState', + 'deployment.template.admissionScope', + 'deployment.template.sourceBranchLink', + 'deployment.template.originCommitLink', + 'deployment.template.preparedCommitLink', + 'deployment.template.promotionPullRequestLink', + 'deployment.template.reconciliationPullRequestLink', + 'deployment.template.productionCommitLink', + 'deployment.template.releaseLink', + 'deployment.template.actionTagLink', + 'deployment.template.npmLink', + 'deployment.template.workflowRunLink', + 'deployment.template.nextPromotion', + 'deployment.milestone.promotionMerged', + 'deployment.milestone.publicationComplete', + 'deployment.milestone.reconciliationBlocked', + 'deployment.milestone.complete', +] as const); + +type SimpleMessageKey = typeof SIMPLE_MESSAGE_KEYS[number]; +type DiagramKey = typeof DIAGRAM_KEYS[number]; +type SimpleMessageId = `deployment.${SimpleMessageKey}`; +type PhaseMessageId = `deployment.phase.${DeploymentPhase}`; +type DiagramMessageId = `deployment.diagram.${DiagramKey}`; +type TemplateMessageId = typeof TEMPLATE_MESSAGE_IDS[number]; + +export type DeploymentMessageId = + | SimpleMessageId + | PhaseMessageId + | DiagramMessageId + | TemplateMessageId; +export type DeploymentMessageCatalog = ResolvedMessageCatalogView; + +export interface DeploymentCopy extends Readonly> { + readonly phase: Readonly>; + readonly diagram: Readonly>; +} + +export const DEPLOYMENT_MESSAGE_IDS: readonly DeploymentMessageId[] = Object.freeze([ + ...SIMPLE_MESSAGE_KEYS.map(key => `deployment.${key}` as const), + ...DEPLOYMENT_PHASES.map(phase => `deployment.phase.${phase}` as const), + ...DIAGRAM_KEYS.map(key => `deployment.diagram.${key}` as const), + ...TEMPLATE_MESSAGE_IDS, +]); + +const ENGLISH_SIMPLE: Readonly> = Object.freeze({ + release: 'Release', hotfix: 'Hotfix', currentStatus: 'Current status', + noAction: 'No action is required while GitHub owns the pending transition.', actionRequired: 'Action required', + progress: 'Progress', currentTransition: 'Current transition', whatNext: 'What happens next', links: 'Links', + technical: 'Technical details', alreadyPublished: 'Package status: already published', notPublished: 'Package status: not published', + productionUpdated: 'Production updated', developmentSynchronized: 'Development synchronized', yes: 'Yes', no: 'No', + from: 'From', to: 'To', state: 'State', compare: 'Compare changes', controlCenter: 'Release control center', + purpose: 'Purpose', afterMerge: 'After merge', purposePromotion: 'accept the prepared change in production', + promotionOverview: 'Production promotion', reconciliationOverview: 'Development reconciliation', + afterPromotion: 'After merge, Copilot will tag and publish the accepted production commit.', + noRepublish: 'Merging or closing this PR cannot publish the package again.', + origin: 'Origin', preparedSource: 'Prepared source', destination: 'Destination', publication: 'Publication', + productionFact: 'Production fact', developmentTarget: 'Development target', completionEffect: 'Completion effect', + closeIssue: 'Close issue after all targets', keepIssue: 'Keep issue open', readyBeforeReview: 'Ready before review', + buildValidation: 'Build and release validation', packageSmoke: 'Package smoke test', + protectedChecks: 'Protected-branch checks and reviews', + syncReason: 'A dedicated sync branch preserves target-only commits and isolates target-dependent checks.', + protectedFacts: 'What Copilot protected', cut: 'Source cut', promotion: 'Production promotion', + reconciliation: 'Development reconciliation', cleanup: 'Cleanup and issue completion', + jobSummary: 'Deployment orchestration', result: 'Result', externalWait: 'Waiting externally', + workflowFailure: 'Workflow failed', previousPhase: 'Previous phase', resultingPhase: 'Resulting phase', + retryable: 'Retryable', retryAfterCorrection: 'Retry after correcting the cause', + manualIntervention: 'Manual intervention is required', + reviewManagedPr: 'review and merge the managed PR when GitHub reports it ready', + fallback: 'prepared -> production PR -> accepted -> published -> reconciled -> complete', + operation: 'Operation', strategy: 'Strategy', pullRequestMode: 'PR mode', sourceSha: 'Source SHA', + productionSha: 'Production SHA', pending: 'pending', publicationReceipt: 'Publication receipt', absent: 'absent', + localization: 'Localization', property: 'Property', value: 'Value', + repositoryLocaleLabel: 'Repository locale', issueLocaleLabel: 'Issue locale', + pullRequestLocaleLabel: 'Pull-request locale', catalogResolution: 'Catalog resolution', + descriptors: 'descriptors', reason: 'reason', +}); + +const SPANISH_SIMPLE: Readonly> = Object.freeze({ + release: 'Release', hotfix: 'Hotfix', currentStatus: 'Estado actual', + noAction: 'No se requiere ninguna acción mientras GitHub gestiona la transición pendiente.', actionRequired: 'Acción necesaria', + progress: 'Progreso', currentTransition: 'Transición actual', whatNext: 'Qué ocurrirá después', links: 'Enlaces', + technical: 'Detalles técnicos', alreadyPublished: 'Estado del paquete: ya publicado', notPublished: 'Estado del paquete: no publicado', + productionUpdated: 'Producción actualizada', developmentSynchronized: 'Desarrollo sincronizado', yes: 'Sí', no: 'No', + from: 'Origen', to: 'Destino', state: 'Estado', compare: 'Comparar cambios', controlCenter: 'Centro de control de la release', + purpose: 'Propósito', afterMerge: 'Después del merge', purposePromotion: 'aceptar en producción el cambio preparado', + promotionOverview: 'Promoción a producción', reconciliationOverview: 'Reconciliación con desarrollo', + afterPromotion: 'Tras el merge, Copilot etiquetará y publicará el commit aceptado en producción.', + noRepublish: 'Mergear o cerrar esta PR no puede volver a publicar el paquete.', + origin: 'Origen', preparedSource: 'Fuente preparada', destination: 'Destino', publication: 'Publicación', + productionFact: 'Estado de producción', developmentTarget: 'Destino de desarrollo', completionEffect: 'Efecto al completar', + closeIssue: 'Cerrar la issue tras todos los destinos', keepIssue: 'Mantener la issue abierta', readyBeforeReview: 'Listo antes de revisar', + buildValidation: 'Build y validación de release', packageSmoke: 'Smoke test del paquete', + protectedChecks: 'Checks y revisiones de la rama protegida', + syncReason: 'Una rama de sincronización dedicada preserva los commits exclusivos del destino y aísla sus checks.', + protectedFacts: 'Qué ha protegido Copilot', cut: 'Corte de la fuente', promotion: 'Promoción a producción', + reconciliation: 'Reconciliación con desarrollo', cleanup: 'Limpieza y cierre de la issue', + jobSummary: 'Orquestación del despliegue', result: 'Resultado', externalWait: 'Esperando fuera del workflow', + workflowFailure: 'Workflow fallido', previousPhase: 'Fase anterior', resultingPhase: 'Fase resultante', + retryable: 'Reintentable', retryAfterCorrection: 'Vuelve a intentarlo después de corregir la causa', + manualIntervention: 'Se requiere intervención manual', + reviewManagedPr: 'revisa y mergea la PR gestionada cuando GitHub indique que está lista', + fallback: 'preparada -> PR de producción -> aceptada -> publicada -> reconciliada -> completada', + operation: 'Operación', strategy: 'Estrategia', pullRequestMode: 'Modo de PR', sourceSha: 'SHA de origen', + productionSha: 'SHA de producción', pending: 'pendiente', publicationReceipt: 'Recibo de publicación', absent: 'ausente', + localization: 'Localización', property: 'Propiedad', value: 'Valor', + repositoryLocaleLabel: 'Locale del repositorio', issueLocaleLabel: 'Locale de la issue', + pullRequestLocaleLabel: 'Locale de la pull request', catalogResolution: 'Resolución del catálogo', + descriptors: 'descriptores', reason: 'motivo', +}); + +const ENGLISH_PHASES: Readonly> = Object.freeze({ + preparing: 'preparing the version', promotion_pr_pending: 'waiting for production approval', + promoted: 'accepted in production', publishing: 'publishing artifacts', + published: 'published; preparing development reconciliation', + reconciliation_pending: 'waiting for development reconciliation', completed: 'completed', blocked: 'needs attention', +}); + +const SPANISH_PHASES: Readonly> = Object.freeze({ + preparing: 'preparando la versión', promotion_pr_pending: 'esperando aprobación en producción', + promoted: 'aceptada en producción', publishing: 'publicando artefactos', + published: 'publicada; preparando la reconciliación', + reconciliation_pending: 'esperando reconciliación con desarrollo', completed: 'completada', blocked: 'necesita atención', +}); + +const ENGLISH_DIAGRAM: Readonly> = Object.freeze({ + source: 'Source snapshot', prepared: 'Version prepared', production: 'Production PR', + accepted: 'Accepted in production', publication: 'Package and release', + reconciliation: 'Development reconciliation', complete: 'Complete', +}); + +const SPANISH_DIAGRAM: Readonly> = Object.freeze({ + source: 'Snapshot de origen', prepared: 'Versión preparada', production: 'PR de producción', + accepted: 'Aceptada en producción', publication: 'Paquete y release', + reconciliation: 'Reconciliación con desarrollo', complete: 'Completada', +}); + +const ENGLISH_TEMPLATES: Readonly> = Object.freeze({ + 'deployment.template.promotionTitle': '{kind}({version}): promote to {branch}', + 'deployment.template.reconciliationTitle': '{kind}({version}): reconcile {source} into {target}', + 'deployment.template.promotionTechnical': 'Operation {operation}; strategy {strategy}; merge mode {mergeMode}.', + 'deployment.template.reconciliationTechnical': 'Operation {operation}; source SHA {sourceSha}.', + 'deployment.template.jobState': 'State: version {version}, revision {revision}', + 'deployment.template.admissionScope': 'Admission scope: repository + launcher issue #{issue}', + 'deployment.template.sourceBranchLink': '{branch} branch', + 'deployment.template.originCommitLink': '{commit} origin commit', + 'deployment.template.preparedCommitLink': '{commit} prepared commit', + 'deployment.template.promotionPullRequestLink': 'Promotion PR #{number}', + 'deployment.template.reconciliationPullRequestLink': 'Reconciliation PR #{number}', + 'deployment.template.productionCommitLink': '{commit} production commit', + 'deployment.template.releaseLink': '{tag} GitHub Release', + 'deployment.template.actionTagLink': '{tag} Action tag', + 'deployment.template.npmLink': '{package} version {version} on npm', + 'deployment.template.workflowRunLink': 'Workflow run', + 'deployment.template.nextPromotion': '{source} is prepared for promotion to {production}.', + 'deployment.milestone.promotionMerged': '✅ Promotion PR #{number} merged. Publication is starting from production SHA {productionSha}.', + 'deployment.milestone.publicationComplete': '📦 {tag} is published from accepted production SHA {productionSha}.', + 'deployment.milestone.reconciliationBlocked': '❌ Deployment blocked: {reason}', + 'deployment.milestone.complete': '✅ Deployment {tag} and every configured reconciliation target are complete.', +}); + +const SPANISH_TEMPLATES: Readonly> = Object.freeze({ + 'deployment.template.promotionTitle': '{kind}({version}): promover a {branch}', + 'deployment.template.reconciliationTitle': '{kind}({version}): reconciliar {source} con {target}', + 'deployment.template.promotionTechnical': 'Operación {operation}; estrategia {strategy}; modo de merge {mergeMode}.', + 'deployment.template.reconciliationTechnical': 'Operación {operation}; SHA de origen {sourceSha}.', + 'deployment.template.jobState': 'Estado: versión {version}, revisión {revision}', + 'deployment.template.admissionScope': 'Ámbito de admisión: repositorio + issue de lanzamiento #{issue}', + 'deployment.template.sourceBranchLink': 'Rama {branch}', + 'deployment.template.originCommitLink': 'Commit de origen {commit}', + 'deployment.template.preparedCommitLink': 'Commit preparado {commit}', + 'deployment.template.promotionPullRequestLink': 'PR de promoción #{number}', + 'deployment.template.reconciliationPullRequestLink': 'PR de reconciliación #{number}', + 'deployment.template.productionCommitLink': 'Commit de producción {commit}', + 'deployment.template.releaseLink': 'Release de GitHub {tag}', + 'deployment.template.actionTagLink': 'Tag de Action {tag}', + 'deployment.template.npmLink': '{package}, versión {version}, en npm', + 'deployment.template.workflowRunLink': 'Ejecución del workflow', + 'deployment.template.nextPromotion': '{source} está preparada para promoverse a {production}.', + 'deployment.milestone.promotionMerged': '✅ La PR de promoción #{number} se ha mergeado. La publicación comienza desde el SHA de producción {productionSha}.', + 'deployment.milestone.publicationComplete': '📦 {tag} se ha publicado desde el SHA de producción aceptado {productionSha}.', + 'deployment.milestone.reconciliationBlocked': '❌ Despliegue bloqueado: {reason}', + 'deployment.milestone.complete': '✅ El despliegue {tag} y todos los destinos de reconciliación configurados se han completado.', +}); + +function catalogMessages( + simple: Readonly>, + phases: Readonly>, + diagram: Readonly>, + templates: Readonly>, +): Readonly> { + return Object.freeze({ + ...Object.fromEntries(SIMPLE_MESSAGE_KEYS.map(key => [`deployment.${key}`, simple[key]])), + ...Object.fromEntries(DEPLOYMENT_PHASES.map(phase => [`deployment.phase.${phase}`, phases[phase]])), + ...Object.fromEntries(DIAGRAM_KEYS.map(key => [`deployment.diagram.${key}`, diagram[key]])), + ...templates, + }) as Readonly>; +} + +export const ENGLISH_DEPLOYMENT_DEFINITION: MessageCatalogDefinition = Object.freeze({ + version: MESSAGE_CATALOG_VERSION, + locale: 'en-US', + compatibleBaseLanguage: 'en', + messages: catalogMessages(ENGLISH_SIMPLE, ENGLISH_PHASES, ENGLISH_DIAGRAM, ENGLISH_TEMPLATES), +}); + +export const SPANISH_DEPLOYMENT_DEFINITION: MessageCatalogDefinition = Object.freeze({ + version: MESSAGE_CATALOG_VERSION, + locale: 'es-ES', + compatibleBaseLanguage: 'es', + messages: catalogMessages(SPANISH_SIMPLE, SPANISH_PHASES, SPANISH_DIAGRAM, SPANISH_TEMPLATES), +}); + +export const DEPLOYMENT_CATALOG_DEFINITIONS = Object.freeze([ + ENGLISH_DEPLOYMENT_DEFINITION, + SPANISH_DEPLOYMENT_DEFINITION, +]); + +export function resolveStaticDeploymentCatalog(locale: string): DeploymentMessageCatalog { + return resolveStaticMessageCatalogView( + locale, + ENGLISH_DEPLOYMENT_DEFINITION, + DEPLOYMENT_CATALOG_DEFINITIONS, + ); +} + +export function resolveDeploymentCatalog( + locale: string, + configuration: Readonly | undefined, + resolver: MessageCatalogResolutionPort | undefined, +): Promise { + return resolveMessageCatalogView( + locale, + DEPLOYMENT_MESSAGE_IDS, + ENGLISH_DEPLOYMENT_DEFINITION, + DEPLOYMENT_CATALOG_DEFINITIONS, + configuration, + resolver, + ); +} + +export function deploymentCopy(catalog: DeploymentMessageCatalog): DeploymentCopy { + return Object.freeze({ + ...Object.fromEntries(SIMPLE_MESSAGE_KEYS.map(key => [key, catalog.message(`deployment.${key}`)])), + phase: Object.freeze(Object.fromEntries( + DEPLOYMENT_PHASES.map(phase => [phase, catalog.message(`deployment.phase.${phase}`)]), + )) as Readonly>, + diagram: Object.freeze(Object.fromEntries( + DIAGRAM_KEYS.map(key => [key, catalog.message(`deployment.diagram.${key}`)]), + )) as Readonly>, + }) as DeploymentCopy; +} diff --git a/src/application/policies/deployment_plan_policy.ts b/src/application/policies/deployment_plan_policy.ts index 4cdfe05df..4829bcf17 100644 --- a/src/application/policies/deployment_plan_policy.ts +++ b/src/application/policies/deployment_plan_policy.ts @@ -11,9 +11,11 @@ import type { MergeQueueReadiness, } from "../../domain/merge_queue_readiness"; import { redactSensitiveText } from "../../domain/security/sensitive_text"; +import type { LocaleProfile } from '../../domain/locale'; export interface InitialDeploymentOperationInput { readonly operationId: string; + readonly locale: LocaleProfile; readonly kind: DeploymentKind; readonly version: string; readonly title: string; @@ -38,6 +40,7 @@ export function buildInitialDeploymentOperation( stateVersion: DEPLOYMENT_STATE_VERSION, revision: 0, operationId: input.operationId, + locale: Object.freeze({ ...input.locale }), kind: input.kind, version: input.version, title: input.title, diff --git a/src/application/policies/deployment_presentation_policy.ts b/src/application/policies/deployment_presentation_policy.ts index 742c7f250..b2abc676e 100644 --- a/src/application/policies/deployment_presentation_policy.ts +++ b/src/application/policies/deployment_presentation_policy.ts @@ -1,151 +1,47 @@ -import type { DeploymentOperationSnapshot, DeploymentPhase, ReconciliationTargetState } from "../../domain/deployment_operation"; -import { buildManagedPullRequestMarker } from "../../domain/managed_pull_request"; +import type { + DeploymentOperationSnapshot, + DeploymentPhase, + ReconciliationTargetState, +} from '../../domain/deployment_operation'; +import { buildManagedPullRequestMarker } from '../../domain/managed_pull_request'; +import { + deploymentCopy, + resolveStaticDeploymentCatalog, + type DeploymentCopy, + type DeploymentMessageCatalog, +} from './deployment_message_catalog'; import { buildPublicationMarker, createSemanticDigest } from './publication_identity_policy'; -type SupportedLocale = "en-US" | "es-ES"; - -interface DeploymentMessages { - release: string; - hotfix: string; - currentStatus: string; - noAction: string; - actionRequired: string; - progress: string; - currentTransition: string; - whatNext: string; - links: string; - technical: string; - alreadyPublished: string; - notPublished: string; - productionUpdated: string; - developmentSynchronized: string; - yes: string; - no: string; - from: string; - to: string; - state: string; - compare: string; - controlCenter: string; - purpose: string; - afterMerge: string; - purposePromotion: string; - purposeReconciliation: string; - afterPromotion: string; - noRepublish: string; - origin: string; - preparedSource: string; - destination: string; - publication: string; - productionFact: string; - developmentTarget: string; - completionEffect: string; - closeIssue: string; - keepIssue: string; - readyBeforeReview: string; - buildValidation: string; - packageSmoke: string; - protectedChecks: string; - syncReason: string; - protectedFacts: string; - cut: string; - promotion: string; - reconciliation: string; - cleanup: string; - jobSummary: string; - result: string; - externalWait: string; - workflowFailure: string; - previousPhase: string; - resultingPhase: string; - retryable: string; - retryAfterCorrection: string; - manualIntervention: string; - reviewManagedPr: string; - createdReused: string; - fallback: string; - phase: Readonly>; - diagram: readonly [string, string, string, string, string, string, string]; -} - -const EN: DeploymentMessages = { - release: "Release", hotfix: "Hotfix", currentStatus: "Current status", - noAction: "No action is required while GitHub owns the pending transition.", actionRequired: "Action required", - progress: "Progress", currentTransition: "Current transition", whatNext: "What happens next", links: "Links", - technical: "Technical details", alreadyPublished: "Package status: already published", notPublished: "Package status: not published", - productionUpdated: "Production updated", developmentSynchronized: "Development synchronized", yes: "Yes", no: "No", - from: "From", to: "To", state: "State", compare: "Compare changes", controlCenter: "Release control center", - purpose: "Purpose", afterMerge: "After merge", purposePromotion: "accept the prepared change in production", - purposeReconciliation: "bring the accepted production state back to the development line", - afterPromotion: "After merge, Copilot will tag and publish the accepted production commit.", - noRepublish: "Merging or closing this PR cannot publish the package again.", - origin: "Origin", preparedSource: "Prepared source", destination: "Destination", publication: "Publication", - productionFact: "Production fact", developmentTarget: "Development target", completionEffect: "Completion effect", - closeIssue: "Close issue after all targets", keepIssue: "Keep issue open", readyBeforeReview: "Ready before review", - buildValidation: "Build and release validation", packageSmoke: "Package smoke test", - protectedChecks: "Protected-branch checks and reviews", syncReason: "A dedicated sync branch preserves target-only commits and isolates target-dependent checks.", - protectedFacts: "What Copilot protected", cut: "Source cut", promotion: "Production promotion", reconciliation: "Development reconciliation", - cleanup: "Cleanup and issue completion", jobSummary: "Deployment orchestration", result: "Result", - externalWait: "Waiting externally", workflowFailure: "Workflow failed", previousPhase: "Previous phase", resultingPhase: "Resulting phase", - retryable: "Retryable", createdReused: "Created, reused, or skipped", fallback: "prepared -> production PR -> accepted -> published -> reconciled -> complete", - retryAfterCorrection: "Retry after correcting the cause", manualIntervention: "Manual intervention is required", - reviewManagedPr: "review and merge the managed PR when GitHub reports it ready", - phase: { - preparing: "preparing the version", promotion_pr_pending: "waiting for production approval", promoted: "accepted in production", - publishing: "publishing artifacts", published: "published; preparing development reconciliation", - reconciliation_pending: "waiting for development reconciliation", completed: "completed", blocked: "needs attention", - }, - diagram: ["Source snapshot", "Version prepared", "Production PR", "Accepted in production", "Package and release", "Development reconciliation", "Complete"], -}; - -const ES: DeploymentMessages = { - release: "Release", hotfix: "Hotfix", currentStatus: "Estado actual", - noAction: "No se requiere ninguna acción mientras GitHub gestiona la transición pendiente.", actionRequired: "Acción necesaria", - progress: "Progreso", currentTransition: "Transición actual", whatNext: "Qué ocurrirá después", links: "Enlaces", - technical: "Detalles técnicos", alreadyPublished: "Estado del paquete: ya publicado", notPublished: "Estado del paquete: no publicado", - productionUpdated: "Producción actualizada", developmentSynchronized: "Desarrollo sincronizado", yes: "Sí", no: "No", - from: "Origen", to: "Destino", state: "Estado", compare: "Comparar cambios", controlCenter: "Centro de control de la release", - purpose: "Propósito", afterMerge: "Después del merge", purposePromotion: "aceptar en producción el cambio preparado", - purposeReconciliation: "llevar el estado aceptado en producción de vuelta a desarrollo", - afterPromotion: "Tras el merge, Copilot etiquetará y publicará el commit aceptado en producción.", - noRepublish: "Mergear o cerrar esta PR no puede volver a publicar el paquete.", - origin: "Origen", preparedSource: "Fuente preparada", destination: "Destino", publication: "Publicación", - productionFact: "Estado de producción", developmentTarget: "Destino de desarrollo", completionEffect: "Efecto al completar", - closeIssue: "Cerrar la issue tras todos los destinos", keepIssue: "Mantener la issue abierta", readyBeforeReview: "Listo antes de revisar", - buildValidation: "Build y validación de release", packageSmoke: "Smoke test del paquete", - protectedChecks: "Checks y revisiones de la rama protegida", syncReason: "Una rama de sincronización dedicada preserva los commits exclusivos del destino y aísla sus checks.", - protectedFacts: "Qué ha protegido Copilot", cut: "Corte de la fuente", promotion: "Promoción a producción", reconciliation: "Reconciliación con desarrollo", - cleanup: "Limpieza y cierre de la issue", jobSummary: "Orquestación del despliegue", result: "Resultado", - externalWait: "Esperando fuera del workflow", workflowFailure: "Workflow fallido", previousPhase: "Fase anterior", resultingPhase: "Fase resultante", - retryable: "Reintentable", createdReused: "Creado, reutilizado u omitido", fallback: "preparada -> PR de producción -> aceptada -> publicada -> reconciliada -> completada", - retryAfterCorrection: "Vuelve a intentarlo después de corregir la causa", manualIntervention: "Se requiere intervención manual", - reviewManagedPr: "revisa y mergea la PR gestionada cuando GitHub indique que está lista", - phase: { - preparing: "preparando la versión", promotion_pr_pending: "esperando aprobación en producción", promoted: "aceptada en producción", - publishing: "publicando artefactos", published: "publicada; preparando la reconciliación", - reconciliation_pending: "esperando reconciliación con desarrollo", completed: "completada", blocked: "necesita atención", - }, - diagram: ["Snapshot de origen", "Versión preparada", "PR de producción", "Aceptada en producción", "Paquete y release", "Reconciliación con desarrollo", "Completada"], -}; - -export const DEPLOYMENT_DASHBOARD_MARKER = "copilot-deployment-dashboard"; +export const DEPLOYMENT_DASHBOARD_MARKER = 'copilot-deployment-dashboard'; export interface DeploymentPresentationContext { readonly owner: string; readonly repository: string; readonly issue: number; + readonly repositoryLocale: string; readonly issueLocale: string; readonly pullRequestLocale: string; readonly workflowRunUrl?: string; readonly packageName?: string; } +export type DeploymentMilestone = + | { readonly kind: 'promotion-merged'; readonly pullRequest: number; readonly productionSha: string } + | { readonly kind: 'publication-complete'; readonly tag: string; readonly productionSha: string } + | { readonly kind: 'reconciliation-blocked'; readonly reason: string } + | { readonly kind: 'orchestration-complete'; readonly tag: string }; + export function deploymentDashboardMarker(operationId: string, issue: number): string { return ``; } -export function renderDeploymentDashboard(operation: DeploymentOperationSnapshot, context: DeploymentPresentationContext): string { - const messages = messagesFor(context.issueLocale); - const title = operation.kind === "release" ? messages.release : messages.hotfix; +export function renderDeploymentDashboard( + operation: DeploymentOperationSnapshot, + context: DeploymentPresentationContext, + catalog: DeploymentMessageCatalog = resolveStaticDeploymentCatalog(context.issueLocale), +): string { + const messages = deploymentCopy(catalog); + const title = operation.kind === 'release' ? messages.release : messages.hotfix; const action = deploymentAction(operation, messages); const lines = [ buildPublicationMarker({ @@ -155,58 +51,80 @@ export function renderDeploymentDashboard(operation: DeploymentOperationSnapshot key: `operation:${createSemanticDigest(operation.operationId)}`, }, sourceVersion: `revision:${operation.revision}`, - digest: createSemanticDigest({ locale: context.issueLocale, operation }), + digest: createSemanticDigest({ + requestedLocale: catalog.requestedLocale, + resolvedLocale: catalog.locale, + operation, + }), }), - deploymentDashboardMarker(operation.operationId, context.issue), "", - `# ${operation.phase === "blocked" ? "❌" : operation.phase === "completed" ? "✅" : "🚀"} ${title} ${inline(operation.version)}`, "", + deploymentDashboardMarker(operation.operationId, context.issue), '', + `## ${operation.phase === 'blocked' ? '❌' : operation.phase === 'completed' ? '✅' : '🚀'} ${title} ${inline(operation.version)}`, '', `> **${messages.currentStatus}: ${messages.phase[operation.phase]}.**`, ]; - if (action.required) lines.push("", `## ${messages.actionRequired}`, "", action.message); + if (action.required) lines.push('', `### ${messages.actionRequired}`, '', action.message); else lines.push(`> ${action.message}`); - lines.push(""); + lines.push(''); - if (operation.phase === "blocked") lines.push(...factTable(operation, messages), "", `## ${messages.protectedFacts}`, "", protectedFact(operation, messages), ""); - if (operation.presentationMode !== "quiet") lines.push(`## ${messages.progress}`, "", ...progressLines(operation, messages), ""); - if (operation.presentationMode === "guided" && operation.diagrams) lines.push(...deploymentDiagram(messages), ""); - if (operation.presentationMode !== "quiet") { - lines.push( - `## ${messages.currentTransition}`, "", ...transitionTable(operation, messages), "", - `## ${messages.whatNext}`, "", nextDescription(operation, messages), "", - `## ${messages.links}`, "", deploymentLinks(operation, context, messages).join(" · "), "", - ); + if (operation.phase === 'blocked') { + lines.push(...factTable(operation, messages), '', `### ${messages.protectedFacts}`, '', protectedFact(operation, messages), ''); + } + if (operation.presentationMode !== 'quiet') { + lines.push(`### ${messages.progress}`, '', ...progressLines(operation, messages), ''); + } + if (operation.presentationMode === 'guided' && operation.diagrams) { + lines.push(...deploymentDiagram(messages), ''); + } + if (operation.presentationMode !== 'quiet') { + lines.push(`### ${messages.currentTransition}`, '', ...transitionTable(operation, messages), ''); + if (operation.phase !== 'blocked') { + lines.push(`### ${messages.whatNext}`, '', nextDescription(operation, messages, catalog), ''); + } + lines.push(`### ${messages.links}`, '', deploymentLinks(operation, context, messages, catalog).join(' · '), ''); } lines.push( - "
", `${messages.technical}`, "", - `- Operation: ${inline(operation.operationId)}`, `- Strategy: ${inline(operation.strategy)}`, - `- PR mode: ${inline(operation.selectedPrMode ?? operation.prMode)}`, `- Source SHA: ${inline(operation.sourceSha)}`, - `- Production SHA: ${inline(operation.productionSha ?? "pending")}`, "
", + '
', `${messages.technical}`, '', + `- ${messages.operation}: ${inline(operation.operationId)}`, + `- ${messages.strategy}: ${inline(operation.strategy)}`, + `- ${messages.pullRequestMode}: ${inline(operation.selectedPrMode ?? operation.prMode)}`, + `- ${messages.sourceSha}: ${inline(operation.sourceSha)}`, + `- ${messages.productionSha}: ${inline(operation.productionSha ?? messages.pending)}`, + '
', ); - return lines.join("\n"); + return lines.join('\n'); } export function renderPromotionPullRequest( operation: DeploymentOperationSnapshot, context: DeploymentPresentationContext, + catalog: DeploymentMessageCatalog = resolveStaticDeploymentCatalog(context.pullRequestLocale), ): { title: string; body: string } { - const messages = messagesFor(context.pullRequestLocale); - const kind = operation.kind === "release" ? "release" : "hotfix"; - const title = `${kind}(${safeText(operation.version)}): promote to ${safeText(operation.productionBranch)}`; + const messages = deploymentCopy(catalog); + const kind = operation.kind; + const title = catalog.message('deployment.template.promotionTitle', { + kind, + version: safeText(operation.version), + branch: safeText(operation.productionBranch), + }).slice(0, 240); const body = [ - `# 🚀 ${capitalize(messages.purposePromotion)}`, "", - `> **${messages.purpose}:** ${capitalize(messages.purposePromotion)}.`, - `> **${messages.afterMerge}:** ${messages.afterPromotion}`, "", + `## 🚀 ${messages.promotionOverview}`, '', + `> **${messages.purpose}:** ${messages.purposePromotion}.`, '', `| ${messages.origin} | ${messages.preparedSource} | ${messages.destination} | ${messages.publication} |`, - "|---|---|---|---|", - `| ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)} | ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)} | ${inline(operation.productionBranch)} | ${messages.afterMerge} |`, "", - `## ${messages.readyBeforeReview}`, "", - `- ✅ ${messages.buildValidation}`, `- ✅ ${messages.packageSmoke}`, `- ⏳ ${messages.protectedChecks}`, "", - `## ${messages.afterMerge}`, "", - `- ${messages.afterPromotion}`, `- ${messages.reconciliation}: ${inline(operation.developmentBranch)}.`, "", - `[${messages.compare}](${compareUrl(context, operation.productionBranch, operation.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, "", - "
", `${messages.technical}`, "", - `Operation ${inline(operation.operationId)}; strategy ${inline(operation.strategy)}; merge mode ${inline(operation.prMode)}.`, - "
", "", buildManagedPullRequestMarker({ operationId: operation.operationId, phase: "promotion", issue: context.issue }), - ].join("\n"); + '|---|---|---|---|', + `| ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)} | ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)} | ${inline(operation.productionBranch)} | ${messages.notPublished} |`, '', + `### ${messages.readyBeforeReview}`, '', + `- ✅ ${messages.buildValidation}`, `- ✅ ${messages.packageSmoke}`, `- ⏳ ${messages.protectedChecks}`, '', + `### ${messages.afterMerge}`, '', + `- ${messages.afterPromotion}`, `- ${messages.reconciliation}: ${inline(operation.developmentBranch)}.`, '', + `[${messages.compare}](${compareUrl(context, operation.productionBranch, operation.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, '', + '
', `${messages.technical}`, '', + catalog.message('deployment.template.promotionTechnical', { + operation: inline(operation.operationId), + strategy: inline(operation.strategy), + mergeMode: inline(operation.prMode), + }), + '
', '', + buildManagedPullRequestMarker({ operationId: operation.operationId, phase: 'promotion', issue: context.issue }), + ].join('\n'); return { title, body }; } @@ -214,21 +132,31 @@ export function renderReconciliationPullRequest( operation: DeploymentOperationSnapshot, target: ReconciliationTargetState, context: DeploymentPresentationContext, + catalog: DeploymentMessageCatalog = resolveStaticDeploymentCatalog(context.pullRequestLocale), ): { title: string; body: string } { - const messages = messagesFor(context.pullRequestLocale); - const kind = operation.kind === "release" ? "release" : "hotfix"; - const title = `${kind}(${safeText(operation.version)}): reconcile ${safeText(target.sourceBranch)} into ${safeText(target.targetBranch)}`; + const messages = deploymentCopy(catalog); + const kind = operation.kind; + const title = catalog.message('deployment.template.reconciliationTitle', { + kind, + version: safeText(operation.version), + source: safeText(target.sourceBranch), + target: safeText(target.targetBranch), + }).slice(0, 240); const body = [ - `# 🔄 ${capitalize(messages.purposeReconciliation)}`, "", - `> **${messages.alreadyPublished}.** ${messages.noRepublish}`, "", - `| ${messages.productionFact} | ${messages.developmentTarget} | ${messages.completionEffect} |`, "|---|---|---|", - `| ${inline(`${operation.tag}@${shortSha(operation.productionSha ?? target.sourceSha)}`)} | ${inline(target.targetBranch)} | ${operation.issueCompletion === "close" ? messages.closeIssue : messages.keepIssue} |`, "", - ...(target.syncBranch ? [`${messages.syncReason} ${inline(target.syncBranch)}`, ""] : []), - `${messages.noRepublish}`, "", - `[${messages.compare}](${compareUrl(context, target.targetBranch, target.syncBranch ?? target.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, "", - "
", `${messages.technical}`, "", `Operation ${inline(operation.operationId)}; source SHA ${inline(target.sourceSha)}.`, - "
", "", buildManagedPullRequestMarker({ operationId: operation.operationId, phase: "reconciliation", issue: context.issue }), - ].join("\n"); + `## 🔄 ${messages.reconciliationOverview}`, '', + `> **${messages.alreadyPublished}.** ${messages.noRepublish}`, '', + `| ${messages.productionFact} | ${messages.developmentTarget} | ${messages.completionEffect} |`, '|---|---|---|', + `| ${inline(`${operation.tag}@${shortSha(operation.productionSha ?? target.sourceSha)}`)} | ${inline(target.targetBranch)} | ${operation.issueCompletion === 'close' ? messages.closeIssue : messages.keepIssue} |`, '', + ...(target.syncBranch ? [`${messages.syncReason} ${inline(target.syncBranch)}`, ''] : []), + `[${messages.compare}](${compareUrl(context, target.targetBranch, target.syncBranch ?? target.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, '', + '
', `${messages.technical}`, '', + catalog.message('deployment.template.reconciliationTechnical', { + operation: inline(operation.operationId), + sourceSha: inline(target.sourceSha), + }), + '
', '', + buildManagedPullRequestMarker({ operationId: operation.operationId, phase: 'reconciliation', issue: context.issue }), + ].join('\n'); return { title, body }; } @@ -236,94 +164,129 @@ export function renderDeploymentJobSummary( operation: DeploymentOperationSnapshot, context: DeploymentPresentationContext, previousPhase?: DeploymentPhase, - operations: readonly string[] = [], + catalog: DeploymentMessageCatalog = resolveStaticDeploymentCatalog(context.repositoryLocale), ): string { - const messages = messagesFor(context.issueLocale); - const externallyPending = operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending"; - const result = operation.phase === "blocked" ? messages.workflowFailure : externallyPending ? messages.externalWait : messages.phase[operation.phase]; + const messages = deploymentCopy(catalog); + const externallyPending = operation.phase === 'promotion_pr_pending' || operation.phase === 'reconciliation_pending'; + const result = operation.phase === 'blocked' + ? messages.workflowFailure + : externallyPending ? messages.externalWait : messages.phase[operation.phase]; const lines = [ - `# ${operation.phase === "blocked" ? "❌" : externallyPending ? "⏳" : "✅"} ${messages.jobSummary}`, "", - `> **${messages.result}: ${result}.**`, "", - `| ${messages.previousPhase} | ${messages.resultingPhase} | ${messages.retryable} |`, "|---|---|---|", - `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${operation.lastFailure?.retryable ? messages.yes : messages.no} |`, "", - `- Operation: ${inline(operation.operationId)}`, - `- State: version ${operation.stateVersion}, revision ${operation.revision}`, - `- Admission scope: repository + launcher issue #${context.issue}`, + `# ${operation.phase === 'blocked' ? '❌' : externallyPending ? '⏳' : '✅'} ${messages.jobSummary}`, '', + `> **${messages.result}: ${result}.**`, '', + `| ${messages.previousPhase} | ${messages.resultingPhase} | ${messages.retryable} |`, '|---|---|---|', + `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${operation.lastFailure?.retryable ? messages.yes : messages.no} |`, '', + `- ${messages.operation}: ${inline(operation.operationId)}`, + `- ${catalog.message('deployment.template.jobState', { version: operation.stateVersion, revision: operation.revision })}`, + `- ${catalog.message('deployment.template.admissionScope', { issue: context.issue })}`, `- ${messages.origin}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, `- ${messages.preparedSource}: ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)}`, - `- ${messages.productionFact}: ${inline(operation.productionSha ? `${operation.productionBranch}@${shortSha(operation.productionSha)}` : "pending")}`, - `- ${messages.publication}: ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, - `- Publication receipt: ${operation.publicationReceipt ? inline(`${operation.publicationReceipt.tag}@${shortSha(operation.publicationReceipt.productionSha)}`) : "absent"}`, - `- ${messages.createdReused}: ${safeText(operations.join(", ") || "none")}`, "", + `- ${messages.productionFact}: ${inline(operation.productionSha ? `${operation.productionBranch}@${shortSha(operation.productionSha)}` : messages.pending)}`, + `- ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, + `- ${messages.publicationReceipt}: ${operation.publicationReceipt + ? inline(`${operation.publicationReceipt.tag}@${shortSha(operation.publicationReceipt.productionSha)}`) + : messages.absent}`, '', ]; - if (operation.phase === "blocked") { + if (operation.phase === 'blocked') { lines.push( - `## ${messages.actionRequired}`, "", - `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${operation.lastFailure?.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`, "", + `## ${messages.actionRequired}`, '', + `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${operation.lastFailure?.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`, '', ); } - lines.push(deploymentLinks(operation, context, messages).join(" · ")); - return lines.join("\n"); + lines.push(deploymentLinks(operation, context, messages, catalog).join(' · ')); + return lines.join('\n'); } -function progressLines(operation: DeploymentOperationSnapshot, messages: DeploymentMessages): string[] { - const phase = operation.phase === "blocked" ? operation.lastFailure?.previousPhase ?? "preparing" : operation.phase; +export function renderDeploymentMilestone( + milestone: DeploymentMilestone, + catalog: DeploymentMessageCatalog, +): string { + switch (milestone.kind) { + case 'promotion-merged': + return catalog.message('deployment.milestone.promotionMerged', { + number: milestone.pullRequest, + productionSha: inline(milestone.productionSha), + }); + case 'publication-complete': + return catalog.message('deployment.milestone.publicationComplete', { + tag: inline(milestone.tag), + productionSha: inline(milestone.productionSha), + }); + case 'reconciliation-blocked': + return catalog.message('deployment.milestone.reconciliationBlocked', { reason: safeText(milestone.reason) }); + case 'orchestration-complete': + return catalog.message('deployment.milestone.complete', { tag: inline(milestone.tag) }); + } +} + +function progressLines(operation: DeploymentOperationSnapshot, messages: DeploymentCopy): string[] { + const phase = operation.phase === 'blocked' ? operation.lastFailure?.previousPhase ?? 'preparing' : operation.phase; const reached = (expected: DeploymentPhase): boolean => phaseRank(phase) >= phaseRank(expected); return [ `- [x] ${messages.cut}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, `- [x] ${messages.buildValidation} + ${messages.packageSmoke}`, - `- [${operation.productionSha || reached("promoted") ? "x" : " "}] ${messages.promotion}: ${inline(operation.productionBranch)}`, - `- [${operation.publicationVerified ? "x" : " "}] ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, - `- [${reconciliationCompleted(operation) ? "x" : " "}] ${messages.reconciliation}: ${inline(operation.developmentBranch)}`, - `- [${operation.phase === "completed" ? "x" : " "}] ${messages.cleanup}`, + `- [${operation.productionSha || reached('promoted') ? 'x' : ' '}] ${messages.promotion}: ${inline(operation.productionBranch)}`, + `- [${operation.publicationVerified ? 'x' : ' '}] ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, + `- [${reconciliationCompleted(operation) ? 'x' : ' '}] ${messages.reconciliation}: ${inline(operation.developmentBranch)}`, + `- [${operation.phase === 'completed' ? 'x' : ' '}] ${messages.cleanup}`, ]; } -function deploymentDiagram(messages: DeploymentMessages): string[] { - const [source, prepared, production, accepted, publication, reconciliation, complete] = messages.diagram; +function deploymentDiagram(messages: DeploymentCopy): string[] { + const { source, prepared, production, accepted, publication, reconciliation, complete } = messages.diagram; return [ - "```mermaid", "flowchart LR", ` D[${source}] --> R[${prepared}]`, ` R --> P[${production}]`, - ` P --> A[${accepted}]`, ` A --> N[${publication}]`, ` N --> B[${reconciliation}]`, ` B --> C[${complete}]`, "```", "", messages.fallback, + '```mermaid', 'flowchart LR', ` D[${source}] --> R[${prepared}]`, ` R --> P[${production}]`, + ` P --> A[${accepted}]`, ` A --> N[${publication}]`, ` N --> B[${reconciliation}]`, + ` B --> C[${complete}]`, '```', '', messages.fallback, ]; } -function transitionTable(operation: DeploymentOperationSnapshot, messages: DeploymentMessages): string[] { - const activeTarget = operation.reconciliationTargets.find((target) => target.status !== "completed"); +function transitionTable(operation: DeploymentOperationSnapshot, messages: DeploymentCopy): string[] { + const activeTarget = operation.reconciliationTargets.find(target => target.status !== 'completed'); const from = activeTarget?.syncBranch ?? activeTarget?.sourceBranch ?? operation.sourceBranch; const to = activeTarget?.targetBranch ?? operation.productionBranch; - return [`| ${messages.from} | ${messages.to} | ${messages.state} |`, "|---|---|---|", `| ${inline(from)} | ${inline(to)} | ${inline(messages.phase[operation.phase])} |`]; + return [ + `| ${messages.from} | ${messages.to} | ${messages.state} |`, '|---|---|---|', + `| ${inline(from)} | ${inline(to)} | ${safeText(messages.phase[operation.phase])} |`, + ]; } -function factTable(operation: DeploymentOperationSnapshot, messages: DeploymentMessages): string[] { +function factTable(operation: DeploymentOperationSnapshot, messages: DeploymentCopy): string[] { return [ - `| ${messages.productionUpdated} | ${messages.alreadyPublished} | ${messages.developmentSynchronized} |`, "|---|---|---|", + `| ${messages.productionUpdated} | ${messages.publication} | ${messages.developmentSynchronized} |`, '|---|---|---|', `| ${operation.productionSha ? messages.yes : messages.no} | ${operation.publicationVerified ? messages.yes : messages.no} | ${reconciliationCompleted(operation) ? messages.yes : messages.no} |`, ]; } -function protectedFact(operation: DeploymentOperationSnapshot, messages: DeploymentMessages): string { +function protectedFact(operation: DeploymentOperationSnapshot, messages: DeploymentCopy): string { if (operation.publicationVerified) return `${messages.alreadyPublished}; ${messages.noRepublish}`; if (operation.productionSha) return `${messages.productionUpdated}: ${messages.yes}. ${messages.notPublished}.`; return `${messages.productionUpdated}: ${messages.no}. ${messages.notPublished}.`; } -function nextDescription(operation: DeploymentOperationSnapshot, messages: DeploymentMessages): string { - if (operation.phase === "blocked") { - const diagnostic = safeText(operation.lastFailure?.message ?? messages.workflowFailure); - return `${diagnostic}. ${messages.actionRequired}: ${operation.lastFailure?.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`; +function nextDescription( + operation: DeploymentOperationSnapshot, + messages: DeploymentCopy, + catalog: DeploymentMessageCatalog, +): string { + if (operation.phase === 'promotion_pr_pending' || operation.phase === 'publishing' || operation.phase === 'promoted') { + return messages.afterPromotion; + } + if (operation.phase === 'reconciliation_pending' || operation.phase === 'published') return messages.noRepublish; + if (operation.phase === 'completed') { + return `${messages.productionUpdated}: ${messages.yes}. ${messages.developmentSynchronized}: ${messages.yes}.`; } - if (operation.phase === "promotion_pr_pending") return messages.afterPromotion; - if (operation.phase === "publishing" || operation.phase === "promoted") return messages.afterPromotion; - if (operation.phase === "reconciliation_pending" || operation.phase === "published") return messages.noRepublish; - if (operation.phase === "completed") return `${messages.productionUpdated}: ${messages.yes}. ${messages.developmentSynchronized}: ${messages.yes}.`; - return `${messages.promotion}: ${inline(operation.sourceBranch)} -> ${inline(operation.productionBranch)}.`; + return catalog.message('deployment.template.nextPromotion', { + source: inline(operation.sourceBranch), + production: inline(operation.productionBranch), + }); } -function deploymentAction(operation: DeploymentOperationSnapshot, messages: DeploymentMessages): { required: boolean; message: string } { - const manual = (operation.selectedPrMode ?? operation.prMode) === "create-only" - && (operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending"); +function deploymentAction(operation: DeploymentOperationSnapshot, messages: DeploymentCopy): { required: boolean; message: string } { + const manual = (operation.selectedPrMode ?? operation.prMode) === 'create-only' + && (operation.phase === 'promotion_pr_pending' || operation.phase === 'reconciliation_pending'); if (manual) return { required: true, message: `${messages.protectedChecks}: ${messages.reviewManagedPr}.` }; - if (operation.phase !== "blocked") return { required: false, message: messages.noAction }; + if (operation.phase !== 'blocked') return { required: false, message: messages.noAction }; return { required: true, message: operation.lastFailure?.retryable @@ -332,45 +295,131 @@ function deploymentAction(operation: DeploymentOperationSnapshot, messages: Depl }; } -function deploymentLinks(operation: DeploymentOperationSnapshot, context: DeploymentPresentationContext, messages: DeploymentMessages): string[] { +function deploymentLinks( + operation: DeploymentOperationSnapshot, + context: DeploymentPresentationContext, + messages: DeploymentCopy, + catalog: DeploymentMessageCatalog, +): string[] { const links = [`[${messages.controlCenter}](${issueUrl(context)})`]; - links.push(`[${safeText(operation.sourceBranch)} branch](${branchUrl(context, operation.sourceBranch)})`); - links.push(`[${shortSha(operation.originSha)} origin commit](${commitUrl(context, operation.originSha)})`); - links.push(`[${shortSha(operation.sourceSha)} prepared commit](${commitUrl(context, operation.sourceSha)})`); - const activeTarget = operation.reconciliationTargets.find((target) => target.status !== "completed"); - links.push(`[${messages.compare}](${compareUrl(context, activeTarget?.targetBranch ?? operation.productionBranch, activeTarget?.syncBranch ?? activeTarget?.sourceBranch ?? operation.sourceBranch)})`); - if (operation.promotionPullRequest) links.push(`[Promotion PR #${operation.promotionPullRequest}](${pullRequestUrl(context, operation.promotionPullRequest)})`); + links.push(markdownLink(catalog.message('deployment.template.sourceBranchLink', { + branch: safeText(operation.sourceBranch), + }), branchUrl(context, operation.sourceBranch))); + links.push(markdownLink(catalog.message('deployment.template.originCommitLink', { + commit: shortSha(operation.originSha), + }), commitUrl(context, operation.originSha))); + links.push(markdownLink(catalog.message('deployment.template.preparedCommitLink', { + commit: shortSha(operation.sourceSha), + }), commitUrl(context, operation.sourceSha))); + const activeTarget = operation.reconciliationTargets.find(target => target.status !== 'completed'); + links.push(`[${messages.compare}](${compareUrl( + context, + activeTarget?.targetBranch ?? operation.productionBranch, + activeTarget?.syncBranch ?? activeTarget?.sourceBranch ?? operation.sourceBranch, + )})`); + if (operation.promotionPullRequest) { + links.push(markdownLink(catalog.message('deployment.template.promotionPullRequestLink', { + number: operation.promotionPullRequest, + }), pullRequestUrl(context, operation.promotionPullRequest))); + } for (const target of operation.reconciliationTargets) { - if (target.pullRequest) links.push(`[Reconciliation PR #${target.pullRequest}](${pullRequestUrl(context, target.pullRequest)})`); + if (target.pullRequest) { + links.push(markdownLink(catalog.message('deployment.template.reconciliationPullRequestLink', { + number: target.pullRequest, + }), pullRequestUrl(context, target.pullRequest))); + } + } + if (operation.productionSha) { + links.push(markdownLink(catalog.message('deployment.template.productionCommitLink', { + commit: shortSha(operation.productionSha), + }), commitUrl(context, operation.productionSha))); } - if (operation.productionSha) links.push(`[${shortSha(operation.productionSha)} production commit](${commitUrl(context, operation.productionSha)})`); if (operation.publicationVerified) { - links.push(`[${safeText(operation.tag)} GitHub Release](${repositoryUrl(context)}/releases/tag/${encodeURIComponent(operation.tag)})`); - links.push(`[v${safeText(operation.version.split(".")[0])} Action tag](${branchUrl(context, `v${operation.version.split(".")[0]}`)})`); - if (context.packageName) links.push(`[${safeText(context.packageName)}@${safeText(operation.version)} on npm](${npmVersionUrl(context.packageName, operation.version)})`); + links.push(markdownLink(catalog.message('deployment.template.releaseLink', { + tag: safeText(operation.tag), + }), `${repositoryUrl(context)}/releases/tag/${encodeURIComponent(operation.tag)}`)); + const actionTag = `v${operation.version.split('.')[0]}`; + links.push(markdownLink(catalog.message('deployment.template.actionTagLink', { + tag: actionTag, + }), branchUrl(context, actionTag))); + if (context.packageName) { + links.push(markdownLink(catalog.message('deployment.template.npmLink', { + package: safeText(context.packageName), + version: safeText(operation.version), + }), npmVersionUrl(context.packageName, operation.version))); + } + } + if (context.workflowRunUrl) { + links.push(markdownLink( + catalog.message('deployment.template.workflowRunLink'), + safeUrl(context.workflowRunUrl), + )); } - if (context.workflowRunUrl) links.push(`[Workflow run](${safeUrl(context.workflowRunUrl)})`); return links; } -function messagesFor(locale: string): DeploymentMessages { return normalizeLocale(locale) === "es-ES" ? ES : EN; } -export function normalizeLocale(locale: string): SupportedLocale { return locale.toLowerCase().startsWith("es") ? "es-ES" : "en-US"; } function reconciliationCompleted(operation: DeploymentOperationSnapshot): boolean { - return operation.phase === "completed" + return operation.phase === 'completed' || (operation.reconciliationTargets.length > 0 - && operation.reconciliationTargets.every((target) => target.status === "completed")); + && operation.reconciliationTargets.every(target => target.status === 'completed')); +} + +function phaseRank(phase: DeploymentPhase): number { + return ['preparing', 'promotion_pr_pending', 'promoted', 'publishing', 'published', 'reconciliation_pending', 'completed'].indexOf(phase); +} + +function repositoryUrl(context: DeploymentPresentationContext): string { + return `https://github.com/${encodeURIComponent(context.owner)}/${encodeURIComponent(context.repository)}`; +} + +function issueUrl(context: DeploymentPresentationContext): string { + return `${repositoryUrl(context)}/issues/${context.issue}`; +} + +function pullRequestUrl(context: DeploymentPresentationContext, number: number): string { + return `${repositoryUrl(context)}/pull/${number}`; +} + +function branchUrl(context: DeploymentPresentationContext, branch: string): string { + return `${repositoryUrl(context)}/tree/${encodeURIComponent(branch)}`; +} + +function commitUrl(context: DeploymentPresentationContext, sha: string): string { + return `${repositoryUrl(context)}/commit/${encodeURIComponent(sha)}`; +} + +function npmVersionUrl(packageName: string, version: string): string { + return `https://www.npmjs.com/package/${encodeURIComponent(packageName)}/v/${encodeURIComponent(version)}`; +} + +function compareUrl(context: DeploymentPresentationContext, base: string, head: string): string { + return `${repositoryUrl(context)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`; +} + +function markdownLink(label: string, url: string): string { + return `[${safeLinkLabel(label)}](${url})`; +} + +function inline(value: string): string { + return `\`${safeText(value)}\``; +} + +function safeText(value: string): string { + return value.replace(/[\r\n`<>|]/gu, '').replace(/@/gu, '@\u200b').replace(/::/gu, '﹕﹕').slice(0, 240); +} + +function safeLinkLabel(value: string): string { + return safeText(value).replace(/[[\]()]/gu, ''); +} + +function safeMarkerValue(value: string): string { + return value.replace(/[^A-Za-z0-9._-]/gu, '').slice(0, 128); +} + +function safeUrl(value: string): string { + return /^https:\/\/github\.com\//u.test(value) ? value : 'https://github.com'; +} + +function shortSha(value: string): string { + return safeText(value).slice(0, 7); } -function phaseRank(phase: DeploymentPhase): number { return ["preparing", "promotion_pr_pending", "promoted", "publishing", "published", "reconciliation_pending", "completed"].indexOf(phase); } -function repositoryUrl(context: DeploymentPresentationContext): string { return `https://github.com/${encodeURIComponent(context.owner)}/${encodeURIComponent(context.repository)}`; } -function issueUrl(context: DeploymentPresentationContext): string { return `${repositoryUrl(context)}/issues/${context.issue}`; } -function pullRequestUrl(context: DeploymentPresentationContext, number: number): string { return `${repositoryUrl(context)}/pull/${number}`; } -function branchUrl(context: DeploymentPresentationContext, branch: string): string { return `${repositoryUrl(context)}/tree/${encodeURIComponent(branch)}`; } -function commitUrl(context: DeploymentPresentationContext, sha: string): string { return `${repositoryUrl(context)}/commit/${encodeURIComponent(sha)}`; } -function npmVersionUrl(packageName: string, version: string): string { return `https://www.npmjs.com/package/${encodeURIComponent(packageName)}/v/${encodeURIComponent(version)}`; } -function compareUrl(context: DeploymentPresentationContext, base: string, head: string): string { return `${repositoryUrl(context)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`; } -function inline(value: string): string { return `\`${safeText(value)}\``; } -function safeText(value: string): string { return value.replace(/[\r\n`<>]/g, "").replace(/@/g, "@\u200b").replace(/::/g, "﹕﹕").slice(0, 240); } -function safeMarkerValue(value: string): string { return value.replace(/[^A-Za-z0-9._-]/g, "").slice(0, 128); } -function safeUrl(value: string): string { return /^https:\/\/github\.com\//.test(value) ? value : "https://github.com"; } -function shortSha(value: string): string { return safeText(value).slice(0, 7); } -function capitalize(value: string): string { return value.charAt(0).toUpperCase() + value.slice(1); } diff --git a/src/application/ports/deployment_orchestration_ports.ts b/src/application/ports/deployment_orchestration_ports.ts index 7f925c4a1..e323688a1 100644 --- a/src/application/ports/deployment_orchestration_ports.ts +++ b/src/application/ports/deployment_orchestration_ports.ts @@ -8,6 +8,8 @@ import type { } from "../../domain/deployment_state_fence"; import type { DeploymentConfigurationValues } from "../../domain/deployment_configuration"; import type { CopilotLifecycleLabels } from "../../domain/copilot_lifecycle"; +import type { AgentConfiguration } from '../../domain/agent'; +import type { LocaleProfile } from '../../domain/locale'; export interface ManagedPullRequestRecord { readonly number: number; @@ -221,7 +223,8 @@ export interface DeploymentOrchestrationContext { readonly hotfixTree: string; }; readonly workflows: { readonly release: string; readonly hotfix: string }; - readonly locale: { readonly issue: string; readonly pullRequest: string }; + readonly locale: LocaleProfile; + readonly agentConfiguration?: Readonly; readonly labels: { readonly isRelease: boolean; readonly isHotfix: boolean; diff --git a/src/application/services/deployment_orchestration_dependencies.ts b/src/application/services/deployment_orchestration_dependencies.ts index 14ed67feb..4226964f2 100644 --- a/src/application/services/deployment_orchestration_dependencies.ts +++ b/src/application/services/deployment_orchestration_dependencies.ts @@ -9,6 +9,7 @@ import type { } from "../ports/deployment_orchestration_ports"; import type { BoundIssueClosurePort } from "../ports/issue_lifecycle_ports"; import type { BoundIssueLabelsPort } from "../ports/issue_management_ports"; +import type { MessageCatalogResolutionPort } from '../ports/message_catalog_ports'; export interface DeploymentOrchestrationDependencies { readonly pullRequests: BoundManagedPullRequestPort; @@ -21,4 +22,5 @@ export interface DeploymentOrchestrationDependencies { readonly labels: BoundIssueLabelsPort; readonly issues: BoundIssueClosurePort; readonly operationId: () => string; + readonly catalogResolver?: MessageCatalogResolutionPort; } diff --git a/src/application/services/deployment_orchestration_runtime.ts b/src/application/services/deployment_orchestration_runtime.ts index 7e878baf3..4eeda73b4 100644 --- a/src/application/services/deployment_orchestration_runtime.ts +++ b/src/application/services/deployment_orchestration_runtime.ts @@ -12,10 +12,16 @@ import { import { deploymentDashboardMarker, renderDeploymentDashboard, + renderDeploymentMilestone, renderPromotionPullRequest, renderReconciliationPullRequest, + type DeploymentMilestone, type DeploymentPresentationContext, } from "../policies/deployment_presentation_policy"; +import { + resolveDeploymentCatalog, + type DeploymentMessageCatalog, +} from "../policies/deployment_message_catalog"; import { blockDeploymentOperation, type DeploymentOperationSnapshot, @@ -32,6 +38,8 @@ import { DeploymentStateBoundary } from "./deployment_state_boundary"; export const DEPLOYMENT_ORCHESTRATION_TASK_ID = "DeploymentOrchestrationUseCase"; export class DeploymentOrchestrationRuntime { + private readonly presentationCatalogs = new Map>(); + constructor( readonly dependencies: DeploymentOrchestrationDependencies, readonly stateBoundary: DeploymentStateBoundary, @@ -59,8 +67,7 @@ export class DeploymentOrchestrationRuntime { await this.publishMilestone( context, blocked, - "reconciliation-blocked", - `❌ Deployment blocked: ${blocked.lastFailure?.message}`, + { kind: "reconciliation-blocked", reason: blocked.lastFailure?.message ?? message }, ); return new Result({ id: DEPLOYMENT_ORCHESTRATION_TASK_ID, @@ -76,7 +83,8 @@ export class DeploymentOrchestrationRuntime { operation: DeploymentOperationSnapshot, ): Promise { const marker = deploymentDashboardMarker(operation.operationId, context.singleAction.issue); - const body = renderDeploymentDashboard(operation, presentationContext(context)); + const catalog = await this.presentationCatalog("issue", context, operation); + const body = renderDeploymentDashboard(operation, presentationContext(context, operation), catalog); const current = await this.dependencies.presentation.findDashboard( context.singleAction.issue, marker, @@ -98,15 +106,15 @@ export class DeploymentOrchestrationRuntime { async publishMilestone( context: DeploymentOrchestrationContext, operation: DeploymentOperationSnapshot, - name: string, - body: string, + milestone: DeploymentMilestone, ): Promise { if (operation.commentMode !== "milestones") return; - const marker = ``; + const marker = ``; + const catalog = await this.presentationCatalog("issue", context, operation); await this.dependencies.presentation.publishMilestone( context.singleAction.issue, marker, - body, + renderDeploymentMilestone(milestone, catalog), ); } @@ -133,10 +141,11 @@ export class DeploymentOrchestrationRuntime { ); } if (existing[0]) return existing[0]; - const presentation = presentationContext(context); + const presentation = presentationContext(context, operation); + const catalog = await this.presentationCatalog("pull-request", context, operation); const content = phase === "promotion" - ? renderPromotionPullRequest(operation, presentation) - : renderReconciliationPullRequest(operation, requireTarget(target), presentation); + ? renderPromotionPullRequest(operation, presentation, catalog) + : renderReconciliationPullRequest(operation, requireTarget(target), presentation, catalog); return await this.dependencies.pullRequests.createManagedPullRequest({ ...query, ...content }); } @@ -195,7 +204,7 @@ export class DeploymentOrchestrationRuntime { ); const decision = selectPullRequestMode(operation.prMode, capabilities); if (decision.kind === "unsupported") { - return this.unsupportedMergeBehavior(context, targetRole, targetBranch, capabilities, decision); + return this.unsupportedMergeBehavior(context, operation, targetRole, targetBranch, capabilities, decision); } if (decision.mode === "merge-queue") { const readiness = evaluateMergeQueueReadiness({ @@ -209,7 +218,7 @@ export class DeploymentOrchestrationRuntime { if (readiness.verdict !== "ready") { return { kind: "blocked", - reason: mergeQueueReadinessFailureMessage(readiness, context.locale.issue), + reason: mergeQueueReadinessFailureMessage(readiness, effectiveLocale(context, operation).issue), }; } } @@ -293,6 +302,7 @@ export class DeploymentOrchestrationRuntime { private unsupportedMergeBehavior( context: DeploymentOrchestrationContext, + operation: DeploymentOperationSnapshot, targetRole: MergeQueueTargetRole, targetBranch: string, capabilities: TargetMergeCapabilities, @@ -311,10 +321,29 @@ export class DeploymentOrchestrationRuntime { }); return { kind: "blocked", - reason: mergeQueueReadinessFailureMessage(readiness, context.locale.issue), + reason: mergeQueueReadinessFailureMessage(readiness, effectiveLocale(context, operation).issue), }; } + private presentationCatalog( + scope: "issue" | "pull-request", + context: DeploymentOrchestrationContext, + operation: DeploymentOperationSnapshot, + ): Promise { + const locale = effectiveLocale(context, operation); + const targetLocale = scope === "issue" ? locale.issue : locale.pullRequest; + const key = `${scope}:${targetLocale}`; + const existing = this.presentationCatalogs.get(key); + if (existing) return existing; + const resolution = resolveDeploymentCatalog( + targetLocale, + context.agentConfiguration, + this.dependencies.catalogResolver, + ); + this.presentationCatalogs.set(key, resolution); + return resolution; + } + private async cleanupSyncBranches( context: DeploymentOrchestrationContext, operation: DeploymentOperationSnapshot, @@ -412,17 +441,29 @@ export function semanticCleanupError(error: unknown): ApplicationError { return toApplicationError(error, "workflow.failed", "Deployment cleanup failed."); } -function presentationContext(context: DeploymentOrchestrationContext): DeploymentPresentationContext { +function presentationContext( + context: DeploymentOrchestrationContext, + operation: DeploymentOperationSnapshot, +): DeploymentPresentationContext { + const locale = effectiveLocale(context, operation); return { owner: context.owner, repository: context.repo, issue: context.singleAction.issue, - issueLocale: context.locale.issue, - pullRequestLocale: context.locale.pullRequest, + repositoryLocale: locale.repository, + issueLocale: locale.issue, + pullRequestLocale: locale.pullRequest, packageName: context.owner === "vypdev" && context.repo === "copilot" ? "@vypdev/copilot" : undefined, }; } +function effectiveLocale( + context: DeploymentOrchestrationContext, + operation: DeploymentOperationSnapshot, +): DeploymentOrchestrationContext["locale"] { + return operation.locale ?? context.locale; +} + function requireTarget(target: ReconciliationTargetState | undefined): ReconciliationTargetState { if (!target) throw new ApplicationError("workflow.stale", "Reconciliation target is missing."); return target; diff --git a/src/application/usecases/__tests__/push_single_action_contexts.test.ts b/src/application/usecases/__tests__/push_single_action_contexts.test.ts index bd7e301c3..46c07acfd 100644 --- a/src/application/usecases/__tests__/push_single_action_contexts.test.ts +++ b/src/application/usecases/__tests__/push_single_action_contexts.test.ts @@ -386,7 +386,8 @@ describe('push and single-action context projection', () => { owner: 'owner', repo: 'repo', tokens: { token: 'secret-token' }, branches: { defaultBranch: 'master', development: 'develop', releaseTree: 'release', hotfixTree: 'hotfix' }, workflows: { release: 'release.yml', hotfix: 'hotfix.yml' }, - locale: { issue: 'en', pullRequest: 'en' }, + locale: { repository: 'fr-FR', issue: 'es-ES', pullRequest: 'de-DE', issueOverride: 'es-ES', pullRequestOverride: 'de-DE' }, + ai: { getAgentConfiguration: () => ({ provider: 'codex', model: 'planner-model' }) }, labels: { isRelease: true, isHotfix: false, deploy: 'deploy', deployed: 'deployed', lifecycle: DEFAULT_COPILOT_LIFECYCLE_LABELS }, deployment: { ...DEFAULT_DEPLOYMENT_CONFIGURATION }, singleAction: { @@ -407,6 +408,10 @@ describe('push and single-action context projection', () => { expect('tokens' in projected).toBe(false); expect(raw.currentConfiguration.releaseBranch).toBe('release/1.2.3'); expect(Object.isFrozen(projected.branches)).toBe(true); + expect(projected.locale).toEqual(raw.locale); + expect(Object.isFrozen(projected.locale)).toBe(true); + expect(projected.agentConfiguration).toEqual({ provider: 'codex', model: 'planner-model' }); + expect(Object.isFrozen(projected.agentConfiguration)).toBe(true); expect(withoutOperation.currentConfiguration.deploymentOrchestration).toBeUndefined(); }); }); diff --git a/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts b/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts index 0bc09d839..d134da431 100644 --- a/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts @@ -8,6 +8,7 @@ import { decideDeploymentStateSave, readDeploymentOperationState, } from "../../../../domain/deployment_state_fence"; +import { ResolveMessageCatalogUseCase } from "../../../usecases/localization/resolve_message_catalog_use_case"; const sourceSha = "a".repeat(40); const originSha = "b".repeat(40); @@ -91,7 +92,11 @@ function execution(action: "prepare" | "continue" | "published" | "failed", curr tokens: { token: "pat" }, branches: { defaultBranch: "master", development: "develop", releaseTree: "release", hotfixTree: "hotfix" }, workflows: { release: "release_workflow.yml", hotfix: "hotfix_workflow.yml" }, - locale: { issue: "en-US", pullRequest: "en-US" }, + locale: { + repository: "en-US", + issue: "en-US", + pullRequest: "en-US", + }, labels: { isRelease: true, isHotfix: false, @@ -126,6 +131,8 @@ function execution(action: "prepare" | "continue" | "published" | "failed", curr } function harness() { + const catalogResolver = new ResolveMessageCatalogUseCase(); + const catalogResolve = jest.spyOn(catalogResolver, "resolve"); const pullRequests = { findManagedPullRequests: jest.fn().mockResolvedValue([]), createManagedPullRequest: jest.fn().mockResolvedValue(pr()), @@ -192,6 +199,7 @@ function harness() { labels, issues, operationId: () => "operation-12345678", + catalogResolver, }); return { useCase, @@ -205,6 +213,7 @@ function harness() { stateFactory, labels, issues, + catalogResolve, }; } @@ -218,6 +227,30 @@ describe("DeploymentOrchestrationUseCase", () => { expect(input.currentConfiguration.deploymentOrchestration).toEqual(expect.objectContaining({ phase: "promotion_pr_pending", promotionPullRequest: 40 })); }); + it("resolves one catalog per destination and snapshots the effective locale profile", async () => { + const value = harness(); + const input = execution("prepare"); + input.locale = { + repository: "fr-FR", + issue: "es-ES", + pullRequest: "de-DE", + issueOverride: "es-ES", + pullRequestOverride: "de-DE", + }; + Object.assign(input, { agentConfiguration: { provider: "codex", model: "planner-model" } }); + + await value.useCase.invoke(input); + + expect(value.catalogResolve).toHaveBeenCalledTimes(2); + expect(value.catalogResolve.mock.calls.map(([request]) => request.targetLocale).sort()) + .toEqual(["de-DE", "es-ES"]); + expect(value.catalogResolve).toHaveBeenCalledWith(expect.objectContaining({ + configuration: { provider: "codex", model: "planner-model" }, + })); + expect(input.currentConfiguration.deploymentOrchestration?.locale).toEqual(input.locale); + expect(Object.isFrozen(input.currentConfiguration.deploymentOrchestration?.locale)).toBe(true); + }); + it("snapshots the exact release origin SHA and workflow", async () => { const value = harness(); const input = execution("prepare"); @@ -274,6 +307,23 @@ describe("DeploymentOrchestrationUseCase", () => { expect(value.pullRequests.createManagedPullRequest).not.toHaveBeenCalled(); }); + it("updates the owned dashboard and lets legacy state use the current locale profile", async () => { + const value = harness(); + value.presentation.findDashboard.mockResolvedValue({ id: 99 }); + value.pullRequests.findManagedPullRequests.mockResolvedValue([pr()]); + const input = execution("prepare", operation("promotion_pr_pending")); + input.locale = { repository: "es-ES", issue: "es-ES", pullRequest: "es-ES" }; + + await value.useCase.invoke(input); + + expect(value.presentation.createDashboard).not.toHaveBeenCalled(); + expect(value.presentation.updateDashboard).toHaveBeenCalledWith( + 355, + 99, + expect.stringContaining("Estado actual"), + ); + }); + it("blocks if the frozen release head changed after preparation", async () => { const value = harness(); value.git.getBranchSha.mockResolvedValue("f".repeat(40)); diff --git a/src/application/usecases/actions/__tests__/deployment_phase_handlers.test.ts b/src/application/usecases/actions/__tests__/deployment_phase_handlers.test.ts index 64faa3668..8ffbf625a 100644 --- a/src/application/usecases/actions/__tests__/deployment_phase_handlers.test.ts +++ b/src/application/usecases/actions/__tests__/deployment_phase_handlers.test.ts @@ -82,7 +82,7 @@ function context( hotfixTree: "hotfix", }, workflows: { release: "release_workflow.yml", hotfix: "hotfix_workflow.yml" }, - locale: { issue: "en-US", pullRequest: "en-US" }, + locale: { repository: "en-US", issue: "en-US", pullRequest: "en-US" }, labels: { isRelease: true, isHotfix: false, diff --git a/src/application/usecases/actions/deployment_handlers/accept_promotion_handler.ts b/src/application/usecases/actions/deployment_handlers/accept_promotion_handler.ts index e3cec4a4e..2d4155e63 100644 --- a/src/application/usecases/actions/deployment_handlers/accept_promotion_handler.ts +++ b/src/application/usecases/actions/deployment_handlers/accept_promotion_handler.ts @@ -67,8 +67,7 @@ export class AcceptPromotionHandler { await this.runtime.publishMilestone( context, publishing, - "promotion-merged", - `✅ Promotion PR #${pullRequest.number} merged. Publication is starting from production SHA \`${productionSha}\`.`, + { kind: "promotion-merged", pullRequest: pullRequest.number, productionSha }, ); await this.runtime.dependencies.continuation.dispatch( operation.publicationWorkflow, diff --git a/src/application/usecases/actions/deployment_handlers/confirm_publication_handler.ts b/src/application/usecases/actions/deployment_handlers/confirm_publication_handler.ts index 0195137f6..fa55830e0 100644 --- a/src/application/usecases/actions/deployment_handlers/confirm_publication_handler.ts +++ b/src/application/usecases/actions/deployment_handlers/confirm_publication_handler.ts @@ -75,8 +75,7 @@ export class ConfirmPublicationHandler { await this.runtime.publishMilestone( context, published, - "publication-complete", - `📦 ${published.tag} is published from accepted production SHA \`${published.productionSha}\`.`, + { kind: "publication-complete", tag: published.tag, productionSha }, ); return await this.beginReconciliation(context, published); } diff --git a/src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts b/src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts index c46c36c5f..0a9e200a6 100644 --- a/src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts +++ b/src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts @@ -103,6 +103,7 @@ export class PreparePromotionHandler { ); const operation = buildInitialDeploymentOperation({ operationId: this.runtime.dependencies.operationId(), + locale: context.locale, kind, version: context.singleAction.version, title: context.singleAction.title, @@ -194,6 +195,7 @@ function validateOperation( ): void { const errors = validateInitialDeploymentInput({ operationId: operation.operationId, + locale: operation.locale ?? context.locale, kind: operation.kind, version: operation.version, title: operation.title, diff --git a/src/application/usecases/actions/deployment_handlers/reconciliation_handler.ts b/src/application/usecases/actions/deployment_handlers/reconciliation_handler.ts index 2fdad071c..6cc911383 100644 --- a/src/application/usecases/actions/deployment_handlers/reconciliation_handler.ts +++ b/src/application/usecases/actions/deployment_handlers/reconciliation_handler.ts @@ -194,8 +194,7 @@ export class ReconciliationHandler { await this.runtime.publishMilestone( context, completed, - "orchestration-complete", - `✅ Deployment ${completed.tag} and every configured reconciliation target are complete.`, + { kind: "orchestration-complete", tag: completed.tag }, ); return deploymentSuccess(`Deployment ${completed.tag} completed${completionContext}.`); } diff --git a/src/application/usecases/push_single_action_contexts.ts b/src/application/usecases/push_single_action_contexts.ts index 3827c8b02..32a00f28d 100644 --- a/src/application/usecases/push_single_action_contexts.ts +++ b/src/application/usecases/push_single_action_contexts.ts @@ -236,7 +236,9 @@ export function projectDeploymentPublicationContext(source: PushSingleActionCont } export function projectDeploymentOrchestrationContext( - source: DeploymentOrchestrationContext, + source: DeploymentOrchestrationContext & { + readonly ai?: { getAgentConfiguration(task: 'planner'): AgentConfiguration }; + }, ): DeploymentOrchestrationContext { return { owner: source.owner, @@ -244,6 +246,11 @@ export function projectDeploymentOrchestrationContext( branches: Object.freeze({ ...source.branches }), workflows: Object.freeze({ ...source.workflows }), locale: Object.freeze({ ...source.locale }), + ...(source.agentConfiguration + ? { agentConfiguration: Object.freeze({ ...source.agentConfiguration }) } + : source.ai + ? { agentConfiguration: Object.freeze({ ...source.ai.getAgentConfiguration('planner') }) } + : {}), labels: Object.freeze({ ...source.labels, lifecycle: Object.freeze({ ...source.labels.lifecycle }), @@ -441,6 +448,7 @@ function copyInitialLabels(source: PushSingleActionContextSource['labels']): Ini function copyDeploymentOperation(operation: DeploymentOperationSnapshot): DeploymentOperationSnapshot { return Object.freeze({ ...operation, + ...(operation.locale ? { locale: Object.freeze({ ...operation.locale }) } : {}), reconciliationTargets: Object.freeze((operation.reconciliationTargets ?? []).map(target => Object.freeze({ ...target }))), ...(operation.publicationReceipt ? { publicationReceipt: Object.freeze({ ...operation.publicationReceipt }) } : {}), ...(operation.lastFailure ? { lastFailure: Object.freeze({ ...operation.lastFailure }) } : {}), diff --git a/src/architecture/__tests__/github_publication_boundaries.test.ts b/src/architecture/__tests__/github_publication_boundaries.test.ts index 25e1cccd9..89cbe0094 100644 --- a/src/architecture/__tests__/github_publication_boundaries.test.ts +++ b/src/architecture/__tests__/github_publication_boundaries.test.ts @@ -87,6 +87,7 @@ describe('GitHub conversation publication boundaries', () => { 'src/application/policies/bugbot_review_presentation_policy.ts', 'src/application/policies/semantic_result_publication_policy.ts', 'src/application/policies/status_command_policy.ts', + 'src/application/policies/deployment_presentation_policy.ts', 'src/application/usecases/steps/common/reply_publication_workflow.ts', 'src/application/usecases/steps/common/status_card_publication_workflow.ts', 'src/application/usecases/steps/commit/bugbot/publish_overflow_comment.ts', @@ -99,6 +100,14 @@ describe('GitHub conversation publication boundaries', () => { expect(violations).toEqual([]); }); + it('keeps deployment presentation semantic instead of replaying internal Result steps', () => { + const completion = readFileSync(join(root, 'src/actions/github_action_completion.ts'), 'utf8'); + const presentation = readFileSync(join(root, 'src/application/policies/deployment_presentation_policy.ts'), 'utf8'); + + expect(completion).not.toMatch(/renderDeploymentJobSummary[\s\S]{0,500}result\.steps/u); + expect(presentation).not.toMatch(/operations|Automatic Actions|Feature Actions/u); + }); + it('keeps Bugbot public presentation free of pseudo-plural copy', () => { const files = [ 'src/application/policies/bugbot_message_catalog.ts', diff --git a/src/domain/__tests__/deployment_operation.test.ts b/src/domain/__tests__/deployment_operation.test.ts index 63d41248c..8ddd918a6 100644 --- a/src/domain/__tests__/deployment_operation.test.ts +++ b/src/domain/__tests__/deployment_operation.test.ts @@ -132,6 +132,25 @@ describe("deployment operation state machine", () => { expect(isDeploymentOperationSnapshot(operation())).toBe(true); }); + it("accepts legacy state without a locale and validates canonical locale snapshots", () => { + expect(isDeploymentOperationSnapshot(operation())).toBe(true); + expect(isDeploymentOperationSnapshot(operation("preparing", { + locale: { + repository: "fr-FR", + issue: "es-ES", + pullRequest: "de-DE", + issueOverride: "es-ES", + pullRequestOverride: "de-DE", + }, + }))).toBe(true); + expect(isDeploymentOperationSnapshot(operation("preparing", { + locale: { repository: "en-us", issue: "en-US", pullRequest: "en-US" }, + }))).toBe(false); + expect(isDeploymentOperationSnapshot(operation("preparing", { + locale: { repository: "en-US", issue: "es-ES", pullRequest: "en-US" }, + }))).toBe(false); + }); + it("requires an exact provider receipt when publication is verified", () => { expect(isDeploymentOperationSnapshot(operation("published", { productionSha: "c".repeat(40), diff --git a/src/domain/__tests__/locale.test.ts b/src/domain/__tests__/locale.test.ts index fd68b2481..d09d95954 100644 --- a/src/domain/__tests__/locale.test.ts +++ b/src/domain/__tests__/locale.test.ts @@ -3,6 +3,7 @@ import { InvalidLocaleTagError, baseLanguage, canonicalizeLocaleTag, + isLocaleProfile, localeForScope, localeLanguagesMatch, normalizeLocaleTag, @@ -94,4 +95,13 @@ describe('locale policy', () => { expect(localeLanguagesMatch('es', 'en')).toBe(false); expect(baseLanguage('zh-Hant-TW')).toBe('zh'); }); + + it('accepts only canonical, internally consistent locale profiles', () => { + expect(isLocaleProfile(resolveLocaleProfile('fr-fr', 'es_mx', 'zh-Hant-TW'))).toBe(true); + expect(isLocaleProfile({ repository: 'en-US', issue: 'es-ES', pullRequest: 'en-US' })).toBe(false); + expect(isLocaleProfile({ repository: 'en-us', issue: 'en-US', pullRequest: 'en-US' })).toBe(false); + expect(isLocaleProfile({ repository: 'en-US', issue: 'en-US', pullRequest: 'en-US', issueOverride: '' })).toBe(false); + expect(isLocaleProfile({ repository: 'not a locale', issue: 'en-US', pullRequest: 'en-US' })).toBe(false); + expect(isLocaleProfile(null)).toBe(false); + }); }); diff --git a/src/domain/__tests__/message_catalog.test.ts b/src/domain/__tests__/message_catalog.test.ts index 6b9210a6b..0c5d57b1c 100644 --- a/src/domain/__tests__/message_catalog.test.ts +++ b/src/domain/__tests__/message_catalog.test.ts @@ -156,6 +156,7 @@ describe('message catalog domain policy', () => { [{ greeting: 'Bonjour @team {name}', items: { one: '{count} élément', other: '{count} éléments' } }, 'mention'], [{ greeting: 'Voir https://example.com/{name}', items: { one: '{count} élément', other: '{count} éléments' } }, 'URL'], [{ greeting: ' {name}', items: { one: '{count} élément', other: '{count} éléments' } }, 'marker'], + [{ greeting: 'Bonjour | forged {name}', items: { one: '{count} élément', other: '{count} éléments' } }, 'table delimiter'], [{ greeting: '\u202EBonjour {name}', items: { one: '{count} élément', other: '{count} éléments' } }, 'bidi control'], [{ greeting: 'Bonjour {name}', items: { one: '{count} élément', other: '' } }, 'empty plural variant'], ])('rejects unsafe dynamic catalog output: %s', (value, _reason) => { diff --git a/src/domain/deployment_operation.ts b/src/domain/deployment_operation.ts index 438b4d53c..d8e14ed86 100644 --- a/src/domain/deployment_operation.ts +++ b/src/domain/deployment_operation.ts @@ -18,6 +18,7 @@ import { RECONCILIATION_PR_MODES, RECONCILIATION_STRATEGIES, } from "./deployment_configuration"; +import { isLocaleProfile, type LocaleProfile } from './locale'; export const DEPLOYMENT_PHASES = [ "preparing", @@ -63,6 +64,8 @@ export interface DeploymentOperationSnapshot { readonly stateVersion: typeof DEPLOYMENT_STATE_VERSION; readonly revision: number; readonly operationId: string; + /** Effective locale profile captured when the durable operation starts. Absent only on legacy v1 state. */ + readonly locale?: LocaleProfile; readonly kind: DeploymentKind; readonly version: string; readonly title: string; @@ -189,6 +192,7 @@ export function isDeploymentOperationSnapshot(value: unknown): value is Deployme && operation.revision > 0 && typeof operation.operationId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operation.operationId) + && (operation.locale === undefined || isLocaleProfile(operation.locale)) && (operation.kind === "release" || operation.kind === "hotfix") && typeof operation.version === "string" && /^[0-9]+\.[0-9]+\.[0-9]+$/.test(operation.version) && typeof operation.title === "string" && operation.title.length <= 1_000 diff --git a/src/domain/locale.ts b/src/domain/locale.ts index b84bcb83c..1a4db0859 100644 --- a/src/domain/locale.ts +++ b/src/domain/locale.ts @@ -86,6 +86,25 @@ export function localeForScope(profile: LocaleProfile, scope: LocaleScope): stri return profile.repository; } +export function isLocaleProfile(value: unknown): value is LocaleProfile { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const candidate = value as Partial; + try { + const resolved = resolveLocaleProfile( + candidate.repository, + candidate.issueOverride ?? '', + candidate.pullRequestOverride ?? '', + ); + return candidate.repository === resolved.repository + && candidate.issue === resolved.issue + && candidate.pullRequest === resolved.pullRequest + && candidate.issueOverride === resolved.issueOverride + && candidate.pullRequestOverride === resolved.pullRequestOverride; + } catch { + return false; + } +} + export function localeLanguagesMatch(left: string, right: string): boolean { return baseLanguage(canonicalizeLocaleTag(left)) === baseLanguage(canonicalizeLocaleTag(right)); } diff --git a/src/domain/message_catalog.ts b/src/domain/message_catalog.ts index 0f512e3d6..0a324497d 100644 --- a/src/domain/message_catalog.ts +++ b/src/domain/message_catalog.ts @@ -199,7 +199,7 @@ function validMessageText(value: unknown): value is string { function safeDynamicText(value: string): boolean { return validMessageText(value) && !/[\r\n\u202A-\u202E\u2066-\u2069]/u.test(value) - && !/|<\/?[A-Za-z]|https?:\/\/|```|[`*_[\]~]|(^|\s)\/(?:copilot)(?:\s|$)|@[A-Za-z0-9]/iu.test(value); + && !/|<\/?[A-Za-z]|https?:\/\/|```|[`*_[\]~|]|(^|\s)\/(?:copilot)(?:\s|$)|@[A-Za-z0-9]/iu.test(value); } function pluralPlaceholderParity(message: CatalogPluralMessage): boolean { diff --git a/src/infrastructure/composition/main_run_route_composition_root.ts b/src/infrastructure/composition/main_run_route_composition_root.ts index 4274f15dc..710b8ad5c 100644 --- a/src/infrastructure/composition/main_run_route_composition_root.ts +++ b/src/infrastructure/composition/main_run_route_composition_root.ts @@ -126,7 +126,7 @@ export function createSingleActionUseCaseCompositionRoot( ? new RepositoryReleasePublicationRepository(createReleaseClient()) : undefined; const deploymentOrchestration = surface === "github-workflow" - ? createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, repositoryReleasePort!, binding) + ? createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, repositoryReleasePort!, binding, catalogResolver) : undefined; return new SingleActionUseCase( repositoryTagPort && repositoryReleasePort @@ -163,6 +163,7 @@ function createDeploymentOrchestrationUseCase( issueDescriptionQueryPort: ReturnType, publication: RepositoryReleasePublicationRepository, binding: BugbotScmBinding, + catalogResolver: ResolveMessageCatalogUseCase, ): DeploymentOrchestrationUseCase { const deploymentClient = new OctokitDeploymentClientAdapter(); return new DeploymentOrchestrationUseCase({ @@ -178,6 +179,7 @@ function createDeploymentOrchestrationUseCase( labels: bindDeploymentLabels(createIssueLabelRepository(), binding), issues: bindDeploymentIssues(createIssueClosureRepository(), binding), operationId: randomUUID, + catalogResolver, }); } From 519de5d931d6fc696faa0d1914f8089eed189041 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Mon, 14 Sep 2026 22:04:48 +0200 Subject: [PATCH 2/5] codex-deployment-localized-presentation: preserve deployment locale inheritance --- build/cli/index.js | 4 +- build/github_action/index.js | 4 +- docs/issues/deployment-orchestration.mdx | 4 ++ setup/workflows/hotfix_workflow.yml | 5 +- setup/workflows/release_workflow.yml | 5 +- specs/repository-locale-and-localization.md | 6 +- .../github_action_completion.test.ts | 66 +++++++++++++++++++ .../deployment_presentation_policy.test.ts | 17 +++++ .../policies/deployment_plan_policy.ts | 6 +- .../deployment_orchestration_runtime.ts | 2 +- .../push_single_action_contexts.test.ts | 20 ++++++ .../prepare_promotion_handler.ts | 5 +- .../validate_workflow_contract.test.ts | 2 + 13 files changed, 132 insertions(+), 14 deletions(-) diff --git a/build/cli/index.js b/build/cli/index.js index 39f2518e0..dd2c52b5a 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -46705,7 +46705,7 @@ class DeploymentOrchestrationRuntime { const blocked = (0, deployment_operation_1.blockDeploymentOperation)(operation, category, message, retryable); await this.persist(context, blocked); await this.publishDashboard(context, blocked); - await this.publishMilestone(context, blocked, { kind: "reconciliation-blocked", reason: blocked.lastFailure?.message ?? message }); + await this.publishMilestone(context, blocked, { kind: "reconciliation-blocked", reason: message }); return new result_1.Result({ id: exports.DEPLOYMENT_ORCHESTRATION_TASK_ID, success: false, @@ -47942,7 +47942,7 @@ function selectOriginBranch(context, kind) { function validateOperation(context, operation) { const errors = (0, deployment_plan_policy_1.validateInitialDeploymentInput)({ operationId: operation.operationId, - locale: operation.locale ?? context.locale, + locale: operation.locale, kind: operation.kind, version: operation.version, title: operation.title, diff --git a/build/github_action/index.js b/build/github_action/index.js index 0b33dc29f..30b3626f2 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -47454,7 +47454,7 @@ class DeploymentOrchestrationRuntime { const blocked = (0, deployment_operation_1.blockDeploymentOperation)(operation, category, message, retryable); await this.persist(context, blocked); await this.publishDashboard(context, blocked); - await this.publishMilestone(context, blocked, { kind: "reconciliation-blocked", reason: blocked.lastFailure?.message ?? message }); + await this.publishMilestone(context, blocked, { kind: "reconciliation-blocked", reason: message }); return new result_1.Result({ id: exports.DEPLOYMENT_ORCHESTRATION_TASK_ID, success: false, @@ -48691,7 +48691,7 @@ function selectOriginBranch(context, kind) { function validateOperation(context, operation) { const errors = (0, deployment_plan_policy_1.validateInitialDeploymentInput)({ operationId: operation.operationId, - locale: operation.locale ?? context.locale, + locale: operation.locale, kind: operation.kind, version: operation.version, title: operation.title, diff --git a/docs/issues/deployment-orchestration.mdx b/docs/issues/deployment-orchestration.mdx index 32bd4de23..fe076ef73 100644 --- a/docs/issues/deployment-orchestration.mdx +++ b/docs/issues/deployment-orchestration.mdx @@ -119,6 +119,10 @@ bounded catalog for the destination. If that response is unavailable, incomplete or unsafe, the whole artifact falls back to English; Copilot never mixes languages or creates a separate fallback comment. +The installed release and hotfix workflow templates pass `repository-locale` +and preserve empty issue/PR overrides. This matters when the repository locale is +not English: replacing an empty override with `en-US` would disable inheritance. + The dashboard leads with current status and a required action only when a person must act. Progress, transition, and links follow, while operation IDs and other technical facts stay collapsed. Managed PR descriptions explain their single diff --git a/setup/workflows/hotfix_workflow.yml b/setup/workflows/hotfix_workflow.yml index fd199be4c..ae56bae7d 100644 --- a/setup/workflows/hotfix_workflow.yml +++ b/setup/workflows/hotfix_workflow.yml @@ -136,8 +136,9 @@ jobs: orchestration-presentation-mode: ${{ vars.ORCHESTRATION_PRESENTATION_MODE || 'guided' }} orchestration-diagrams: ${{ vars.ORCHESTRATION_DIAGRAMS || 'true' }} orchestration-comment-mode: ${{ vars.ORCHESTRATION_COMMENT_MODE || 'update' }} - issues-locale: ${{ vars.ISSUES_LOCALE || 'en-US' }} - pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} + repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }} + issues-locale: ${{ vars.ISSUES_LOCALE || '' }} + pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || '' }} token: ${{ secrets.PAT }} publish: diff --git a/setup/workflows/release_workflow.yml b/setup/workflows/release_workflow.yml index 7031a4e4f..e59f15273 100644 --- a/setup/workflows/release_workflow.yml +++ b/setup/workflows/release_workflow.yml @@ -136,8 +136,9 @@ jobs: orchestration-presentation-mode: ${{ vars.ORCHESTRATION_PRESENTATION_MODE || 'guided' }} orchestration-diagrams: ${{ vars.ORCHESTRATION_DIAGRAMS || 'true' }} orchestration-comment-mode: ${{ vars.ORCHESTRATION_COMMENT_MODE || 'update' }} - issues-locale: ${{ vars.ISSUES_LOCALE || 'en-US' }} - pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} + repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }} + issues-locale: ${{ vars.ISSUES_LOCALE || '' }} + pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || '' }} token: ${{ secrets.PAT }} publish: diff --git a/specs/repository-locale-and-localization.md b/specs/repository-locale-and-localization.md index b0a72967f..af18bdf65 100644 --- a/specs/repository-locale-and-localization.md +++ b/specs/repository-locale-and-localization.md @@ -950,8 +950,10 @@ behavior are executable tests. The deployment-presentation slice snapshots the effective locale for new durable operations, resolves one catalog per issue or managed-PR destination, renders repository-locale Job Summaries, localizes the four bounded milestones, and removes internal `Result.steps` from deployment -operator UI. Legacy version-1 operations without a locale remain readable and -use the current effective profile until completion. Merge-readiness diagnostics, +operator UI. The installed release/hotfix templates pass `repository-locale` and +empty inheriting issue/PR overrides. Legacy version-1 operations without a locale +remain readable and use the current effective profile until completion. +Merge-readiness diagnostics, lifecycle, setup/doctor, and the remaining public surfaces are not claimed complete by this evidence. diff --git a/src/actions/__tests__/github_action_completion.test.ts b/src/actions/__tests__/github_action_completion.test.ts index 32e5a0be7..8f88389f1 100644 --- a/src/actions/__tests__/github_action_completion.test.ts +++ b/src/actions/__tests__/github_action_completion.test.ts @@ -295,6 +295,72 @@ describe('finishGithubAction', () => { expect(mockPublishInvoke).not.toHaveBeenCalled(); }); + it('links the published package from the first-party deployment summary', async () => { + const base = deploymentOperation(); + const productionSha = 'c'.repeat(40); + const action = Object.assign(singleActionExecution(), { + owner: 'vypdev', + repo: 'copilot', + singleAction: { + ...singleActionExecution().singleAction, + issue: 11, + isDeploymentOrchestrationAction: true, + }, + currentConfiguration: { + results: [], + deploymentOrchestration: { + ...base, + phase: 'published', + productionSha, + publicationVerified: true, + publicationReceipt: { + tag: base.tag, + productionSha, + operationId: base.operationId, + releaseUrl: 'https://github.com/vypdev/copilot/releases/tag/v3.4.0', + }, + }, + }, + }) as Execution; + + await finishGithubAction( + action, + [new Result({ id: 'DeploymentOrchestrationUseCase', success: true, executed: true })], + {} as never, + {} as never, + undefined, + { publish: mockSummaryPublish }, + ); + + expect(mockSummaryPublish).toHaveBeenCalledWith(expect.stringContaining( + 'https://www.npmjs.com/package/%40vypdev%2Fcopilot/v/3.4.0', + )); + }); + + it('uses the current repository locale for a legacy deployment without a locale snapshot', async () => { + const { locale: _locale, ...legacyOperation } = deploymentOperation(); + const action = Object.assign(singleActionExecution(), { + locale: { repository: 'es-ES', issue: 'es-ES', pullRequest: 'es-ES' }, + singleAction: { + ...singleActionExecution().singleAction, + issue: 11, + isDeploymentOrchestrationAction: true, + }, + currentConfiguration: { results: [], deploymentOrchestration: legacyOperation }, + }) as Execution; + + await finishGithubAction( + action, + [new Result({ id: 'DeploymentOrchestrationUseCase', success: true, executed: true })], + {} as never, + {} as never, + undefined, + { publish: mockSummaryPublish }, + ); + + expect(mockSummaryPublish).toHaveBeenCalledWith(expect.stringContaining('# ⏳ Orquestación del despliegue')); + }); + it('uses the short-lived evidence token only for the native Check Run', async () => { process.env.COPILOT_EVIDENCE_TOKEN = 'github-actions-token'; const action = Object.assign(execution(), { diff --git a/src/application/policies/__tests__/deployment_presentation_policy.test.ts b/src/application/policies/__tests__/deployment_presentation_policy.test.ts index 377260db5..286fb0196 100644 --- a/src/application/policies/__tests__/deployment_presentation_policy.test.ts +++ b/src/application/policies/__tests__/deployment_presentation_policy.test.ts @@ -196,6 +196,23 @@ describe("deployment presentation policy", () => { expect(value.body).toContain('phase="reconciliation" issue="355"'); }); + it("uses the target source fact and preserves an open launcher issue when production is pending", () => { + const target = { + targetBranch: "develop", + sourceBranch: "master", + sourceSha: "d".repeat(40), + status: "pending" as const, + }; + const value = renderReconciliationPullRequest( + operation("reconciliation_pending", { issueCompletion: "keep-open", productionSha: undefined }), + target, + context, + ); + + expect(value.body).toMatch(/`v3\.4\.0@\u200bddddddd`/u); + expect(value.body).toContain("Keep issue open"); + }); + it("localizes the reconciliation PR and explains a sync branch", () => { const target = { targetBranch: "develop", sourceBranch: "master", sourceSha: "c".repeat(40), syncBranch: "sync/release", status: "pending" as const }; const value = renderReconciliationPullRequest( diff --git a/src/application/policies/deployment_plan_policy.ts b/src/application/policies/deployment_plan_policy.ts index 4829bcf17..50320c49b 100644 --- a/src/application/policies/deployment_plan_policy.ts +++ b/src/application/policies/deployment_plan_policy.ts @@ -30,9 +30,13 @@ export interface InitialDeploymentOperationInput { readonly publicationWorkflow: string; } +export type InitialDeploymentOperation = DeploymentOperationSnapshot & { + readonly locale: LocaleProfile; +}; + export function buildInitialDeploymentOperation( input: InitialDeploymentOperationInput, -): DeploymentOperationSnapshot { +): InitialDeploymentOperation { const strategy = input.kind === "release" ? input.configuration.releaseReconciliationStrategy : input.configuration.hotfixReconciliationStrategy; diff --git a/src/application/services/deployment_orchestration_runtime.ts b/src/application/services/deployment_orchestration_runtime.ts index 4eeda73b4..0c85af356 100644 --- a/src/application/services/deployment_orchestration_runtime.ts +++ b/src/application/services/deployment_orchestration_runtime.ts @@ -67,7 +67,7 @@ export class DeploymentOrchestrationRuntime { await this.publishMilestone( context, blocked, - { kind: "reconciliation-blocked", reason: blocked.lastFailure?.message ?? message }, + { kind: "reconciliation-blocked", reason: message }, ); return new Result({ id: DEPLOYMENT_ORCHESTRATION_TASK_ID, diff --git a/src/application/usecases/__tests__/push_single_action_contexts.test.ts b/src/application/usecases/__tests__/push_single_action_contexts.test.ts index 46c07acfd..0438dc0ec 100644 --- a/src/application/usecases/__tests__/push_single_action_contexts.test.ts +++ b/src/application/usecases/__tests__/push_single_action_contexts.test.ts @@ -400,6 +400,21 @@ describe('push and single-action context projection', () => { } as unknown as DeploymentOrchestrationContext; const projected = projectDeploymentOrchestrationContext(raw); projected.currentConfiguration.releaseBranch = 'release/changed'; + const explicitConfiguration = projectDeploymentOrchestrationContext({ + ...raw, + agentConfiguration: { provider: 'codex', model: 'explicit-planner' }, + }); + const withoutConfiguration = projectDeploymentOrchestrationContext({ ...raw, ai: undefined }); + const withLocaleSnapshot = projectDeploymentOrchestrationContext({ + ...raw, + currentConfiguration: { + ...raw.currentConfiguration, + deploymentOrchestration: { + ...(operation as unknown as Record), + locale: { repository: 'en-US', issue: 'es-ES', pullRequest: 'de-DE' }, + } as never, + }, + }); const withoutOperation = projectDeploymentOrchestrationContext({ ...raw, currentConfiguration: { ...raw.currentConfiguration, deploymentOrchestration: undefined }, @@ -412,6 +427,11 @@ describe('push and single-action context projection', () => { expect(Object.isFrozen(projected.locale)).toBe(true); expect(projected.agentConfiguration).toEqual({ provider: 'codex', model: 'planner-model' }); expect(Object.isFrozen(projected.agentConfiguration)).toBe(true); + expect(explicitConfiguration.agentConfiguration).toEqual({ provider: 'codex', model: 'explicit-planner' }); + expect(withoutConfiguration).not.toHaveProperty('agentConfiguration'); + expect(withLocaleSnapshot.currentConfiguration.deploymentOrchestration?.locale) + .toEqual({ repository: 'en-US', issue: 'es-ES', pullRequest: 'de-DE' }); + expect(Object.isFrozen(withLocaleSnapshot.currentConfiguration.deploymentOrchestration?.locale)).toBe(true); expect(withoutOperation.currentConfiguration.deploymentOrchestration).toBeUndefined(); }); }); diff --git a/src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts b/src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts index 0a9e200a6..e5713ba81 100644 --- a/src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts +++ b/src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts @@ -3,6 +3,7 @@ import type { DeploymentOrchestrationContext } from "../../../ports/deployment_o import { buildInitialDeploymentOperation, validateInitialDeploymentInput, + type InitialDeploymentOperation, } from "../../../policies/deployment_plan_policy"; import { resumeBlockedDeployment, @@ -191,11 +192,11 @@ function selectOriginBranch( function validateOperation( context: DeploymentOrchestrationContext, - operation: DeploymentOperationSnapshot, + operation: InitialDeploymentOperation, ): void { const errors = validateInitialDeploymentInput({ operationId: operation.operationId, - locale: operation.locale ?? context.locale, + locale: operation.locale, kind: operation.kind, version: operation.version, title: operation.title, diff --git a/src/tooling/__tests__/validate_workflow_contract.test.ts b/src/tooling/__tests__/validate_workflow_contract.test.ts index 7ccbc5f02..fa593c30c 100644 --- a/src/tooling/__tests__/validate_workflow_contract.test.ts +++ b/src/tooling/__tests__/validate_workflow_contract.test.ts @@ -652,6 +652,8 @@ describe('workflow contract validator', () => { 'copilot_issue_comment.yml', 'copilot_pull_request.yml', 'copilot_pull_request_comment.yml', + 'release_workflow.yml', + 'hotfix_workflow.yml', ]) { const file = path.join(process.cwd(), directory, fileName); const workflow = yaml.load(readFileSync(file, 'utf8')) as MutationWorkflow; From 51b2cda4b683d81db5f1e2d5481fe456f6d0152f Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Mon, 14 Sep 2026 22:12:07 +0200 Subject: [PATCH 3/5] codex-deployment-localized-presentation: cover unsafe workflow links --- .../__tests__/deployment_presentation_policy.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/application/policies/__tests__/deployment_presentation_policy.test.ts b/src/application/policies/__tests__/deployment_presentation_policy.test.ts index 286fb0196..13a8b9c6b 100644 --- a/src/application/policies/__tests__/deployment_presentation_policy.test.ts +++ b/src/application/policies/__tests__/deployment_presentation_policy.test.ts @@ -247,6 +247,16 @@ describe("deployment presentation policy", () => { expect(body).toContain("/compare/"); }); + it("replaces an untrusted workflow URL with the safe GitHub root", () => { + const body = renderDeploymentDashboard(operation(), { + ...context, + workflowRunUrl: "https://example.com/@team\nunsafe", + }); + + expect(body).toContain("[Workflow run](https://github.com)"); + expect(body).not.toContain("example.com"); + }); + it("renders a pending external transition as a successful wait in the Job Summary", () => { const summary = renderDeploymentJobSummary(operation(), context, "preparing"); expect(summary).toContain("Waiting externally"); From 1cdc74c459f290aaeb12c456a40afaccdc7ede62 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Mon, 14 Sep 2026 22:18:49 +0200 Subject: [PATCH 4/5] codex-deployment-localized-presentation: sanitize blocked milestones --- .../services/deployment_orchestration_runtime.ts | 10 ++++++---- .../deployment_orchestration_use_case.test.ts | 12 +++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/application/services/deployment_orchestration_runtime.ts b/src/application/services/deployment_orchestration_runtime.ts index 0c85af356..a8f8ebcf2 100644 --- a/src/application/services/deployment_orchestration_runtime.ts +++ b/src/application/services/deployment_orchestration_runtime.ts @@ -24,6 +24,7 @@ import { } from "../policies/deployment_message_catalog"; import { blockDeploymentOperation, + sanitizeDeploymentMessage, type DeploymentOperationSnapshot, type ReconciliationTargetState, } from "../../domain/deployment_operation"; @@ -61,20 +62,21 @@ export class DeploymentOrchestrationRuntime { retryable: boolean, semanticError?: ApplicationError, ): Promise { - const blocked = blockDeploymentOperation(operation, category, message, retryable); + const sanitizedMessage = sanitizeDeploymentMessage(message); + const blocked = blockDeploymentOperation(operation, category, sanitizedMessage, retryable); await this.persist(context, blocked); await this.publishDashboard(context, blocked); await this.publishMilestone( context, blocked, - { kind: "reconciliation-blocked", reason: message }, + { kind: "reconciliation-blocked", reason: sanitizedMessage }, ); return new Result({ id: DEPLOYMENT_ORCHESTRATION_TASK_ID, success: false, executed: true, - steps: [message], - errors: [semanticError ?? new ApplicationError("workflow.failed", message, { retryable })], + steps: [sanitizedMessage], + errors: [semanticError ?? new ApplicationError("workflow.failed", sanitizedMessage, { retryable })], }); } diff --git a/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts b/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts index d134da431..7c8309b46 100644 --- a/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts @@ -640,7 +640,8 @@ describe("DeploymentOrchestrationUseCase", () => { it("records a publication workflow failure in durable state and the dashboard", async () => { const value = harness(); - const input = execution("failed", operation("publishing")); + const input = execution("failed", operation("publishing", { commentMode: "milestones" })); + input.singleAction.message = "::error:: @team "; const result = await value.useCase.invoke(input); expect(result[0].success).toBe(false); expect(input.currentConfiguration.deploymentOrchestration).toEqual(expect.objectContaining({ @@ -648,6 +649,15 @@ describe("DeploymentOrchestrationUseCase", () => { lastFailure: expect.objectContaining({ category: "publication", previousPhase: "publishing", retryable: true }), })); expect(value.presentation.createDashboard).toHaveBeenCalled(); + expect(value.presentation.publishMilestone).toHaveBeenCalledWith( + 355, + expect.stringContaining('name="reconciliation-blocked"'), + expect.stringContaining("﹕﹕error﹕﹕"), + ); + const milestone = value.presentation.publishMilestone.mock.calls[0]?.[2] as string; + expect(milestone).toContain("<!-- unsafe -->"); + expect(milestone).not.toContain(""); + expect(milestone).not.toContain("@team"); }); it("ignores a delayed failure report after completion", async () => { From c6862c5f85f584319ff1a46f1eab00d51ddae17a Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Mon, 14 Sep 2026 22:22:24 +0200 Subject: [PATCH 5/5] codex-deployment-localized-presentation: refresh generated bundles --- build/cli/index.js | 9 +++++---- build/github_action/index.js | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/build/cli/index.js b/build/cli/index.js index dd2c52b5a..88b79ff03 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -46702,16 +46702,17 @@ class DeploymentOrchestrationRuntime { await this.stateBoundary.persist(context); } async block(context, operation, category, message, retryable, semanticError) { - const blocked = (0, deployment_operation_1.blockDeploymentOperation)(operation, category, message, retryable); + const sanitizedMessage = (0, deployment_operation_1.sanitizeDeploymentMessage)(message); + const blocked = (0, deployment_operation_1.blockDeploymentOperation)(operation, category, sanitizedMessage, retryable); await this.persist(context, blocked); await this.publishDashboard(context, blocked); - await this.publishMilestone(context, blocked, { kind: "reconciliation-blocked", reason: message }); + await this.publishMilestone(context, blocked, { kind: "reconciliation-blocked", reason: sanitizedMessage }); return new result_1.Result({ id: exports.DEPLOYMENT_ORCHESTRATION_TASK_ID, success: false, executed: true, - steps: [message], - errors: [semanticError ?? new application_error_1.ApplicationError("workflow.failed", message, { retryable })], + steps: [sanitizedMessage], + errors: [semanticError ?? new application_error_1.ApplicationError("workflow.failed", sanitizedMessage, { retryable })], }); } async publishDashboard(context, operation) { diff --git a/build/github_action/index.js b/build/github_action/index.js index 30b3626f2..0cb006a99 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -47451,16 +47451,17 @@ class DeploymentOrchestrationRuntime { await this.stateBoundary.persist(context); } async block(context, operation, category, message, retryable, semanticError) { - const blocked = (0, deployment_operation_1.blockDeploymentOperation)(operation, category, message, retryable); + const sanitizedMessage = (0, deployment_operation_1.sanitizeDeploymentMessage)(message); + const blocked = (0, deployment_operation_1.blockDeploymentOperation)(operation, category, sanitizedMessage, retryable); await this.persist(context, blocked); await this.publishDashboard(context, blocked); - await this.publishMilestone(context, blocked, { kind: "reconciliation-blocked", reason: message }); + await this.publishMilestone(context, blocked, { kind: "reconciliation-blocked", reason: sanitizedMessage }); return new result_1.Result({ id: exports.DEPLOYMENT_ORCHESTRATION_TASK_ID, success: false, executed: true, - steps: [message], - errors: [semanticError ?? new application_error_1.ApplicationError("workflow.failed", message, { retryable })], + steps: [sanitizedMessage], + errors: [semanticError ?? new application_error_1.ApplicationError("workflow.failed", sanitizedMessage, { retryable })], }); } async publishDashboard(context, operation) {