Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5c881c5
docs: add design for Seqera executor resourceLabels support
pditommaso Apr 17, 2026
c37151d
docs: add implementation plan for Seqera executor resourceLabels
pditommaso Apr 17, 2026
21bef93
build(nf-seqera): bump sched-client to 0.51.0 via includeBuild
pditommaso Apr 17, 2026
1c79e67
build: keep sched includeBuild commented for CI safety
pditommaso Apr 17, 2026
1d7f81e
feat(nf-seqera): add Labels.withProcessResourceLabels
pditommaso Apr 17, 2026
3117e9e
feat(nf-seqera): add Labels.toStringMap and Labels.delta helpers
pditommaso Apr 17, 2026
d53dfef
refactor(nf-seqera)!: remove seqera.executor.labels in favour of proc…
pditommaso Apr 17, 2026
eebebf1
feat(nf-seqera): attach process.resourceLabels to Sched run labels
pditommaso Apr 17, 2026
e569cc9
feat(nf-seqera): send per-task resourceLabels delta on Sched task
pditommaso Apr 17, 2026
03658b6
docs(nf-seqera): document resourceLabels support and bump to 0.18.0
pditommaso Apr 17, 2026
a7d3a1c
test(nf-seqera): cover createRun labels and tighten submit-test mocks
pditommaso Apr 17, 2026
6273bd2
build(nf-seqera): bump sched-client to 0.52.0-SNAPSHOT
pditommaso Apr 17, 2026
f496f01
Minor change [ci fast]
pditommaso Apr 19, 2026
0129976
Minor change [ci skip]
pditommaso Apr 19, 2026
29a4340
refactor(nf-seqera): defensive check and immutable run labels [ci fast]
pditommaso Apr 19, 2026
171e140
feat(nf-seqera): filter autoLabels to selected workflow-metadata fiel…
pditommaso Apr 19, 2026
6b3d007
Merge branch 'master' into feat/seqera-auto-labels-filter [ci fast]
pditommaso Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ import nextflow.util.Duration
@CompileStatic
class ExecutorOpts implements ConfigScope {

static final Set<String> VALID_AUTO_LABELS = Collections.unmodifiableSet(new LinkedHashSet<>([
'projectName', 'userName', 'runName', 'sessionId', 'resume',
'revision', 'commitId', 'repository', 'manifestName',
'runtimeVersion', 'workflowId'
]))

final RetryOpts retryPolicy

@ConfigOption
Expand Down Expand Up @@ -73,10 +79,17 @@ class ExecutorOpts implements ConfigScope {

@ConfigOption
@Description("""
When `true`, automatically adds workflow metadata labels (e.g. project name,
run name, session ID) with the `nextflow.io/` prefix to the session (default: `false`).
Automatically attach workflow metadata labels (with the `nextflow.io/` and
`seqera.io/platform/` prefixes) to the session. Accepts:
- `true`: include all available metadata labels
- `false` (default): disable
- a list or comma-separated string of short names: e.g.
`['runName', 'projectName']` or `'runName,projectName'`
Valid names: `projectName`, `userName`, `runName`, `sessionId`, `resume`,
`revision`, `commitId`, `repository`, `manifestName`, `runtimeVersion`,
`workflowId`.
""")
final boolean autoLabels
final Set<String> autoLabels

@ConfigOption
@Description("""
Expand Down Expand Up @@ -119,7 +132,7 @@ class ExecutorOpts implements ConfigScope {
: Duration.of('1 sec')
// machine requirement settings
this.machineRequirement = new MachineRequirementOpts(opts.machineRequirement as Map ?: Map.of())
this.autoLabels = opts.autoLabels as boolean ?: false
this.autoLabels = parseAutoLabels(opts.get('autoLabels'))
// prediction model
this.predictionModel = opts.predictionModel as String ?: null
// custom task environment variables
Expand Down Expand Up @@ -156,10 +169,28 @@ class ExecutorOpts implements ConfigScope {
return machineRequirement
}

boolean getAutoLabels() {
Set<String> getAutoLabels() {
return autoLabels
}

protected static Set<String> parseAutoLabels(Object value) {
if( value == null || value == false )
return Collections.<String>emptySet()
if( value == true )
return VALID_AUTO_LABELS
List<String> raw
if( value instanceof CharSequence )
raw = value.toString().tokenize(',').collect { String s -> s.trim() }.findAll { String s -> s }
else if( value instanceof List )
raw = ((List) value).collect { it?.toString()?.trim() }.findAll { String s -> s } as List<String>
else
throw new IllegalArgumentException("Invalid 'seqera.executor.autoLabels' value '${value}' - expected true, false, a list, or a comma-separated string")
final invalid = raw.findAll { String s -> !(s in VALID_AUTO_LABELS) }
if( invalid )
throw new IllegalArgumentException("Invalid 'seqera.executor.autoLabels' name(s) ${invalid} - valid names are: ${VALID_AUTO_LABELS.join(', ')}")
return Collections.unmodifiableSet(new LinkedHashSet<>(raw))
}

String getPredictionModel() {
return predictionModel
}
Expand Down
42 changes: 30 additions & 12 deletions plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -33,32 +33,50 @@ import nextflow.script.WorkflowMetadata
@CompileStatic
class Labels {

static final Set<String> ALL_AUTO_LABELS = Collections.unmodifiableSet(new LinkedHashSet<>([
'projectName', 'userName', 'runName', 'sessionId', 'resume',
'revision', 'commitId', 'repository', 'manifestName',
'runtimeVersion', 'workflowId'
]))

private final Map<String,String> entries = new LinkedHashMap<>(20)

/**
* Add {@code nextflow.io/*} labels from workflow metadata
* Add all {@code nextflow.io/*} and {@code seqera.io/platform/*} labels
* derived from workflow metadata.
*/
Labels withWorkflowMetadata(WorkflowMetadata workflow) {
if( workflow.projectName )
return withWorkflowMetadata(workflow, ALL_AUTO_LABELS)
}

/**
* Add workflow metadata labels filtered by the {@code include} set of
* short names (e.g. {@code 'runName'}). Unknown names are ignored; the
* caller is expected to validate membership upstream.
*/
Labels withWorkflowMetadata(WorkflowMetadata workflow, Set<String> include) {
if( !include ) return this
if( include.contains('projectName') && workflow.projectName )
entries.put('nextflow.io/projectName', workflow.projectName)
if( workflow.userName )
if( include.contains('userName') && workflow.userName )
entries.put('nextflow.io/userName', workflow.userName)
if( workflow.runName )
if( include.contains('runName') && workflow.runName )
entries.put('nextflow.io/runName', workflow.runName)
if( workflow.sessionId )
if( include.contains('sessionId') && workflow.sessionId )
entries.put('nextflow.io/sessionId', workflow.sessionId.toString())
entries.put('nextflow.io/resume', String.valueOf(workflow.resume))
if( workflow.revision )
if( include.contains('resume') )
entries.put('nextflow.io/resume', String.valueOf(workflow.resume))
if( include.contains('revision') && workflow.revision )
entries.put('nextflow.io/revision', workflow.revision)
if( workflow.commitId )
if( include.contains('commitId') && workflow.commitId )
entries.put('nextflow.io/commitId', workflow.commitId)
if( workflow.repository )
if( include.contains('repository') && workflow.repository )
entries.put('nextflow.io/repository', workflow.repository)
if( workflow.manifest?.name )
if( include.contains('manifestName') && workflow.manifest?.name )
entries.put('nextflow.io/manifestName', workflow.manifest.name)
if( NextflowMeta.instance.version )
if( include.contains('runtimeVersion') && NextflowMeta.instance.version )
entries.put('nextflow.io/runtimeVersion', NextflowMeta.instance.version.toString())
if( workflow.platform?.workflowId )
if( include.contains('workflowId') && workflow.platform?.workflowId )
entries.put('seqera.io/platform/workflowId', workflow.platform.workflowId)
return this
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class SeqeraExecutor extends Executor implements ExtensionPoint {
computeRunResourceLabels()
final labels = new Labels()
if( seqeraConfig.autoLabels )
labels.withWorkflowMetadata(session.workflowMetadata)
labels.withWorkflowMetadata(session.workflowMetadata, seqeraConfig.autoLabels)
labels.withProcessResourceLabels(runResourceLabels)
final predictionModel = seqeraConfig.predictionModel ? PredictionModel.fromValue(seqeraConfig.predictionModel) : null
final pipeline = new PipelineSpec()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,106 @@ class ExecutorOptsTest extends Specification {
config.machineRequirement.provisioning == 'spot'
}

def 'should enable auto labels' () {
def 'should enable all auto labels when set to true' () {
when:
def config = new ExecutorOpts([
endpoint: 'https://sched.example.com',
autoLabels: true
])

then:
config.autoLabels
config.autoLabels == ExecutorOpts.VALID_AUTO_LABELS
}

def 'should disable auto labels when set to false' () {
when:
def config = new ExecutorOpts([
endpoint: 'https://sched.example.com',
autoLabels: false
])

then:
config.autoLabels.isEmpty()
}

def 'should accept auto labels as a list of short names' () {
when:
def config = new ExecutorOpts([
endpoint: 'https://sched.example.com',
autoLabels: ['runName', 'projectName']
])

then:
config.autoLabels == ['runName', 'projectName'] as Set
}

def 'should trim whitespace in auto labels list entries' () {
when:
def config = new ExecutorOpts([
endpoint: 'https://sched.example.com',
autoLabels: [' runName', 'projectName ']
])

then:
config.autoLabels == ['runName', 'projectName'] as Set
}

def 'should accept auto labels as a comma-separated string' () {
when:
def config = new ExecutorOpts([
endpoint: 'https://sched.example.com',
autoLabels: 'runName,projectName,workflowId'
])

then:
config.autoLabels == ['runName', 'projectName', 'workflowId'] as Set
}

def 'should tolerate whitespace around comma-separated auto labels' () {
when:
def config = new ExecutorOpts([
endpoint: 'https://sched.example.com',
autoLabels: 'runName, projectName ,workflowId'
])

then:
config.autoLabels == ['runName', 'projectName', 'workflowId'] as Set
}

def 'should treat empty auto labels list as disabled' () {
when:
def config = new ExecutorOpts([
endpoint: 'https://sched.example.com',
autoLabels: []
])

then:
config.autoLabels.isEmpty()
}

def 'should treat empty auto labels string as disabled' () {
when:
def config = new ExecutorOpts([
endpoint: 'https://sched.example.com',
autoLabels: ''
])

then:
config.autoLabels.isEmpty()
}

def 'should reject unknown auto labels name' () {
when:
new ExecutorOpts([
endpoint: 'https://sched.example.com',
autoLabels: ['runName', 'foo']
])

then:
def err = thrown(IllegalArgumentException)
err.message.contains("'seqera.executor.autoLabels'")
err.message.contains('foo')
err.message.contains('valid names')
}

def 'should create config with prediction model' () {
Expand Down
52 changes: 52 additions & 0 deletions plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,58 @@ class LabelsTest extends Specification {
!labels.entries.containsKey('seqera.io/platform/workflowId')
}

def 'should emit only included workflow metadata labels'() {
given:
def workflow = Mock(WorkflowMetadata) {
getProjectName() >> 'nf-core/rnaseq'
getRunName() >> 'crazy_darwin'
getSessionId() >> UUID.randomUUID()
isResume() >> false
getRevision() >> '3.12.0'
getManifest() >> new Manifest([name: 'nf-core/rnaseq'])
}

when:
def labels = new Labels()
.withWorkflowMetadata(workflow, ['runName', 'revision'] as Set)

then:
labels.entries.keySet() == ['nextflow.io/runName', 'nextflow.io/revision'] as Set
}

def 'should emit only the workflowId label when filtered to workflowId'() {
given:
def workflow = Mock(WorkflowMetadata) {
getProjectName() >> 'hello'
getRunName() >> 'happy_turing'
getSessionId() >> UUID.randomUUID()
isResume() >> false
getManifest() >> new Manifest([:])
getPlatform() >> new PlatformMetadata('wf-abc123')
}

when:
def labels = new Labels()
.withWorkflowMetadata(workflow, ['workflowId'] as Set)

then:
labels.entries.keySet() == ['seqera.io/platform/workflowId'] as Set
}

def 'should emit nothing for an empty include set'() {
given:
def workflow = Mock(WorkflowMetadata) {
getProjectName() >> 'hello'
}

when:
def labels = new Labels()
.withWorkflowMetadata(workflow, [] as Set)

then:
labels.entries.isEmpty()
}

def 'should add process resource labels coercing values to string'() {
when:
def labels = new Labels()
Expand Down
Loading