Skip to content

Commit 5d59206

Browse files
fix: show single success emoji in completion notifications (#70)
## Summary - Remove the duplicate ✅ from the default **Status** field on pipeline success notifications - Keep the checkmark in the message header (`✅ *Pipeline completed successfully*`) so success is still visually indicated once in the message body - Applies to both the default complete message and map-based custom messages that include the `status` field Closes #66 ## Test plan - [x] `./gradlew test --tests "nextflow.slack.SlackMessageBuilderTest.should build workflow complete message"` - [x] `./gradlew test --tests "nextflow.slack.SlackMessageBuilderTest.should use map-based custom complete message with selective fields"` - [x] `./gradlew test --tests "nextflow.slack.SlackMessageBuilderTest.should use custom complete message template"` Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 54f0bd2 commit 5d59206

2 files changed

Lines changed: 100 additions & 8 deletions

File tree

src/main/groovy/nextflow/slack/SlackMessageBuilder.groovy

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ class SlackMessageBuilder {
311311
fields << createMarkdownField('Duration', duration.toString())
312312
}
313313
if (shouldIncludeField(includeFields, 'status')) {
314-
fields << createMarkdownField('Status', '✅ Success')
314+
fields << createMarkdownField('Status', getStatusText('completed'))
315315
}
316316

317317
// Add resource usage if configured (via includeResourceUsage or includeFields)
@@ -373,7 +373,7 @@ class SlackMessageBuilder {
373373
fields << createMarkdownField('Duration', duration.toString())
374374
}
375375
if (shouldIncludeField(includeFields, 'status')) {
376-
fields << createMarkdownField('Status', '❌ Failed')
376+
fields << createMarkdownField('Status', getStatusText('failed'))
377377
}
378378

379379
if (shouldIncludeField(includeFields, 'failedProcess') && errorRecord) {
@@ -487,7 +487,7 @@ class SlackMessageBuilder {
487487
fields << createMarkdownField('Duration', duration.toString())
488488
}
489489
if (includeFields.contains('status')) {
490-
fields << createMarkdownField('Status', getStatusEmoji(status))
490+
fields << createMarkdownField('Status', getStatusText(status))
491491
}
492492
if (includeFields.contains('failedProcess') && errorRecord) {
493493
def processName = errorRecord.get('process')
@@ -566,16 +566,16 @@ class SlackMessageBuilder {
566566
}
567567

568568
/**
569-
* Get status emoji
569+
* Get status text for the Status field (emoji-free; header carries the emoji).
570570
*/
571-
private static String getStatusEmoji(String status) {
571+
private static String getStatusText(String status) {
572572
switch (status) {
573573
case 'started':
574-
return '🚀 Running'
574+
return 'Running'
575575
case 'completed':
576-
return 'Success'
576+
return 'Success'
577577
case 'failed':
578-
return 'Failed'
578+
return 'Failed'
579579
default:
580580
return 'Unknown'
581581
}

src/test/groovy/nextflow/slack/SlackMessageBuilderTest.groovy

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,4 +879,96 @@ class SlackMessageBuilderTest extends Specification {
879879
!fields.find { it.text.contains('Duration') }
880880
!fields.find { it.text.contains('Status') }
881881
}
882+
883+
def 'status field should be text-only in default complete message'() {
884+
given:
885+
def metadata = Mock(WorkflowMetadata)
886+
metadata.scriptName >> 'test-workflow.nf'
887+
metadata.duration >> Duration.of('1h')
888+
metadata.stats >> null
889+
session.workflowMetadata >> metadata
890+
891+
when:
892+
def message = messageBuilder.buildWorkflowCompleteMessage()
893+
def json = new JsonSlurper().parseText(message)
894+
895+
then:
896+
def headerBlock = json.blocks.find { it.type == 'section' && it.text?.text?.contains('Pipeline completed successfully') }
897+
headerBlock.text.text.contains('')
898+
statusFieldValue(json) == 'Success'
899+
!statusFieldValue(json).contains('')
900+
}
901+
902+
def 'status field should be text-only in default error message'() {
903+
given:
904+
def errorSession = Mock(Session)
905+
def metadata = Mock(WorkflowMetadata)
906+
metadata.scriptName >> 'test-workflow.nf'
907+
metadata.duration >> Duration.of('30m')
908+
metadata.errorMessage >> 'Process failed'
909+
errorSession.workflowMetadata >> metadata
910+
errorSession.runName >> 'test-run'
911+
def builder = new SlackMessageBuilder(config, errorSession)
912+
913+
when:
914+
def message = builder.buildWorkflowErrorMessage(null)
915+
def json = new JsonSlurper().parseText(message)
916+
917+
then:
918+
def headerBlock = json.blocks.find { it.type == 'section' && it.text?.text?.contains('Pipeline failed') }
919+
headerBlock.text.text.contains('')
920+
statusFieldValue(json) == 'Failed'
921+
!statusFieldValue(json).contains('')
922+
}
923+
924+
def 'status field should be text-only in map-based custom messages'() {
925+
given:
926+
def metadata = Mock(WorkflowMetadata)
927+
metadata.scriptName >> 'test-workflow.nf'
928+
metadata.duration >> Duration.of('1h')
929+
session.workflowMetadata >> metadata
930+
931+
def startConfig = new SlackConfig([
932+
enabled: true,
933+
webhook: 'https://hooks.slack.com/services/TEST/TEST/TEST',
934+
onStart: [
935+
enabled: true,
936+
message: [text: '🚀 *Starting*', includeFields: ['status']]
937+
]
938+
])
939+
def completeConfig = new SlackConfig([
940+
enabled: true,
941+
webhook: 'https://hooks.slack.com/services/TEST/TEST/TEST',
942+
onComplete: [
943+
enabled: true,
944+
message: [text: '✅ *Done*', includeFields: ['status']]
945+
]
946+
])
947+
def errorConfig = new SlackConfig([
948+
enabled: true,
949+
webhook: 'https://hooks.slack.com/services/TEST/TEST/TEST',
950+
onError: [
951+
enabled: true,
952+
message: [text: '❌ *Boom*', includeFields: ['status']]
953+
]
954+
])
955+
def errorSession = Mock(Session)
956+
def errorMetadata = Mock(WorkflowMetadata)
957+
errorMetadata.scriptName >> 'test-workflow.nf'
958+
errorMetadata.duration >> Duration.of('30m')
959+
errorMetadata.errorMessage >> 'failed'
960+
errorSession.workflowMetadata >> errorMetadata
961+
errorSession.runName >> 'test-run'
962+
963+
expect:
964+
statusFieldValue(new JsonSlurper().parseText(new SlackMessageBuilder(startConfig, session).buildWorkflowStartMessage())) == 'Running'
965+
statusFieldValue(new JsonSlurper().parseText(new SlackMessageBuilder(completeConfig, session).buildWorkflowCompleteMessage())) == 'Success'
966+
statusFieldValue(new JsonSlurper().parseText(new SlackMessageBuilder(errorConfig, errorSession).buildWorkflowErrorMessage(null))) == 'Failed'
967+
}
968+
969+
private static String statusFieldValue(json) {
970+
def fieldsBlock = json.blocks.find { it.type == 'section' && it.fields }
971+
def statusField = fieldsBlock?.fields?.find { it.text.startsWith('*Status*') }
972+
return statusField?.text?.replace('*Status*\n', '') ?: ''
973+
}
882974
}

0 commit comments

Comments
 (0)