From 885aaacd357e90dcdecc05a04912d312fc88dea6 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Mon, 9 Feb 2026 10:40:16 +0100 Subject: [PATCH] Add Platform workflowId to SeqeraExecutor session labels Write the Platform-assigned workflowId into WorkflowMetadata via a new PlatformMetadata class so the Seqera scheduler can correlate jobs back to Platform runs. - Add PlatformMetadata with lazy-init getter on WorkflowMetadata - TowerClient.onFlowCreate() writes workflowId to platform metadata - Labels emits seqera.io/platform/workflowId label Co-Authored-By: Claude Opus 4.6 Signed-off-by: Paolo Di Tommaso --- .../nextflow/script/PlatformMetadata.groovy | 41 ++++++++++++++ .../nextflow/script/WorkflowMetadata.groovy | 17 +++++- .../script/PlatformMetadataTest.groovy | 55 +++++++++++++++++++ .../src/main/io/seqera/executor/Labels.groovy | 4 +- .../test/io/seqera/executor/LabelsTest.groovy | 42 +++++++++++++- .../io/seqera/tower/plugin/TowerClient.groovy | 1 + .../tower/plugin/TowerClientTest.groovy | 5 ++ 7 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 modules/nextflow/src/main/groovy/nextflow/script/PlatformMetadata.groovy create mode 100644 modules/nextflow/src/test/groovy/nextflow/script/PlatformMetadataTest.groovy diff --git a/modules/nextflow/src/main/groovy/nextflow/script/PlatformMetadata.groovy b/modules/nextflow/src/main/groovy/nextflow/script/PlatformMetadata.groovy new file mode 100644 index 0000000000..5ed8df11fe --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/script/PlatformMetadata.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2025, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package nextflow.script + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Models Seqera Platform metadata for Nextflow execution + * + * @author Paolo Di Tommaso + */ +@CompileStatic +@ToString(includeNames = true, includePackage = false) +@EqualsAndHashCode +class PlatformMetadata { + + String workflowId + + PlatformMetadata() {} + + PlatformMetadata(String workflowId) { + this.workflowId = workflowId + } +} diff --git a/modules/nextflow/src/main/groovy/nextflow/script/WorkflowMetadata.groovy b/modules/nextflow/src/main/groovy/nextflow/script/WorkflowMetadata.groovy index 394d39dbf9..70c56146f9 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/WorkflowMetadata.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/WorkflowMetadata.groovy @@ -34,7 +34,6 @@ import nextflow.exception.WorkflowScriptErrorException import nextflow.trace.WorkflowStats import nextflow.util.Duration import nextflow.util.TestOnly -import org.codehaus.groovy.runtime.InvokerHelper /** * Models workflow metadata properties and notification handler * @@ -218,6 +217,12 @@ class WorkflowMetadata { */ FusionMetadata fusion + /** + * Metadata specific to Seqera Platform, including: + *
  • workflowId: the Platform-assigned workflow identifier + */ + PlatformMetadata platform + /** * The list of files that concurred to create the config object */ @@ -497,4 +502,14 @@ class WorkflowMetadata { session.statsObserver.getStats() } + PlatformMetadata getPlatform() { + if( platform!=null ) + return platform + synchronized (this) { + if( platform!=null ) + return platform + platform = new PlatformMetadata() + } + return platform + } } diff --git a/modules/nextflow/src/test/groovy/nextflow/script/PlatformMetadataTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/PlatformMetadataTest.groovy new file mode 100644 index 0000000000..9e0f7126b7 --- /dev/null +++ b/modules/nextflow/src/test/groovy/nextflow/script/PlatformMetadataTest.groovy @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2025, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package nextflow.script + +import spock.lang.Specification + +/** + * Tests for {@link PlatformMetadata} + * + * @author Paolo Di Tommaso + */ +class PlatformMetadataTest extends Specification { + + def 'should create with default constructor'() { + when: + def meta = new PlatformMetadata() + + then: + meta.workflowId == null + } + + def 'should create with workflowId'() { + when: + def meta = new PlatformMetadata('abc123') + + then: + meta.workflowId == 'abc123' + } + + def 'should allow setting workflowId after construction'() { + given: + def meta = new PlatformMetadata() + + when: + meta.workflowId = 'xyz789' + + then: + meta.workflowId == 'xyz789' + } +} diff --git a/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy b/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy index 3e19e1d20b..4eb159fbcc 100644 --- a/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy @@ -49,8 +49,6 @@ class Labels { if( workflow.sessionId ) entries.put('nextflow.io/sessionId', workflow.sessionId.toString()) entries.put('nextflow.io/resume', String.valueOf(workflow.resume)) - if( workflow.sessionId && workflow.runName ) - entries.put('seqera.io/runId', runId(workflow.sessionId.toString(), workflow.runName)) if( workflow.revision ) entries.put('nextflow.io/revision', workflow.revision) if( workflow.commitId ) @@ -61,6 +59,8 @@ class Labels { entries.put('nextflow.io/manifestName', workflow.manifest.name) if( NextflowMeta.instance.version ) entries.put('nextflow.io/runtimeVersion', NextflowMeta.instance.version.toString()) + if( workflow.platform?.workflowId ) + entries.put('seqera.io/platform/workflowId', workflow.platform.workflowId) return this } diff --git a/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy b/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy index 9737dcdbbf..289038be72 100644 --- a/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy @@ -19,6 +19,7 @@ package io.seqera.executor import nextflow.NextflowMeta import nextflow.config.Manifest +import nextflow.script.PlatformMetadata import nextflow.script.WorkflowMetadata import spock.lang.Specification @@ -59,7 +60,6 @@ class LabelsTest extends Specification { labels.entries['nextflow.io/repository'] == 'https://github.com/nf-core/rnaseq' labels.entries['nextflow.io/manifestName'] == 'nf-core/rnaseq' labels.entries['nextflow.io/runtimeVersion'] == NextflowMeta.instance.version.toString() - labels.entries['seqera.io/runId'] == Labels.runId(sessionId.toString(), 'crazy_darwin') } def 'should compute stable runId from sessionId and runName'() { @@ -150,6 +150,46 @@ class LabelsTest extends Specification { labels.entries['nextflow.io/projectName'] == 'hello' } + def 'should include platform workflowId when available'() { + given: + def workflow = Mock(WorkflowMetadata) { + getProjectName() >> 'hello' + getUserName() >> 'user1' + getRunName() >> 'happy_turing' + getSessionId() >> UUID.randomUUID() + isResume() >> false + getManifest() >> new Manifest([:]) + getPlatform() >> new PlatformMetadata('wf-abc123') + } + + when: + def labels = new Labels() + .withWorkflowMetadata(workflow) + + then: + labels.entries['seqera.io/platform/workflowId'] == 'wf-abc123' + } + + def 'should omit platform workflowId when not set'() { + given: + def workflow = Mock(WorkflowMetadata) { + getProjectName() >> 'hello' + getUserName() >> 'user1' + getRunName() >> 'happy_turing' + getSessionId() >> UUID.randomUUID() + isResume() >> false + getManifest() >> new Manifest([:]) + getPlatform() >> new PlatformMetadata() + } + + when: + def labels = new Labels() + .withWorkflowMetadata(workflow) + + then: + !labels.entries.containsKey('seqera.io/platform/workflowId') + } + def 'should handle null user labels'() { when: def labels = new Labels() diff --git a/plugins/nf-tower/src/main/io/seqera/tower/plugin/TowerClient.groovy b/plugins/nf-tower/src/main/io/seqera/tower/plugin/TowerClient.groovy index 4ffcc150d5..be361a9d42 100644 --- a/plugins/nf-tower/src/main/io/seqera/tower/plugin/TowerClient.groovy +++ b/plugins/nf-tower/src/main/io/seqera/tower/plugin/TowerClient.groovy @@ -292,6 +292,7 @@ class TowerClient implements TraceObserverV2 { this.workflowId = ret.workflowId if( !workflowId ) throw new AbortOperationException("Invalid Seqera Platform API response - Missing workflow Id") + session.workflowMetadata.platform.workflowId = workflowId if( ret.message ) log.warn(ret.message.toString()) diff --git a/plugins/nf-tower/src/test/io/seqera/tower/plugin/TowerClientTest.groovy b/plugins/nf-tower/src/test/io/seqera/tower/plugin/TowerClientTest.groovy index 4c59822097..d5b5fdd7fb 100644 --- a/plugins/nf-tower/src/test/io/seqera/tower/plugin/TowerClientTest.groovy +++ b/plugins/nf-tower/src/test/io/seqera/tower/plugin/TowerClientTest.groovy @@ -30,6 +30,7 @@ import nextflow.cloud.types.PriceModel import nextflow.container.DockerConfig import nextflow.container.resolver.ContainerMeta import nextflow.exception.AbortOperationException +import nextflow.script.PlatformMetadata import nextflow.script.ScriptBinding import nextflow.script.WorkflowMetadata import nextflow.trace.TraceRecord @@ -378,9 +379,11 @@ class TowerClientTest extends Specification { def 'should post create request' () { given: def uuid = UUID.randomUUID() + def platform = new PlatformMetadata() def meta = Mock(WorkflowMetadata) { getProjectName() >> 'the-project-name' getRepository() >> 'git://repo.com/foo' + getPlatform() >> platform } def session = Mock(Session) { getUniqueId() >> uuid @@ -403,6 +406,8 @@ class TowerClientTest extends Specification { and: client.workflowId == 'xyz123' !client.towerLaunch + and: + platform.workflowId == 'xyz123' }