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..88b79ff03 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,27 +46695,30 @@ class DeploymentOrchestrationRuntime {
constructor(dependencies, stateBoundary) {
this.dependencies = dependencies;
this.stateBoundary = stateBoundary;
+ this.presentationCatalogs = new Map();
}
async persist(context, operation) {
context.currentConfiguration.deploymentOrchestration = operation;
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, "reconciliation-blocked", `❌ Deployment blocked: ${blocked.lastFailure?.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) {
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 +46726,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 +46749,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 +46777,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 +46791,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 +46843,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 +46857,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 +46939,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 +47611,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 +47684,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 +47885,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 +47943,7 @@ function selectOriginBranch(context, kind) {
function validateOperation(context, operation) {
const errors = (0, deployment_plan_policy_1.validateInitialDeploymentInput)({
operationId: operation.operationId,
+ locale: operation.locale,
kind: operation.kind,
version: operation.version,
title: operation.title,
@@ -47772,7 +48084,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 +51621,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 +51815,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 +72622,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 +72705,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 +73057,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 +73127,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 +73474,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 +75331,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 +75350,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..0cb006a99 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,27 +47444,30 @@ class DeploymentOrchestrationRuntime {
constructor(dependencies, stateBoundary) {
this.dependencies = dependencies;
this.stateBoundary = stateBoundary;
+ this.presentationCatalogs = new Map();
}
async persist(context, operation) {
context.currentConfiguration.deploymentOrchestration = operation;
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, "reconciliation-blocked", `❌ Deployment blocked: ${blocked.lastFailure?.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) {
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 +47475,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 +47498,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 +47526,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 +47540,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 +47592,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 +47606,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 +47688,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 +48360,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 +48433,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 +48634,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 +48692,7 @@ function selectOriginBranch(context, kind) {
function validateOperation(context, operation) {
const errors = (0, deployment_plan_policy_1.validateInitialDeploymentInput)({
operationId: operation.operationId,
+ locale: operation.locale,
kind: operation.kind,
version: operation.version,
title: operation.title,
@@ -48487,7 +48833,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 +52511,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 +52705,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 +70960,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 +71043,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 +71395,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 +71465,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 +71812,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 +73483,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 +73502,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..fe076ef73 100644
--- a/docs/issues/deployment-orchestration.mdx
+++ b/docs/issues/deployment-orchestration.mdx
@@ -99,6 +99,44 @@ 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 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
+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/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/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..af18bdf65 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,25 @@ 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. 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.
### 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..8f88389f1 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,113 @@ 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('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/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..13a8b9c6b 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", () => {
@@ -181,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(
@@ -215,31 +247,67 @@ 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", ["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..50320c49b 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;
@@ -28,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;
@@ -38,6 +44,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..a8f8ebcf2 100644
--- a/src/application/services/deployment_orchestration_runtime.ts
+++ b/src/application/services/deployment_orchestration_runtime.ts
@@ -12,12 +12,19 @@ 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,
+ sanitizeDeploymentMessage,
type DeploymentOperationSnapshot,
type ReconciliationTargetState,
} from "../../domain/deployment_operation";
@@ -32,6 +39,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,
@@ -53,21 +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,
- "reconciliation-blocked",
- `❌ Deployment blocked: ${blocked.lastFailure?.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 })],
});
}
@@ -76,7 +85,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 +108,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 +143,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 +206,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 +220,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 +304,7 @@ export class DeploymentOrchestrationRuntime {
private unsupportedMergeBehavior(
context: DeploymentOrchestrationContext,
+ operation: DeploymentOperationSnapshot,
targetRole: MergeQueueTargetRole,
targetBranch: string,
capabilities: TargetMergeCapabilities,
@@ -311,10 +323,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 +443,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..0438dc0ec 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: {
@@ -399,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 },
@@ -407,6 +423,15 @@ 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(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/__tests__/deployment_orchestration_use_case.test.ts b/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts
index 0bc09d839..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
@@ -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));
@@ -590,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({
@@ -598,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 () => {
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..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,
@@ -103,6 +104,7 @@ export class PreparePromotionHandler {
);
const operation = buildInitialDeploymentOperation({
operationId: this.runtime.dependencies.operationId(),
+ locale: context.locale,
kind,
version: context.singleAction.version,
title: context.singleAction.title,
@@ -190,10 +192,11 @@ function selectOriginBranch(
function validateOperation(
context: DeploymentOrchestrationContext,
- operation: DeploymentOperationSnapshot,
+ operation: InitialDeploymentOperation,
): void {
const errors = validateInitialDeploymentInput({
operationId: operation.operationId,
+ locale: operation.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,
});
}
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;