From f044bc863f1ee45a487edb74a943899902763a25 Mon Sep 17 00:00:00 2001 From: Mayursinh Sarvaiya Date: Fri, 21 Aug 2026 18:31:57 +0530 Subject: [PATCH 1/2] fix(ui): show a failed promotion step's own error message The alert under a failed step read promotion.status.message, which keeps changing as later steps execute and, for a continueOnError step, never reflects that step at all. Read status.stepExecutionMetadata[i].message instead, so each failed step shows the error that actually caused it. The promotion-level message keeps its place after the last step, shown only when no step surfaced an error of its own -- which is how the abort, pre-step and workdir failures stay visible. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Mayursinh Sarvaiya --- ui/src/features/stage/promotion-steps.tsx | 46 +++++++++++++++-------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/ui/src/features/stage/promotion-steps.tsx b/ui/src/features/stage/promotion-steps.tsx index 1fb40d593b..f89979734d 100644 --- a/ui/src/features/stage/promotion-steps.tsx +++ b/ui/src/features/stage/promotion-steps.tsx @@ -34,22 +34,21 @@ export const PromotionSteps = (props: PromotionStepsProps) => { const phase = getPromotionStatusPhase(props.promotion); + const message = props.promotion?.status?.message; + + // A failed step's message becomes the Promotion's, so it is already on screen. + const shownUnderStep = (props.promotion.status?.stepExecutionMetadata ?? []).some( + (meta, i) => isFailedStep(i, props.promotion.status) && !!meta.message + ); + const shouldShowMessage = isPromotionPhaseTerminal(phase) && phase !== PromotionStatusPhase.SUCCEEDED && - phase !== PromotionStatusPhase.ERRORED && // because its already handled at individual step level - !!props.promotion?.status?.message; + !!message && + !shownUnderStep; const steps = props.promotion?.spec?.steps ?? []; - const errorItem = { - key: 'error', - label: , - showArrow: false, - collapsible: 'disabled' as const, - styles: { header: { paddingTop: 0 } } - }; - // Steps with a registered extension are interactive const hasExtension = (step: (typeof steps)[number]) => promotionStepExtensions.some((ext) => ext.identifier === step.uses); @@ -76,9 +75,26 @@ export const PromotionSteps = (props: PromotionStepsProps) => { key }; - return isFailedStep(i, props.promotion.status) - ? [{ ...item, className: `${item.className || ''} !border-none` }, errorItem] - : [item]; + if (!isFailedStep(i, props.promotion.status)) { + return [item]; + } + + const stepMessage = props.promotion.status?.stepExecutionMetadata?.[i]?.message; + + if (!stepMessage) { + return [item]; + } + + return [ + { ...item, className: `${item.className || ''} !border-none` }, + { + key: `${key}-error`, + label: , + showArrow: false, + collapsible: 'disabled' as const, + styles: { header: { paddingTop: 0 } } + } + ]; }); useEffect(() => { @@ -97,9 +113,7 @@ export const PromotionSteps = (props: PromotionStepsProps) => { activeKey={activeKeys} onChange={(keys) => setActiveKeys(typeof keys === 'string' ? [keys] : keys)} /> - {shouldShowMessage && ( - - )} + {shouldShowMessage && } ); }; From 9cc9c0cca80201e90abadb5e2f61b7623cc95f92 Mon Sep 17 00:00:00 2001 From: Mayursinh Sarvaiya Date: Tue, 25 Aug 2026 10:30:01 +0530 Subject: [PATCH 2/2] fix(ui): always show an aborted Promotion's message Split the promotion-level message visibility check into per-phase cases. Failed and Errored keep suppressing the message when the failing step already displays it, but an abort is not attributable to any one step, so its message is always shown. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Mayursinh Sarvaiya --- ui/src/features/stage/promotion-steps.tsx | 30 +++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/ui/src/features/stage/promotion-steps.tsx b/ui/src/features/stage/promotion-steps.tsx index f89979734d..9ab9016d96 100644 --- a/ui/src/features/stage/promotion-steps.tsx +++ b/ui/src/features/stage/promotion-steps.tsx @@ -37,15 +37,25 @@ export const PromotionSteps = (props: PromotionStepsProps) => { const message = props.promotion?.status?.message; // A failed step's message becomes the Promotion's, so it is already on screen. - const shownUnderStep = (props.promotion.status?.stepExecutionMetadata ?? []).some( - (meta, i) => isFailedStep(i, props.promotion.status) && !!meta.message - ); - - const shouldShowMessage = - isPromotionPhaseTerminal(phase) && - phase !== PromotionStatusPhase.SUCCEEDED && - !!message && - !shownUnderStep; + const hasIndividualPromotionStepTerminalMessage = ( + props.promotion.status?.stepExecutionMetadata ?? [] + ).some((meta, i) => isFailedStep(i, props.promotion.status) && !!meta.message); + + let shouldShowMessage = false; + + if (isPromotionPhaseTerminal(phase)) { + switch (phase) { + case PromotionStatusPhase.FAILED: + case PromotionStatusPhase.ERRORED: + // The failing step already shows this message, so don't repeat it. + shouldShowMessage = !hasIndividualPromotionStepTerminalMessage; + break; + case PromotionStatusPhase.ABORTED: + // An abort is not attributable to any one step. + shouldShowMessage = true; + break; + } + } const steps = props.promotion?.spec?.steps ?? []; @@ -113,7 +123,7 @@ export const PromotionSteps = (props: PromotionStepsProps) => { activeKey={activeKeys} onChange={(keys) => setActiveKeys(typeof keys === 'string' ? [keys] : keys)} /> - {shouldShowMessage && } + {shouldShowMessage && !!message && } ); };