From 5c881c5e07063db7b24f016a384c4bc11aad59ee Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 17 Apr 2026 20:07:46 +0200 Subject: [PATCH 01/16] docs: add design for Seqera executor resourceLabels support Spec for honouring process.resourceLabels in nf-seqera with cumulative semantics: config-level baseline on the Sched run, per-task delta on Sched task. Removes the now-redundant seqera.executor.labels option. Signed-off-by: Paolo Di Tommaso --- ...026-04-17-seqera-resource-labels-design.md | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-17-seqera-resource-labels-design.md diff --git a/docs/superpowers/specs/2026-04-17-seqera-resource-labels-design.md b/docs/superpowers/specs/2026-04-17-seqera-resource-labels-design.md new file mode 100644 index 0000000000..efda8cf15a --- /dev/null +++ b/docs/superpowers/specs/2026-04-17-seqera-resource-labels-design.md @@ -0,0 +1,170 @@ +# Seqera executor: support `process.resourceLabels` + +Date: 2026-04-17 +Status: Approved + +## Problem + +The `nf-seqera` executor does not honour the `process.resourceLabels` +directive. `SeqeraTaskHandler.submit()` builds the scheduler `Task` with +`name`, `image`, `command`, `environment`, `resourceRequirement`, +`resourceLimit`, `machineRequirement`, and `nextflow(taskId/hash/workDir)` — +it never reads `task.config.getResourceLabels()`. The plugin's only label +path is at the run level (`SeqeraExecutor.createRun()`), where +`Labels.withUserLabels(seqeraConfig.labels)` and optional auto-labels are +attached to `CreateRunRequest`. + +`AbstractComputePlatformProvider.addConfigResourceLabels()` emits +`process.resourceLabels = [...]` into the Nextflow config for every CE type +including the Seqera Compute default config. AWS Batch / GCP Batch / Azure / +K8s honour the directive; on the Seqera Compute path the directive is +effectively dead. + +## Goal + +Implement support for `process.resourceLabels` in the Seqera executor and +pass labels through to the `sched-client`, with cumulative semantics that +mirror Nextflow's existing label model. + +## Label model + +Nextflow labels are cumulative: + +- `process.resourceLabels` at the top level of `nextflow.config` is the + common baseline — it applies to every task across every process. +- Selector-scoped (`withName:`, `withLabel:`) and in-process-body + `resourceLabels` directives merge on top, per process. +- `TaskConfig.getResourceLabels()` returns the final merged map for a given + task. + +The `sched-api` (≥ 0.51.0) exposes labels at two scopes: + +- `CreateRunRequest.labels` — set once at run creation +- `Task.labels` — set per task + +We map cumulative Nextflow labels onto these two scopes: + +- **Run-level labels** = config-level `process.resourceLabels` (the common + baseline) + `nextflow.io/*` auto-labels (when `seqera.executor.autoLabels` + is enabled). +- **Per-task labels** = the *delta* between `task.config.getResourceLabels()` + and the run-level baseline: + - keys present on the task but absent from the run baseline + - keys present in both where the task value differs from the run value + - keys present in both with identical values are omitted + +When the delta is empty, `Task.labels` is left unset. + +The Sched scheduler is expected to merge run + task labels with task labels +overriding run labels on key collision; this preserves the Nextflow +semantic where a per-process `resourceLabels` directive overrides the +config-level default for the same key. + +## Changes + +### 1. Remove `seqera.executor.labels` + +This config option becomes redundant once `process.resourceLabels` is the +canonical user-facing way to attach run-level labels. + +- `ExecutorOpts` (`plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy`): + remove the `labels` field, getter, and `@ConfigOption` declaration. +- `SeqeraExecutor.createRun()`: drop the + `labels.withUserLabels(seqeraConfig.labels)` call. +- `Labels`: remove `withUserLabels(Map)` (no remaining callers). +- `ExecutorOptsTest`, `LabelsTest`, `SeqeraExecutorTest`: drop assertions + for the removed option. +- Plugin `changelog.txt`: note the removal as a breaking change for plugin + `nf-seqera` 0.18.0. + +### 2. Add `withProcessResourceLabels` to `Labels` + +`plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy`: + +```groovy +Labels withProcessResourceLabels(Map map) { + if( map ) + map.each { k, v -> entries.put(k.toString(), String.valueOf(v)) } + return this +} +``` + +Values are coerced to `String` via `String.valueOf` to satisfy +`sched-api`'s `Map` typing without rejecting non-string +values that Nextflow's `resourceLabels` directive accepts. + +### 3. Wire run-level labels in `SeqeraExecutor.createRun()` + +```groovy +final processLabels = (session.config.process as Map)?.resourceLabels as Map +final labels = new Labels() +if( seqeraConfig.autoLabels ) + labels.withWorkflowMetadata(session.workflowMetadata) +labels.withProcessResourceLabels(processLabels) +this.runResourceLabels = coerceToStringMap(processLabels) +``` + +The coerced map is cached on the executor as `runResourceLabels` so task +handlers can compute the delta without re-reading config or duplicating the +coercion logic. `coerceToStringMap` lives next to `Labels` (or as a +`static` helper on it) and applies `String.valueOf` to each value. + +### 4. Compute and attach the per-task delta in `SeqeraTaskHandler.submit()` + +```groovy +final taskLabels = coerceToStringMap(task.config.getResourceLabels()) +final delta = deltaLabels(taskLabels, executor.runResourceLabels) +if( delta ) + schedTask.labels(delta) +``` + +`deltaLabels(task, run)` returns a `Map` containing entries +in `task` that are missing in `run` or whose value differs from the value +in `run`. Empty map → return `null` so the caller can omit the field. + +The helper lives alongside `Labels` (e.g. `Labels.delta(task, run)`). + +### 5. `sched-client` dependency + +- `plugins/nf-seqera/build.gradle`: bump `io.seqera:sched-client` to the + released version exposing `Task.labels` (≥ 0.51.0 once published). +- `settings.gradle`: add an `includeBuild '../sched'` block matching the + existing commented `includeBuild '../nextflow-plugin-gradle'` pattern, so + development against an unreleased sched is opt-in via uncommenting. + +### 6. Tests (Spock) + +- `LabelsTest`: + - `withProcessResourceLabels` merges entries, coerces non-String values, + no-ops on null/empty. + - `delta(task, run)` returns missing keys, returns differing keys, omits + matching keys, returns empty/null when fully covered. + - Existing `withUserLabels` assertions removed. +- `SeqeraExecutorTest`: + - `createRun` populates `CreateRunRequest.labels` with config-level + `process.resourceLabels` merged with auto-labels. + - `runResourceLabels` accessor returns the coerced baseline map. + - Removed assertions for `seqera.executor.labels`. +- `SeqeraTaskHandlerTest`: + - `submit` attaches `Task.labels` containing the delta when the task adds + new labels or overrides values. + - `submit` leaves `Task.labels` unset when the task labels equal the run + baseline. +- `ExecutorOptsTest`: remove `labels` parsing test. + +### 7. Docs + +- `docs/reference/process.md` (resourceLabels section, around line 1388): + add `{ref}seqera-executor` to the list of executors that support + `resourceLabels`. +- `plugins/nf-seqera/changelog.txt`: entry covering the new behaviour and + the removal of `seqera.executor.labels`. + +## Out of scope + +- No deprecation warning shim for `seqera.executor.labels` — the plugin is + early (0.17.0) and the user has approved removal. +- No changes to `seqera.executor.autoLabels` semantics or to the + `nextflow.io/*` / `seqera:sched:*` label namespaces. +- No changes to the sched-api or sched-client itself; the `Task.labels` + field is assumed already published in the version we depend on. From c37151d6564d650a6b9546e8afea2c71626f0b53 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 17 Apr 2026 20:14:00 +0200 Subject: [PATCH 02/16] docs: add implementation plan for Seqera executor resourceLabels Task-by-task TDD plan implementing the spec from docs/superpowers/specs/2026-04-17-seqera-resource-labels-design.md. Signed-off-by: Paolo Di Tommaso --- .../2026-04-17-seqera-resource-labels.md | 667 ++++++++++++++++++ 1 file changed, 667 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-17-seqera-resource-labels.md diff --git a/docs/superpowers/plans/2026-04-17-seqera-resource-labels.md b/docs/superpowers/plans/2026-04-17-seqera-resource-labels.md new file mode 100644 index 0000000000..5f4fd63973 --- /dev/null +++ b/docs/superpowers/plans/2026-04-17-seqera-resource-labels.md @@ -0,0 +1,667 @@ +# Seqera executor `process.resourceLabels` Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the `nf-seqera` executor honour `process.resourceLabels` by sending the config-level baseline as run labels and the per-task delta as Sched task labels. + +**Architecture:** Cumulative Nextflow labels split into two scheduler scopes — config-level `process.resourceLabels` becomes `CreateRunRequest.labels`; the difference between `task.config.getResourceLabels()` and that baseline becomes `Task.labels`. The redundant `seqera.executor.labels` config option is removed. + +**Tech Stack:** Groovy 4 / Java 21 toolchain, Gradle, Spock, `io.seqera:sched-client` (≥ 0.51.0 — must expose `Task.labels`), Nextflow extension-point plugin model. + +**Spec:** `docs/superpowers/specs/2026-04-17-seqera-resource-labels-design.md` + +**File map:** +- Modify `settings.gradle` — uncomment-style `includeBuild '../sched'` for dev +- Modify `plugins/nf-seqera/build.gradle` — bump `sched-client` version +- Modify `plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy` — add `withProcessResourceLabels`, add `delta`, remove `withUserLabels` +- Modify `plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy` — remove `labels` field +- Modify `plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy` — wire run labels + cache `runResourceLabels` +- Modify `plugins/nf-seqera/src/main/io/seqera/executor/SeqeraTaskHandler.groovy` — attach delta to `Task.labels` +- Modify `plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy` +- Modify `plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy` +- Modify `plugins/nf-seqera/src/test/io/seqera/executor/SeqeraTaskHandlerTest.groovy` +- Modify `docs/reference/process.md` — add Seqera executor to support list +- Modify `plugins/nf-seqera/changelog.txt` — entry +- Modify `plugins/nf-seqera/VERSION` — bump to `0.18.0` + +--- + +### Task 1: Bump `sched-client` and wire `includeBuild '../sched'` + +**Files:** +- Modify: `plugins/nf-seqera/build.gradle:54` +- Modify: `settings.gradle:17-19` + +The local `~/Projects/sched` checkout is at `0.51.0`; that is the version exposing `Task.labels`. Until 0.51.0 is published to the Seqera Maven repo, we use a Gradle composite build to substitute the dependency from the local checkout. + +- [ ] **Step 1: Bump sched-client version** + +Edit `plugins/nf-seqera/build.gradle:54`: + +```gradle + api 'io.seqera:sched-client:0.51.0' +``` + +- [ ] **Step 2: Add `includeBuild '../sched'` block to `settings.gradle`** + +Replace the commented `pluginManagement` block at `settings.gradle:17-19` with both blocks (keep the existing comment, add a new one for sched as dev-only opt-in): + +```gradle +// pluginManagement { +// includeBuild '../nextflow-plugin-gradle' +// } + +// For local development against an unpublished sched-client, uncomment: +// includeBuild '../sched' +includeBuild '../sched' +``` + +(The uncommented `includeBuild '../sched'` line is required for the build to resolve `sched-client:0.51.0` until the artifact is published. The commented hint stays for future reference.) + +- [ ] **Step 3: Verify the build resolves** + +Run: `./gradlew :plugins:nf-seqera:compileGroovy` +Expected: BUILD SUCCESSFUL. If it fails with a missing `sched-client:0.51.0`, confirm `~/Projects/sched/VERSION` contains `0.51.0` and that `~/Projects/sched/sched-client` builds locally (`cd ~/Projects/sched && ./gradlew :sched-client:assemble`). + +- [ ] **Step 4: Commit** + +```bash +git add settings.gradle plugins/nf-seqera/build.gradle +git commit -s -m "build(nf-seqera): bump sched-client to 0.51.0 via includeBuild" +``` + +--- + +### Task 2: Add `Labels.withProcessResourceLabels` (TDD) + +**Files:** +- Modify: `plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy` +- Modify: `plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy` + +- [ ] **Step 1: Write the failing tests** + +Append to `plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy` (before the closing `}`): + +```groovy + def 'should add process resource labels coercing values to string'() { + when: + def labels = new Labels() + .withProcessResourceLabels([team: 'genomics', priority: 7, retain: true]) + + then: + labels.entries['team'] == 'genomics' + labels.entries['priority'] == '7' + labels.entries['retain'] == 'true' + } + + def 'should ignore null or empty process resource labels'() { + when: + def a = new Labels().withProcessResourceLabels(null) + def b = new Labels().withProcessResourceLabels([:]) + + then: + a.entries.isEmpty() + b.entries.isEmpty() + } + + def 'should let process resource labels override workflow metadata on key collision'() { + given: + def workflow = Mock(WorkflowMetadata) { + getProjectName() >> 'hello' + getRunName() >> 'happy_turing' + getSessionId() >> UUID.randomUUID() + isResume() >> false + getManifest() >> new Manifest([:]) + } + + when: + def labels = new Labels() + .withWorkflowMetadata(workflow) + .withProcessResourceLabels(['nextflow.io/runName': 'custom', team: 'a']) + + then: + labels.entries['nextflow.io/runName'] == 'custom' + labels.entries['team'] == 'a' + labels.entries['nextflow.io/projectName'] == 'hello' + } +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `./gradlew :plugins:nf-seqera:test --tests 'io.seqera.executor.LabelsTest' -i` +Expected: FAIL — `MissingMethodException: No signature of method ... withProcessResourceLabels`. + +- [ ] **Step 3: Implement `withProcessResourceLabels`** + +Edit `plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy`. After the `withUserLabels` method (which will be removed in Task 4), add: + +```groovy + /** + * Add config-level {@code process.resourceLabels}. Values are coerced to + * string via {@link String#valueOf} to satisfy the scheduler API typing. + */ + Labels withProcessResourceLabels(Map map) { + if( !map ) return this + map.each { k, v -> entries.put(k.toString(), String.valueOf(v)) } + return this + } +``` + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `./gradlew :plugins:nf-seqera:test --tests 'io.seqera.executor.LabelsTest' -i` +Expected: PASS for the three new tests; existing tests still PASS. + +- [ ] **Step 5: Commit** + +```bash +git add plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy \ + plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy +git commit -s -m "feat(nf-seqera): add Labels.withProcessResourceLabels" +``` + +--- + +### Task 3: Add `Labels.delta` and `Labels.toStringMap` helpers (TDD) + +**Files:** +- Modify: `plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy` +- Modify: `plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy` + +These two static helpers compute the per-task delta and coerce arbitrary `Map` values to strings — used by both the executor (to cache the run baseline) and the task handler (to compute the delta). + +- [ ] **Step 1: Write the failing tests** + +Append to `plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy` (before the closing `}`): + +```groovy + def 'should coerce map values to strings'() { + expect: + Labels.toStringMap(null) == [:] + Labels.toStringMap([:]) == [:] + Labels.toStringMap([a: 1, b: 'x', c: true]) == [a: '1', b: 'x', c: 'true'] + } + + def 'should compute null delta when task labels are empty'() { + expect: + Labels.delta(null, [team: 'a']) == null + Labels.delta([:], [team: 'a']) == null + } + + def 'should return full task labels when run labels are empty'() { + expect: + Labels.delta([team: 'a', region: 'us'], null) == [team: 'a', region: 'us'] + Labels.delta([team: 'a', region: 'us'], [:]) == [team: 'a', region: 'us'] + } + + def 'should keep only differing or missing keys in delta'() { + expect: + Labels.delta([team: 'a', region: 'us'], [team: 'a']) == [region: 'us'] + Labels.delta([team: 'b'], [team: 'a']) == [team: 'b'] + Labels.delta([team: 'a', region: 'us'], [team: 'a', region: 'us']) == null + } +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `./gradlew :plugins:nf-seqera:test --tests 'io.seqera.executor.LabelsTest' -i` +Expected: FAIL — `MissingMethodException: ... toStringMap` and `... delta`. + +- [ ] **Step 3: Implement the helpers** + +Edit `plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy`. Inside the class, after the existing `runId` method, add: + +```groovy + /** + * Coerce arbitrary map values to strings via {@link String#valueOf}. + * Returns an empty map for null/empty input. + */ + static Map toStringMap(Map map) { + if( !map ) return Collections.emptyMap() + final result = new LinkedHashMap(map.size()) + map.each { k, v -> result.put(k.toString(), String.valueOf(v)) } + return result + } + + /** + * Return the entries of {@code task} that are missing from {@code run} + * or have a different value. Returns {@code null} if the resulting + * map would be empty (so callers can omit the field). + */ + static Map delta(Map task, Map run) { + if( !task ) return null + final result = new LinkedHashMap() + task.each { k, v -> + if( run == null || !run.containsKey(k) || run.get(k) != v ) + result.put(k, v) + } + return result.isEmpty() ? null : result + } +``` + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `./gradlew :plugins:nf-seqera:test --tests 'io.seqera.executor.LabelsTest' -i` +Expected: PASS for all new tests; existing tests still PASS. + +- [ ] **Step 5: Commit** + +```bash +git add plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy \ + plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy +git commit -s -m "feat(nf-seqera): add Labels.toStringMap and Labels.delta helpers" +``` + +--- + +### Task 4: Remove `seqera.executor.labels` config option + +**Files:** +- Modify: `plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy:74-79,130,168-170` +- Modify: `plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy:81-88` +- Modify: `plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy:120` +- Modify: `plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy:131-165` +- Modify: `plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy:127-150,192-199` + +The user-facing `seqera.executor.labels` option is replaced by the standard Nextflow `process.resourceLabels` directive. + +- [ ] **Step 1: Remove the field and getter from `ExecutorOpts`** + +Edit `plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy`: + +Remove lines 74-79 (the `@ConfigOption` block and `final Map labels` field): + +```groovy + @ConfigOption + @Description(""" + Custom labels to apply to AWS resources for cost tracking and resource organization. + Labels are propagated to ECS tasks, capacity providers, and EC2 instances. + """) + final Map labels +``` + +Remove the assignment in the constructor (around line 129-130): + +```groovy + // labels for cost tracking + this.labels = opts.labels as Map +``` + +Remove the getter (around line 168-170): + +```groovy + Map getLabels() { + return labels + } +``` + +- [ ] **Step 2: Remove the `withUserLabels` method from `Labels`** + +Edit `plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy`. Delete the entire `withUserLabels` method (lines 81-88): + +```groovy + /** + * Add user-configured labels. These take precedence over implicit labels. + */ + Labels withUserLabels(Map labels) { + if( labels ) + entries.putAll(labels) + return this + } +``` + +- [ ] **Step 3: Remove the `withUserLabels` call site in `SeqeraExecutor.createRun()`** + +Edit `plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy`. Delete the line: + +```groovy + labels.withUserLabels(seqeraConfig.labels) +``` + +- [ ] **Step 4: Remove obsolete tests** + +Edit `plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy`. Delete the three tests (lines 131-165): `'should create config with labels'`, `'should handle null labels'`, `'should handle empty labels'`. + +Edit `plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy`. Delete the two tests: `'should allow user labels to override implicit labels'` (lines 127-150) and `'should handle null user labels'` (lines 192-199). + +- [ ] **Step 5: Compile and run tests** + +Run: `./gradlew :plugins:nf-seqera:compileGroovy :plugins:nf-seqera:test` +Expected: BUILD SUCCESSFUL; all remaining tests PASS. If the compiler complains about a stray reference to `seqeraConfig.labels` or `withUserLabels`, grep for and remove them: `rg "seqeraConfig\.labels|withUserLabels" plugins/nf-seqera`. + +- [ ] **Step 6: Commit** + +```bash +git add plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy \ + plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy \ + plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy \ + plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy \ + plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy +git commit -s -m "refactor(nf-seqera)!: remove seqera.executor.labels in favour of process.resourceLabels" +``` + +--- + +### Task 5: Wire `process.resourceLabels` into `SeqeraExecutor.createRun()` and expose `runResourceLabels` (TDD) + +**Files:** +- Modify: `plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy` +- Modify: `plugins/nf-seqera/src/test/io/seqera/executor/SeqeraExecutorTest.groovy` + +The executor reads the config-level `process.resourceLabels` map once at run creation, attaches it to the run labels via `Labels.withProcessResourceLabels`, and caches the coerced map so task handlers can compute deltas. + +- [ ] **Step 1: Write the failing test** + +Append to `plugins/nf-seqera/src/test/io/seqera/executor/SeqeraExecutorTest.groovy` (before the final closing `}`): + +```groovy + def 'should expose run resource labels coerced from config-level process.resourceLabels'() { + given: + def executor = new SeqeraExecutor() + executor.@session = Mock(Session) { + getConfig() >> [process: [resourceLabels: [team: 'a', priority: 7]]] + } + + when: + executor.computeRunResourceLabels() + + then: + executor.runResourceLabels == [team: 'a', priority: '7'] + } + + def 'should yield empty run resource labels when process.resourceLabels is absent'() { + given: + def executor = new SeqeraExecutor() + executor.@session = Mock(Session) { + getConfig() >> [:] + } + + when: + executor.computeRunResourceLabels() + + then: + executor.runResourceLabels == [:] + } +``` + +(`Session` is already imported at line 21; if not, add the import.) + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `./gradlew :plugins:nf-seqera:test --tests 'io.seqera.executor.SeqeraExecutorTest' -i` +Expected: FAIL — `computeRunResourceLabels` / `runResourceLabels` don't exist. + +- [ ] **Step 3: Implement on `SeqeraExecutor`** + +Edit `plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy`. + +Add a private field near the other private fields (after `runId` at line 65): + +```groovy + private volatile Map runResourceLabels = Collections.emptyMap() +``` + +Add a method to compute the run resource labels (place near other protected/package methods, e.g. before `createRun()` at line 110): + +```groovy + @groovy.transform.PackageScope + void computeRunResourceLabels() { + final processMap = session.config.process as Map + final raw = processMap?.get('resourceLabels') as Map + this.runResourceLabels = Labels.toStringMap(raw) + } +``` + +Add the public getter (after `getRunId()` around line 204): + +```groovy + Map getRunResourceLabels() { + return runResourceLabels + } +``` + +Wire it into `createRun()`. Replace the labels-building block at `SeqeraExecutor.groovy:117-120` (after the deletion in Task 4 it should look like the first three lines below) with: + +```groovy + computeRunResourceLabels() + final labels = new Labels() + if( seqeraConfig.autoLabels ) + labels.withWorkflowMetadata(session.workflowMetadata) + labels.withProcessResourceLabels(runResourceLabels) +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `./gradlew :plugins:nf-seqera:test --tests 'io.seqera.executor.SeqeraExecutorTest' -i` +Expected: PASS for both new tests; existing tests still PASS. + +- [ ] **Step 5: Commit** + +```bash +git add plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy \ + plugins/nf-seqera/src/test/io/seqera/executor/SeqeraExecutorTest.groovy +git commit -s -m "feat(nf-seqera): attach process.resourceLabels to Sched run labels" +``` + +--- + +### Task 6: Send per-task delta on `Task.labels` from `SeqeraTaskHandler.submit()` (TDD) + +**Files:** +- Modify: `plugins/nf-seqera/src/main/io/seqera/executor/SeqeraTaskHandler.groovy` +- Modify: `plugins/nf-seqera/src/test/io/seqera/executor/SeqeraTaskHandlerTest.groovy` + +Capture the `Task` passed to the batch submitter and assert its `labels` field reflects the delta between the task's `getResourceLabels()` and the executor's `runResourceLabels`. + +- [ ] **Step 1: Write the failing tests** + +Append to `plugins/nf-seqera/src/test/io/seqera/executor/SeqeraTaskHandlerTest.groovy` (before the final closing `}`): + +```groovy + def 'submit attaches Task.labels containing only the per-task delta'() { + given: + Task captured = null + def batchSubmitter = Mock(SeqeraBatchSubmitter) { + submit(_, _) >> { args -> captured = args[1] as Task } + } + def taskConfig = Mock(TaskConfig) { + getCpus() >> 2 + getMemory() >> MemoryUnit.of('1 GB') + getAccelerator() >> null + getResourceLabels() >> [team: 'a', region: 'us-east-1'] + getResourceLimit('memory') >> null + getResourceLimit('cpus') >> null + getDisk() >> null + } + def taskRun = Mock(TaskRun) { + getConfig() >> taskConfig + getWorkDir() >> Paths.get('/work/ab/cd1234') + getWorkDirStr() >> '/work/ab/cd1234' + getContainer() >> 'docker.io/library/alpine:3' + getContainerPlatform() >> 'linux/amd64' + getId() >> TaskId.of(1) + getHash() >> HashCode.fromInt(1) + lazyName() >> 'sample_task' + } + def executor = Mock(SeqeraExecutor) { + getClient() >> Mock(SchedClient) + getBatchSubmitter() >> batchSubmitter + getSeqeraConfig() >> Mock(ExecutorOpts) { + getMachineRequirement() >> Mock(io.seqera.config.MachineRequirementOpts) + getTaskEnvironment() >> [:] + } + getRunResourceLabels() >> [team: 'a'] + ensureRunCreated() >> {} + } + def handler = Spy(new SeqeraTaskHandler(taskRun, executor)) { + fusionEnabled() >> true + fusionLauncher() >> Mock(nextflow.fusion.FusionScriptLauncher) { + fusionEnv() >> [:] + } + fusionSubmitCli() >> ['/bin/sh', '-c', 'true'] + fusionConfig() >> Mock(nextflow.fusion.FusionConfig) { + snapshotsEnabled() >> false + } + } + + when: + handler.submit() + + then: + captured != null + captured.getLabels() == [region: 'us-east-1'] + } + + def 'submit leaves Task.labels unset when the task labels equal the run baseline'() { + given: + Task captured = null + def batchSubmitter = Mock(SeqeraBatchSubmitter) { + submit(_, _) >> { args -> captured = args[1] as Task } + } + def taskConfig = Mock(TaskConfig) { + getCpus() >> 2 + getMemory() >> MemoryUnit.of('1 GB') + getAccelerator() >> null + getResourceLabels() >> [team: 'a'] + getResourceLimit('memory') >> null + getResourceLimit('cpus') >> null + getDisk() >> null + } + def taskRun = Mock(TaskRun) { + getConfig() >> taskConfig + getWorkDir() >> Paths.get('/work/ab/cd1234') + getWorkDirStr() >> '/work/ab/cd1234' + getContainer() >> 'docker.io/library/alpine:3' + getContainerPlatform() >> 'linux/amd64' + getId() >> TaskId.of(1) + getHash() >> HashCode.fromInt(1) + lazyName() >> 'sample_task' + } + def executor = Mock(SeqeraExecutor) { + getClient() >> Mock(SchedClient) + getBatchSubmitter() >> batchSubmitter + getSeqeraConfig() >> Mock(ExecutorOpts) { + getMachineRequirement() >> Mock(io.seqera.config.MachineRequirementOpts) + getTaskEnvironment() >> [:] + } + getRunResourceLabels() >> [team: 'a'] + ensureRunCreated() >> {} + } + def handler = Spy(new SeqeraTaskHandler(taskRun, executor)) { + fusionEnabled() >> true + fusionLauncher() >> Mock(nextflow.fusion.FusionScriptLauncher) { + fusionEnv() >> [:] + } + fusionSubmitCli() >> ['/bin/sh', '-c', 'true'] + fusionConfig() >> Mock(nextflow.fusion.FusionConfig) { + snapshotsEnabled() >> false + } + } + + when: + handler.submit() + + then: + captured != null + captured.getLabels() == null + } +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `./gradlew :plugins:nf-seqera:test --tests 'io.seqera.executor.SeqeraTaskHandlerTest' -i` +Expected: FAIL — assertions on `captured.getLabels()` fail because submit() does not set them. + +- [ ] **Step 3: Wire the delta into `submit()`** + +Edit `plugins/nf-seqera/src/main/io/seqera/executor/SeqeraTaskHandler.groovy`. After the `final schedTask = new Task() ... .nextflow(...)` block ending around line 140, before the `log.debug` call at line 141, insert: + +```groovy + // attach per-task resource labels delta (over run-level baseline) + final taskLabels = Labels.toStringMap(task.config.getResourceLabels()) + final delta = Labels.delta(taskLabels, executor.runResourceLabels) + if( delta ) + schedTask.labels(delta) +``` + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `./gradlew :plugins:nf-seqera:test --tests 'io.seqera.executor.SeqeraTaskHandlerTest' -i` +Expected: PASS for both new tests; existing tests still PASS. + +- [ ] **Step 5: Run the full plugin test suite** + +Run: `./gradlew :plugins:nf-seqera:test` +Expected: BUILD SUCCESSFUL; no regressions. + +- [ ] **Step 6: Commit** + +```bash +git add plugins/nf-seqera/src/main/io/seqera/executor/SeqeraTaskHandler.groovy \ + plugins/nf-seqera/src/test/io/seqera/executor/SeqeraTaskHandlerTest.groovy +git commit -s -m "feat(nf-seqera): send per-task resourceLabels delta on Sched task" +``` + +--- + +### Task 7: Docs, changelog, and version bump + +**Files:** +- Modify: `docs/reference/process.md:1388-1393` +- Modify: `plugins/nf-seqera/changelog.txt` +- Modify: `plugins/nf-seqera/VERSION` + +- [ ] **Step 1: Update docs** + +Edit `docs/reference/process.md`. Replace the executor support list at lines 1388-1393: + +```markdown +Resource labels are currently supported by the following executors: + +- {ref}`awsbatch-executor` +- {ref}`azurebatch-executor` +- {ref}`google-batch-executor` +- {ref}`k8s-executor` +- {ref}`seqera-executor` +``` + +(If `seqera-executor` is not a defined ref, drop the `{ref}` wrapper and write `Seqera executor` as plain text.) + +- [ ] **Step 2: Update plugin changelog** + +Edit `plugins/nf-seqera/changelog.txt`. Add a new entry at the top, above the `0.17.0` block: + +``` +0.18.0 - +- Support process.resourceLabels: config-level labels attached to Sched run, per-task delta attached to Sched task +- Remove seqera.executor.labels config option (use process.resourceLabels instead) +- Bump sched-client@0.51.0 +``` + +- [ ] **Step 3: Bump plugin VERSION** + +Edit `plugins/nf-seqera/VERSION`: + +``` +0.18.0 +``` + +- [ ] **Step 4: Verify everything builds** + +Run: `./gradlew :plugins:nf-seqera:check` +Expected: BUILD SUCCESSFUL. + +- [ ] **Step 5: Commit** + +```bash +git add docs/reference/process.md plugins/nf-seqera/changelog.txt plugins/nf-seqera/VERSION +git commit -s -m "docs(nf-seqera): document resourceLabels support and bump to 0.18.0" +``` + +--- + +## Self-review checklist (executed) + +- **Spec coverage:** every section of `2026-04-17-seqera-resource-labels-design.md` maps to a task — sched-client bump (Task 1), `withProcessResourceLabels` (Task 2), `delta` + `toStringMap` (Task 3), removal of `seqera.executor.labels` (Task 4), run-level wiring + `runResourceLabels` (Task 5), per-task delta on `Task.labels` (Task 6), docs / changelog / VERSION (Task 7). +- **Placeholder scan:** no TBDs, no "implement later", every code step has the actual code. +- **Type consistency:** `Labels.toStringMap(Map)` and `Labels.delta(Map, Map)` referenced consistently in Tasks 3, 5, 6; `runResourceLabels` field, `computeRunResourceLabels()` method, and `getRunResourceLabels()` getter consistent across Tasks 5 and 6. From 21bef93bf91c6d0fd3a4412754c21576778e8d6a Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 17 Apr 2026 20:21:12 +0200 Subject: [PATCH 03/16] build(nf-seqera): bump sched-client to 0.51.0 via includeBuild Signed-off-by: Paolo Di Tommaso --- plugins/nf-seqera/build.gradle | 2 +- settings.gradle | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/nf-seqera/build.gradle b/plugins/nf-seqera/build.gradle index 0c263c6ada..c61f285a6f 100644 --- a/plugins/nf-seqera/build.gradle +++ b/plugins/nf-seqera/build.gradle @@ -51,7 +51,7 @@ dependencies { compileOnly project(':nextflow') compileOnly 'org.slf4j:slf4j-api:2.0.17' compileOnly 'org.pf4j:pf4j:3.14.1' - api 'io.seqera:sched-client:0.49.0' + api 'io.seqera:sched-client:0.51.0' testImplementation(testFixtures(project(":nextflow"))) testImplementation "org.apache.groovy:groovy:4.0.31" diff --git a/settings.gradle b/settings.gradle index bb90d2b520..065c4c5d56 100644 --- a/settings.gradle +++ b/settings.gradle @@ -50,4 +50,7 @@ include 'plugins:nf-k8s' include 'plugins:nf-seqera' //includeBuild('../plugin-registry') -//includeBuild '../sched' + +// For local development against an unpublished sched-client, uncomment: +// includeBuild '../sched' +includeBuild '../sched' From 1c79e673dddf3fe497a6f1f8aa73d0c899610bff Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 17 Apr 2026 20:24:25 +0200 Subject: [PATCH 04/16] build: keep sched includeBuild commented for CI safety Signed-off-by: Paolo Di Tommaso --- settings.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/settings.gradle b/settings.gradle index 065c4c5d56..f8dcbb3178 100644 --- a/settings.gradle +++ b/settings.gradle @@ -52,5 +52,4 @@ include 'plugins:nf-seqera' //includeBuild('../plugin-registry') // For local development against an unpublished sched-client, uncomment: -// includeBuild '../sched' -includeBuild '../sched' +//includeBuild '../sched' From 1d7f81ec0ea8bf986652dfca1118522389abc11f Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 17 Apr 2026 20:27:09 +0200 Subject: [PATCH 05/16] feat(nf-seqera): add Labels.withProcessResourceLabels Signed-off-by: Paolo Di Tommaso --- .../src/main/io/seqera/executor/Labels.groovy | 11 +++++ .../test/io/seqera/executor/LabelsTest.groovy | 42 +++++++++++++++++++ 2 files changed, 53 insertions(+) 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 941c408ae0..34f5a3ebb9 100644 --- a/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy @@ -87,6 +87,17 @@ class Labels { return this } + /** + * Add config-level {@code process.resourceLabels}. Values are coerced to + * string via {@link String#valueOf} to satisfy the scheduler API typing. + */ + Labels withProcessResourceLabels(Map map) { + if( !map ) return this + for( Map.Entry entry : map.entrySet() ) + entries.put(entry.key.toString(), String.valueOf(entry.value)) + return this + } + /** * @return all labels as an unmodifiable map */ 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 0b646989e3..198bd78efc 100644 --- a/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy @@ -197,4 +197,46 @@ class LabelsTest extends Specification { then: labels.entries.isEmpty() } + + def 'should add process resource labels coercing values to string'() { + when: + def labels = new Labels() + .withProcessResourceLabels([team: 'genomics', priority: 7, retain: true]) + + then: + labels.entries['team'] == 'genomics' + labels.entries['priority'] == '7' + labels.entries['retain'] == 'true' + } + + def 'should ignore null or empty process resource labels'() { + when: + def a = new Labels().withProcessResourceLabels(null) + def b = new Labels().withProcessResourceLabels([:]) + + then: + a.entries.isEmpty() + b.entries.isEmpty() + } + + def 'should let process resource labels override workflow metadata on key collision'() { + given: + def workflow = Mock(WorkflowMetadata) { + getProjectName() >> 'hello' + getRunName() >> 'happy_turing' + getSessionId() >> UUID.randomUUID() + isResume() >> false + getManifest() >> new Manifest([:]) + } + + when: + def labels = new Labels() + .withWorkflowMetadata(workflow) + .withProcessResourceLabels(['nextflow.io/runName': 'custom', team: 'a']) + + then: + labels.entries['nextflow.io/runName'] == 'custom' + labels.entries['team'] == 'a' + labels.entries['nextflow.io/projectName'] == 'hello' + } } From 3117e9eb1ba89583c58b564aa1b900c65c831681 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 17 Apr 2026 20:32:12 +0200 Subject: [PATCH 06/16] feat(nf-seqera): add Labels.toStringMap and Labels.delta helpers Signed-off-by: Paolo Di Tommaso --- .../src/main/io/seqera/executor/Labels.groovy | 29 +++++++++++++++++++ .../test/io/seqera/executor/LabelsTest.groovy | 26 +++++++++++++++++ 2 files changed, 55 insertions(+) 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 34f5a3ebb9..a07718fa9d 100644 --- a/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy @@ -117,4 +117,33 @@ class Labels { .hash() .toString() } + + /** + * Coerce arbitrary map values to strings via {@link String#valueOf}. + * Returns an empty map for null/empty input. + */ + static Map toStringMap(Map map) { + if( !map ) return Collections.emptyMap() + final result = new LinkedHashMap(map.size()) + for( Map.Entry entry : map.entrySet() ) + result.put(entry.key.toString(), String.valueOf(entry.value)) + return result + } + + /** + * Return the entries of {@code task} that are missing from {@code run} + * or have a different value. Returns {@code null} if the resulting + * map would be empty (so callers can omit the field). + */ + static Map delta(Map task, Map run) { + if( !task ) return null + final result = new LinkedHashMap() + for( Map.Entry entry : task.entrySet() ) { + final k = entry.key + final v = entry.value + if( run == null || !run.containsKey(k) || run.get(k) != v ) + result.put(k, v) + } + return result.isEmpty() ? null : result + } } 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 198bd78efc..114d136228 100644 --- a/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy @@ -239,4 +239,30 @@ class LabelsTest extends Specification { labels.entries['team'] == 'a' labels.entries['nextflow.io/projectName'] == 'hello' } + + def 'should coerce map values to strings'() { + expect: + Labels.toStringMap(null) == [:] + Labels.toStringMap([:]) == [:] + Labels.toStringMap([a: 1, b: 'x', c: true]) == [a: '1', b: 'x', c: 'true'] + } + + def 'should compute null delta when task labels are empty'() { + expect: + Labels.delta(null, [team: 'a']) == null + Labels.delta([:], [team: 'a']) == null + } + + def 'should return full task labels when run labels are empty'() { + expect: + Labels.delta([team: 'a', region: 'us'], null) == [team: 'a', region: 'us'] + Labels.delta([team: 'a', region: 'us'], [:]) == [team: 'a', region: 'us'] + } + + def 'should keep only differing or missing keys in delta'() { + expect: + Labels.delta([team: 'a', region: 'us'], [team: 'a']) == [region: 'us'] + Labels.delta([team: 'b'], [team: 'a']) == [team: 'b'] + Labels.delta([team: 'a', region: 'us'], [team: 'a', region: 'us']) == null + } } From d53dfef410d9fa8b5ddf1432d0740018b1ae381a Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 17 Apr 2026 20:37:09 +0200 Subject: [PATCH 07/16] refactor(nf-seqera)!: remove seqera.executor.labels in favour of process.resourceLabels Signed-off-by: Paolo Di Tommaso --- .../main/io/seqera/config/ExecutorOpts.groovy | 13 ------- .../src/main/io/seqera/executor/Labels.groovy | 9 ----- .../io/seqera/executor/SeqeraExecutor.groovy | 1 - .../io/seqera/config/ExecutorOptsTest.groovy | 36 ------------------- .../io/seqera/config/SeqeraConfigTest.groovy | 5 --- .../test/io/seqera/executor/LabelsTest.groovy | 34 ------------------ 6 files changed, 98 deletions(-) diff --git a/plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy b/plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy index 84ad15d5ea..1facffaa74 100644 --- a/plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy @@ -71,13 +71,6 @@ class ExecutorOpts implements ConfigScope { """) final MachineRequirementOpts machineRequirement - @ConfigOption - @Description(""" - Custom labels to apply to AWS resources for cost tracking and resource organization. - Labels are propagated to ECS tasks, capacity providers, and EC2 instances. - """) - final Map labels - @ConfigOption @Description(""" When `true`, automatically adds workflow metadata labels (e.g. project name, @@ -126,8 +119,6 @@ class ExecutorOpts implements ConfigScope { : Duration.of('1 sec') // machine requirement settings this.machineRequirement = new MachineRequirementOpts(opts.machineRequirement as Map ?: Map.of()) - // labels for cost tracking - this.labels = opts.labels as Map this.autoLabels = opts.autoLabels as boolean ?: false // prediction model this.predictionModel = opts.predictionModel as String ?: null @@ -165,10 +156,6 @@ class ExecutorOpts implements ConfigScope { return machineRequirement } - Map getLabels() { - return labels - } - boolean getAutoLabels() { return autoLabels } 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 a07718fa9d..eb09007c92 100644 --- a/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy @@ -78,15 +78,6 @@ class Labels { return this } - /** - * Add user-configured labels. These take precedence over implicit labels. - */ - Labels withUserLabels(Map labels) { - if( labels ) - entries.putAll(labels) - return this - } - /** * Add config-level {@code process.resourceLabels}. Values are coerced to * string via {@link String#valueOf} to satisfy the scheduler API typing. diff --git a/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy b/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy index 51ec50e731..d8e676a212 100644 --- a/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy @@ -117,7 +117,6 @@ class SeqeraExecutor extends Executor implements ExtensionPoint { final labels = new Labels() if( seqeraConfig.autoLabels ) labels.withWorkflowMetadata(session.workflowMetadata) - labels.withUserLabels(seqeraConfig.labels) final predictionModel = seqeraConfig.predictionModel ? PredictionModel.fromValue(seqeraConfig.predictionModel) : null final pipeline = new PipelineSpec() .workflowId(workflowId) diff --git a/plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy b/plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy index 495c9b1895..2d374b8a56 100644 --- a/plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy @@ -128,42 +128,6 @@ class ExecutorOptsTest extends Specification { config.machineRequirement.provisioning == 'spot' } - def 'should create config with labels' () { - when: - def config = new ExecutorOpts([ - endpoint: 'https://sched.example.com', - labels: [ - project: 'genomics', - team: 'research', - costCenter: 'CC-1234' - ] - ]) - - then: - config.labels == [project: 'genomics', team: 'research', costCenter: 'CC-1234'] - } - - def 'should handle null labels' () { - when: - def config = new ExecutorOpts([ - endpoint: 'https://sched.example.com' - ]) - - then: - config.labels == null - } - - def 'should handle empty labels' () { - when: - def config = new ExecutorOpts([ - endpoint: 'https://sched.example.com', - labels: [:] - ]) - - then: - config.labels == [:] - } - def 'should enable auto labels' () { when: def config = new ExecutorOpts([ diff --git a/plugins/nf-seqera/src/test/io/seqera/config/SeqeraConfigTest.groovy b/plugins/nf-seqera/src/test/io/seqera/config/SeqeraConfigTest.groovy index 84e4712ed6..098ff6e4e4 100644 --- a/plugins/nf-seqera/src/test/io/seqera/config/SeqeraConfigTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/config/SeqeraConfigTest.groovy @@ -60,10 +60,6 @@ class SeqeraConfigTest extends Specification { machineRequirement: [ arch: 'arm64', provisioning: 'spot' - ], - labels: [ - project: 'genomics', - team: 'research' ] ] ]) @@ -76,7 +72,6 @@ class SeqeraConfigTest extends Specification { config.executor.batchFlushInterval == Duration.of('2 sec') config.executor.machineRequirement.arch == 'arm64' config.executor.machineRequirement.provisioning == 'spot' - config.executor.labels == [project: 'genomics', team: 'research'] } def 'should throw error when executor endpoint is missing' () { 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 114d136228..a22bbdfbad 100644 --- a/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy @@ -124,31 +124,6 @@ class LabelsTest extends Specification { !labels.entries.containsKey('seqera:sched:clusterId') } - def 'should allow user labels to override implicit labels'() { - given: - def workflow = Mock(WorkflowMetadata) { - getProjectName() >> 'hello' - getUserName() >> 'user1' - getRunName() >> 'happy_turing' - getSessionId() >> UUID.randomUUID() - isResume() >> false - getManifest() >> new Manifest([:]) - } - - when: - def labels = new Labels() - .withWorkflowMetadata(workflow) - .withUserLabels([ - 'nextflow.io/runName': 'custom_name', - 'team': 'research' - ]) - - then: - labels.entries['nextflow.io/runName'] == 'custom_name' - labels.entries['team'] == 'research' - labels.entries['nextflow.io/projectName'] == 'hello' - } - def 'should include platform workflowId when available'() { given: def workflow = Mock(WorkflowMetadata) { @@ -189,15 +164,6 @@ class LabelsTest extends Specification { !labels.entries.containsKey('seqera.io/platform/workflowId') } - def 'should handle null user labels'() { - when: - def labels = new Labels() - .withUserLabels(null) - - then: - labels.entries.isEmpty() - } - def 'should add process resource labels coercing values to string'() { when: def labels = new Labels() From eebebf170ca9b8e9ae44463c6d31b5a94a950b18 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 17 Apr 2026 20:41:49 +0200 Subject: [PATCH 08/16] feat(nf-seqera): attach process.resourceLabels to Sched run labels Signed-off-by: Paolo Di Tommaso --- .../io/seqera/executor/SeqeraExecutor.groovy | 15 ++++++++++ .../seqera/executor/SeqeraExecutorTest.groovy | 30 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy b/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy index d8e676a212..216b0cadff 100644 --- a/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy @@ -64,6 +64,8 @@ class SeqeraExecutor extends Executor implements ExtensionPoint { private volatile String runId + private volatile Map runResourceLabels = Collections.emptyMap() + private SeqeraBatchSubmitter batchSubmitter @Override @@ -114,9 +116,11 @@ class SeqeraExecutor extends Executor implements ExtensionPoint { final workspaceId = PlatformHelper.getWorkspaceId(towerConfig, SysEnv.get()) as Long final computeEnvId = PlatformHelper.getComputeEnvId(towerConfig, SysEnv.get()) ?: seqeraConfig.computeEnvId + computeRunResourceLabels() final labels = new Labels() if( seqeraConfig.autoLabels ) labels.withWorkflowMetadata(session.workflowMetadata) + labels.withProcessResourceLabels(runResourceLabels) final predictionModel = seqeraConfig.predictionModel ? PredictionModel.fromValue(seqeraConfig.predictionModel) : null final pipeline = new PipelineSpec() .workflowId(workflowId) @@ -202,6 +206,17 @@ class SeqeraExecutor extends Executor implements ExtensionPoint { return runId } + Map getRunResourceLabels() { + return runResourceLabels + } + + @groovy.transform.PackageScope + void computeRunResourceLabels() { + final processMap = session.config.process as Map + final raw = processMap?.get('resourceLabels') as Map + this.runResourceLabels = Labels.toStringMap(raw) + } + SeqeraBatchSubmitter getBatchSubmitter() { return batchSubmitter } diff --git a/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraExecutorTest.groovy b/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraExecutorTest.groovy index 9b564a555c..24d4cf23b7 100644 --- a/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraExecutorTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraExecutorTest.groovy @@ -140,6 +140,36 @@ class SeqeraExecutorTest extends Specification { fusionConfig.targetVersion == null } + def 'should expose run resource labels coerced from config-level process.resourceLabels'() { + given: + SysEnv.push([:]) + def executor = new SeqeraExecutor() + executor.session = Mock(Session) { + getConfig() >> [process: [resourceLabels: [team: 'a', priority: 7]]] + } + + when: + executor.computeRunResourceLabels() + + then: + executor.runResourceLabels == [team: 'a', priority: '7'] + } + + def 'should yield empty run resource labels when process.resourceLabels is absent'() { + given: + SysEnv.push([:]) + def executor = new SeqeraExecutor() + executor.session = Mock(Session) { + getConfig() >> [:] + } + + when: + executor.computeRunResourceLabels() + + then: + executor.runResourceLabels == [:] + } + /** * Builds a SchedClientConfig using the same logic as {@link SeqeraExecutor#createClient()} */ From e569cc9bd02a5ccef7c19508eb11eb2d68d91b03 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 17 Apr 2026 20:48:55 +0200 Subject: [PATCH 09/16] feat(nf-seqera): send per-task resourceLabels delta on Sched task Signed-off-by: Paolo Di Tommaso --- .../seqera/executor/SeqeraTaskHandler.groovy | 6 ++ .../executor/SeqeraTaskHandlerTest.groovy | 102 ++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraTaskHandler.groovy b/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraTaskHandler.groovy index 83da4cbc5d..6fd85ae211 100644 --- a/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraTaskHandler.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraTaskHandler.groovy @@ -21,6 +21,7 @@ import java.nio.file.Path import groovy.transform.CompileStatic import groovy.transform.PackageScope import groovy.util.logging.Slf4j +import io.seqera.executor.Labels import io.seqera.sched.api.schema.v1a1.AcceleratorType import io.seqera.sched.api.schema.v1a1.GetTaskLogsResponse import io.seqera.sched.api.schema.v1a1.NextflowTask @@ -138,6 +139,11 @@ class SeqeraTaskHandler extends TaskHandler implements FusionAwareTask { .taskId(task.id?.intValue()) .hash(task.hash?.toString()) .workDir(task.getWorkDirStr())) + // attach per-task resource labels delta (over run-level baseline) + final taskLabels = Labels.toStringMap(task.config.getResourceLabels()) + final delta = Labels.delta(taskLabels, executor.runResourceLabels) + if( delta ) + schedTask.labels(delta) log.debug "[SEQERA] Enqueueing task for batch submission: ${schedTask}" // Enqueue for batch submission - status will be set by setBatchTaskId callback executor.getBatchSubmitter().submit(this, schedTask) diff --git a/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraTaskHandlerTest.groovy b/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraTaskHandlerTest.groovy index 713cfdc991..90421c9d30 100644 --- a/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraTaskHandlerTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraTaskHandlerTest.groovy @@ -800,6 +800,108 @@ class SeqeraTaskHandlerTest extends Specification { capturedTask.getResourceLimit().cpuShares == null } + def 'submit attaches Task.labels containing only the per-task delta'() { + given: + Task captured = null + def batchSubmitter = Mock(SeqeraBatchSubmitter) { + submit(_, _) >> { args -> captured = args[1] as Task } + } + def taskConfig = Mock(TaskConfig) { + getCpus() >> 2 + getMemory() >> MemoryUnit.of('1 GB') + getAccelerator() >> null + getResourceLabels() >> [team: 'a', region: 'us-east-1'] + getResourceLimit('memory') >> null + getResourceLimit('cpus') >> null + getDisk() >> null + } + def taskRun = Mock(TaskRun) { + getConfig() >> taskConfig + getWorkDir() >> Paths.get('/work/ab/cd1234') + getWorkDirStr() >> '/work/ab/cd1234' + getContainer() >> 'docker.io/library/alpine:3' + getContainerPlatform() >> 'linux/amd64' + getId() >> TaskId.of(1) + getHash() >> HashCode.fromInt(1) + lazyName() >> 'sample_task' + } + def executor = Mock(SeqeraExecutor) { + getClient() >> Mock(SchedClient) + getBatchSubmitter() >> batchSubmitter + getSeqeraConfig() >> Mock(ExecutorOpts) { + getMachineRequirement() >> null + getTaskEnvironment() >> [:] + } + getRunResourceLabels() >> [team: 'a'] + ensureRunCreated() >> {} + } + def handler = Spy(new SeqeraTaskHandler(taskRun, executor)) { + fusionEnabled() >> true + fusionLauncher() >> Mock(nextflow.fusion.FusionScriptLauncher) { + fusionEnv() >> [:] + } + fusionSubmitCli() >> ['/bin/sh', '-c', 'true'] + } + + when: + handler.submit() + + then: + captured != null + captured.getLabels() == [region: 'us-east-1'] + } + + def 'submit leaves Task.labels unset when the task labels equal the run baseline'() { + given: + Task captured = null + def batchSubmitter = Mock(SeqeraBatchSubmitter) { + submit(_, _) >> { args -> captured = args[1] as Task } + } + def taskConfig = Mock(TaskConfig) { + getCpus() >> 2 + getMemory() >> MemoryUnit.of('1 GB') + getAccelerator() >> null + getResourceLabels() >> [team: 'a'] + getResourceLimit('memory') >> null + getResourceLimit('cpus') >> null + getDisk() >> null + } + def taskRun = Mock(TaskRun) { + getConfig() >> taskConfig + getWorkDir() >> Paths.get('/work/ab/cd1234') + getWorkDirStr() >> '/work/ab/cd1234' + getContainer() >> 'docker.io/library/alpine:3' + getContainerPlatform() >> 'linux/amd64' + getId() >> TaskId.of(1) + getHash() >> HashCode.fromInt(1) + lazyName() >> 'sample_task' + } + def executor = Mock(SeqeraExecutor) { + getClient() >> Mock(SchedClient) + getBatchSubmitter() >> batchSubmitter + getSeqeraConfig() >> Mock(ExecutorOpts) { + getMachineRequirement() >> null + getTaskEnvironment() >> [:] + } + getRunResourceLabels() >> [team: 'a'] + ensureRunCreated() >> {} + } + def handler = Spy(new SeqeraTaskHandler(taskRun, executor)) { + fusionEnabled() >> true + fusionLauncher() >> Mock(nextflow.fusion.FusionScriptLauncher) { + fusionEnv() >> [:] + } + fusionSubmitCli() >> ['/bin/sh', '-c', 'true'] + } + + when: + handler.submit() + + then: + captured != null + captured.getLabels() == null + } + /** * Creates a test handler with minimal mocked dependencies */ From 03658b6fa34bb55743bd2a65494223f2086780ad Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 17 Apr 2026 20:51:48 +0200 Subject: [PATCH 10/16] docs(nf-seqera): document resourceLabels support and bump to 0.18.0 Signed-off-by: Paolo Di Tommaso --- docs/reference/process.md | 1 + plugins/nf-seqera/VERSION | 2 +- plugins/nf-seqera/changelog.txt | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/reference/process.md b/docs/reference/process.md index bb6ecb0ebb..e3e4d6a24d 100644 --- a/docs/reference/process.md +++ b/docs/reference/process.md @@ -1391,6 +1391,7 @@ Resource labels are currently supported by the following executors: - {ref}`azurebatch-executor` - {ref}`google-batch-executor` - {ref}`k8s-executor` +- {ref}`seqera-executor` :::{note} The limits and the syntax of the corresponding executor should be taken into consideration when using resource labels. diff --git a/plugins/nf-seqera/VERSION b/plugins/nf-seqera/VERSION index c5523bd09b..66333910a4 100644 --- a/plugins/nf-seqera/VERSION +++ b/plugins/nf-seqera/VERSION @@ -1 +1 @@ -0.17.0 +0.18.0 diff --git a/plugins/nf-seqera/changelog.txt b/plugins/nf-seqera/changelog.txt index f67bcfb46f..64fc63861e 100644 --- a/plugins/nf-seqera/changelog.txt +++ b/plugins/nf-seqera/changelog.txt @@ -1,5 +1,10 @@ nf-seqera changelog ==================== +0.18.0 - 17 Apr 2026 +- Support process.resourceLabels: config-level labels attached to Sched run, per-task delta attached to Sched task +- Remove seqera.executor.labels config option (use process.resourceLabels instead) +- Bump sched-client@0.51.0 + 0.17.0 - 7 Apr 2026 - Add resourceAllocation field to trace record (#6973) [a2742939c] - Add compute env ID and provider support to Seqera executor (#6906) [4c2eb9390] From a7d3a1c0ff6a11fe07750d3484aeb4cfe209f585 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 17 Apr 2026 21:07:24 +0200 Subject: [PATCH 11/16] test(nf-seqera): cover createRun labels and tighten submit-test mocks Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Paolo Di Tommaso --- .../io/seqera/executor/SeqeraExecutor.groovy | 3 +- .../seqera/executor/SeqeraExecutorTest.groovy | 58 +++++++++++++++++++ .../executor/SeqeraTaskHandlerTest.groovy | 20 +++++-- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy b/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy index 216b0cadff..8bb05e4207 100644 --- a/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy @@ -17,6 +17,7 @@ package io.seqera.executor import groovy.transform.CompileStatic +import groovy.transform.PackageScope import groovy.util.logging.Slf4j import io.seqera.config.SeqeraConfig import io.seqera.config.ExecutorOpts @@ -210,7 +211,7 @@ class SeqeraExecutor extends Executor implements ExtensionPoint { return runResourceLabels } - @groovy.transform.PackageScope + @PackageScope void computeRunResourceLabels() { final processMap = session.config.process as Map final raw = processMap?.get('resourceLabels') as Map diff --git a/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraExecutorTest.groovy b/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraExecutorTest.groovy index 24d4cf23b7..78a8b338d2 100644 --- a/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraExecutorTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraExecutorTest.groovy @@ -16,11 +16,16 @@ package io.seqera.executor +import io.seqera.config.ExecutorOpts import io.seqera.config.SeqeraConfig +import io.seqera.sched.api.schema.v1a1.CreateRunRequest +import io.seqera.sched.api.schema.v1a1.CreateRunResponse +import io.seqera.sched.client.SchedClient import io.seqera.sched.client.SchedClientConfig import nextflow.Session import nextflow.SysEnv import nextflow.platform.PlatformHelper +import nextflow.script.WorkflowMetadata import spock.lang.Specification /** @@ -170,6 +175,59 @@ class SeqeraExecutorTest extends Specification { executor.runResourceLabels == [:] } + def 'createRun populates CreateRunRequest.labels with config-level resourceLabels merged with auto-labels'() { + given: + SysEnv.push([:]) + CreateRunRequest captured = null + def mockClient = Mock(SchedClient) { + createRun(_) >> { args -> + captured = args[0] as CreateRunRequest + new CreateRunResponse().runId('run-1') + } + } + def workflowMeta = Mock(WorkflowMetadata) { + getProjectName() >> 'my-project' + getUserName() >> 'alice' + getRunName() >> 'test-run' + getSessionId() >> UUID.fromString('00000000-0000-0000-0000-000000000001') + getResume() >> false + getRevision() >> null + getCommitId() >> null + getRepository() >> null + getManifest() >> null + getPlatform() >> null + } + def sessionConfig = [ + process: [resourceLabels: [team: 'platform', priority: 3]], + seqera: [executor: [endpoint: 'https://sched.example.com', provider: 'aws', region: 'us-east-1', autoLabels: true]], + tower: [:] + ] + def session = Mock(Session) { + getConfig() >> sessionConfig + getWorkflowMetadata() >> workflowMeta + getWorkDir() >> java.nio.file.Paths.get('/work') + getRunName() >> 'test-run' + } + def seqeraOpts = new ExecutorOpts(endpoint: 'https://sched.example.com', provider: 'aws', region: 'us-east-1', autoLabels: true) + def executor = new SeqeraExecutor() + executor.session = session + executor.@seqeraConfig = seqeraOpts + executor.@client = mockClient + + when: + executor.createRun() + + then: + captured != null + captured.getLabels()['team'] == 'platform' + captured.getLabels()['priority'] == '3' + captured.getLabels()['nextflow.io/projectName'] == 'my-project' + captured.getLabels()['nextflow.io/runName'] == 'test-run' + + cleanup: + executor.batchSubmitter?.shutdown() + } + /** * Builds a SchedClientConfig using the same logic as {@link SeqeraExecutor#createClient()} */ diff --git a/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraTaskHandlerTest.groovy b/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraTaskHandlerTest.groovy index 90421c9d30..1773ac8ae6 100644 --- a/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraTaskHandlerTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/executor/SeqeraTaskHandlerTest.groovy @@ -306,6 +306,7 @@ class SeqeraTaskHandlerTest extends Specification { } def executor = Mock(SeqeraExecutor) { getClient() >> client + getRunResourceLabels() >> [:] } def handler = Spy(new SeqeraTaskHandler(taskRun, executor)) { readExitFile() >> Integer.MAX_VALUE @@ -347,6 +348,7 @@ class SeqeraTaskHandlerTest extends Specification { } def executor = Mock(SeqeraExecutor) { getClient() >> client + getRunResourceLabels() >> [:] } def handler = Spy(new SeqeraTaskHandler(taskRun, executor)) { readExitFile() >> Integer.MAX_VALUE @@ -389,6 +391,7 @@ class SeqeraTaskHandlerTest extends Specification { } def executor = Mock(SeqeraExecutor) { getClient() >> client + getRunResourceLabels() >> [:] } def handler = Spy(new SeqeraTaskHandler(taskRun, executor)) { readExitFile() >> 1 @@ -418,6 +421,7 @@ class SeqeraTaskHandlerTest extends Specification { getClient() >> Mock(SchedClient) getBatchSubmitter() >> batchSubmitter getSeqeraConfig() >> seqeraConfig + getRunResourceLabels() >> [:] } def taskConfig = Mock(TaskConfig) { getCpus() >> 1 @@ -468,6 +472,7 @@ class SeqeraTaskHandlerTest extends Specification { getClient() >> Mock(SchedClient) getBatchSubmitter() >> batchSubmitter getSeqeraConfig() >> seqeraConfig + getRunResourceLabels() >> [:] } def taskConfig = Mock(TaskConfig) { getCpus() >> 1 @@ -512,6 +517,7 @@ class SeqeraTaskHandlerTest extends Specification { def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient) getSeqeraConfig() >> seqeraConfig + getRunResourceLabels() >> [:] } def taskRun = Mock(TaskRun) { getWorkDir() >> Paths.get('/work/ab/cd1234') @@ -538,6 +544,7 @@ class SeqeraTaskHandlerTest extends Specification { def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient) getSeqeraConfig() >> seqeraConfig + getRunResourceLabels() >> [:] } def taskRun = Mock(TaskRun) { getWorkDir() >> Paths.get('/work/ab/cd1234') @@ -564,6 +571,7 @@ class SeqeraTaskHandlerTest extends Specification { def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient) getSeqeraConfig() >> seqeraConfig + getRunResourceLabels() >> [:] } def taskRun = Mock(TaskRun) { getWorkDir() >> Paths.get('/work/ab/cd1234') @@ -679,7 +687,7 @@ class SeqeraTaskHandlerTest extends Specification { getWorkDir() >> Paths.get('/work/ab/cd1234') getConfig() >> Mock(TaskConfig) { getTime() >> Duration.of('6h') } } - def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient) } + def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient); getRunResourceLabels() >> [:] } def handler = new SeqeraTaskHandler(taskRun, executor) expect: @@ -696,7 +704,7 @@ class SeqeraTaskHandlerTest extends Specification { getWorkDir() >> Paths.get('/work/ab/cd1234') getConfig() >> taskConfig } - def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient) } + def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient); getRunResourceLabels() >> [:] } def handler = new SeqeraTaskHandler(taskRun, executor) when: @@ -718,7 +726,7 @@ class SeqeraTaskHandlerTest extends Specification { getWorkDir() >> Paths.get('/work/ab/cd1234') getConfig() >> taskConfig } - def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient) } + def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient); getRunResourceLabels() >> [:] } def handler = new SeqeraTaskHandler(taskRun, executor) when: @@ -740,7 +748,7 @@ class SeqeraTaskHandlerTest extends Specification { getWorkDir() >> Paths.get('/work/ab/cd1234') getConfig() >> taskConfig } - def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient) } + def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient); getRunResourceLabels() >> [:] } def handler = new SeqeraTaskHandler(taskRun, executor) when: @@ -763,6 +771,7 @@ class SeqeraTaskHandlerTest extends Specification { getClient() >> Mock(SchedClient) getBatchSubmitter() >> batchSubmitter getSeqeraConfig() >> seqeraConfig + getRunResourceLabels() >> [:] } def taskConfig = Mock(TaskConfig) { getCpus() >> 1 @@ -912,6 +921,7 @@ class SeqeraTaskHandlerTest extends Specification { } def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient) + getRunResourceLabels() >> [:] } return new SeqeraTaskHandler(taskRun, executor) } @@ -927,6 +937,7 @@ class SeqeraTaskHandlerTest extends Specification { } def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient) + getRunResourceLabels() >> [:] } return new SeqeraTaskHandler(taskRun, executor) } @@ -938,6 +949,7 @@ class SeqeraTaskHandlerTest extends Specification { def executor = Mock(SeqeraExecutor) { getClient() >> Mock(SchedClient) getName() >> 'seqera' + getRunResourceLabels() >> [:] } def processor = Mock(TaskProcessor) { getName() >> 'test_process' From 6273bd27e48f765c57a7210e83c3ddc4daeaaefd Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Fri, 17 Apr 2026 21:43:48 +0200 Subject: [PATCH 12/16] build(nf-seqera): bump sched-client to 0.52.0-SNAPSHOT Picks up the published artifact so the build resolves without the includeBuild composite. The transitive sched-api 0.52.0-SNAPSHOT is now also published. Signed-off-by: Paolo Di Tommaso --- plugins/nf-seqera/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/nf-seqera/build.gradle b/plugins/nf-seqera/build.gradle index c61f285a6f..5d79ff9457 100644 --- a/plugins/nf-seqera/build.gradle +++ b/plugins/nf-seqera/build.gradle @@ -51,7 +51,7 @@ dependencies { compileOnly project(':nextflow') compileOnly 'org.slf4j:slf4j-api:2.0.17' compileOnly 'org.pf4j:pf4j:3.14.1' - api 'io.seqera:sched-client:0.51.0' + api 'io.seqera:sched-client:0.52.0-SNAPSHOT' testImplementation(testFixtures(project(":nextflow"))) testImplementation "org.apache.groovy:groovy:4.0.31" From f496f01a90b4315baa80179ec6dc826ce612ed46 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Sun, 19 Apr 2026 14:28:53 +0200 Subject: [PATCH 13/16] Minor change [ci fast] Signed-off-by: Paolo Di Tommaso --- plugins/nf-seqera/build.gradle | 2 +- .../superpowers/plans/2026-04-17-seqera-resource-labels.md | 0 .../specs/2026-04-17-seqera-resource-labels-design.md | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename {docs => specs}/superpowers/plans/2026-04-17-seqera-resource-labels.md (100%) rename {docs => specs}/superpowers/specs/2026-04-17-seqera-resource-labels-design.md (100%) diff --git a/plugins/nf-seqera/build.gradle b/plugins/nf-seqera/build.gradle index 5d79ff9457..b1cba72385 100644 --- a/plugins/nf-seqera/build.gradle +++ b/plugins/nf-seqera/build.gradle @@ -51,7 +51,7 @@ dependencies { compileOnly project(':nextflow') compileOnly 'org.slf4j:slf4j-api:2.0.17' compileOnly 'org.pf4j:pf4j:3.14.1' - api 'io.seqera:sched-client:0.52.0-SNAPSHOT' + api 'io.seqera:sched-client:0.52.0' testImplementation(testFixtures(project(":nextflow"))) testImplementation "org.apache.groovy:groovy:4.0.31" diff --git a/docs/superpowers/plans/2026-04-17-seqera-resource-labels.md b/specs/superpowers/plans/2026-04-17-seqera-resource-labels.md similarity index 100% rename from docs/superpowers/plans/2026-04-17-seqera-resource-labels.md rename to specs/superpowers/plans/2026-04-17-seqera-resource-labels.md diff --git a/docs/superpowers/specs/2026-04-17-seqera-resource-labels-design.md b/specs/superpowers/specs/2026-04-17-seqera-resource-labels-design.md similarity index 100% rename from docs/superpowers/specs/2026-04-17-seqera-resource-labels-design.md rename to specs/superpowers/specs/2026-04-17-seqera-resource-labels-design.md From 012997630b2a6ce7259d59e5174300f779a57eb9 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Sun, 19 Apr 2026 14:31:32 +0200 Subject: [PATCH 14/16] Minor change [ci skip] Signed-off-by: Paolo Di Tommaso --- plugins/nf-seqera/VERSION | 2 +- plugins/nf-seqera/changelog.txt | 5 ----- settings.gradle | 2 -- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/plugins/nf-seqera/VERSION b/plugins/nf-seqera/VERSION index 66333910a4..c5523bd09b 100644 --- a/plugins/nf-seqera/VERSION +++ b/plugins/nf-seqera/VERSION @@ -1 +1 @@ -0.18.0 +0.17.0 diff --git a/plugins/nf-seqera/changelog.txt b/plugins/nf-seqera/changelog.txt index 64fc63861e..f67bcfb46f 100644 --- a/plugins/nf-seqera/changelog.txt +++ b/plugins/nf-seqera/changelog.txt @@ -1,10 +1,5 @@ nf-seqera changelog ==================== -0.18.0 - 17 Apr 2026 -- Support process.resourceLabels: config-level labels attached to Sched run, per-task delta attached to Sched task -- Remove seqera.executor.labels config option (use process.resourceLabels instead) -- Bump sched-client@0.51.0 - 0.17.0 - 7 Apr 2026 - Add resourceAllocation field to trace record (#6973) [a2742939c] - Add compute env ID and provider support to Seqera executor (#6906) [4c2eb9390] diff --git a/settings.gradle b/settings.gradle index f8dcbb3178..bb90d2b520 100644 --- a/settings.gradle +++ b/settings.gradle @@ -50,6 +50,4 @@ include 'plugins:nf-k8s' include 'plugins:nf-seqera' //includeBuild('../plugin-registry') - -// For local development against an unpublished sched-client, uncomment: //includeBuild '../sched' From 29a43406a8dbbeb78e729315c268d489c558dceb Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Sun, 19 Apr 2026 14:38:36 +0200 Subject: [PATCH 15/16] refactor(nf-seqera): defensive check and immutable run labels [ci fast] - Labels.toStringMap now accepts Object and throws IllegalArgumentException when the value is not a Map, giving a clear error when process.resourceLabels is misconfigured (e.g. as a list). - SeqeraExecutor.getRunResourceLabels wraps the cached map in Collections.unmodifiableMap, matching Labels.getEntries. Signed-off-by: Paolo Di Tommaso --- .../src/main/io/seqera/executor/Labels.groovy | 17 +++++++++++++---- .../io/seqera/executor/SeqeraExecutor.groovy | 5 ++--- .../test/io/seqera/executor/LabelsTest.groovy | 11 +++++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) 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 eb09007c92..199814954c 100644 --- a/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy @@ -111,12 +111,21 @@ class Labels { /** * Coerce arbitrary map values to strings via {@link String#valueOf}. - * Returns an empty map for null/empty input. + * Returns an empty map for null/empty input. Throws + * {@link IllegalArgumentException} when the value is not a {@link Map}, + * to surface a clear error when {@code process.resourceLabels} is + * misconfigured (e.g. as a list). */ - static Map toStringMap(Map map) { - if( !map ) return Collections.emptyMap() + static Map toStringMap(Object value) { + if( value == null ) + return Collections.emptyMap() + if( value !instanceof Map ) + throw new IllegalArgumentException("Invalid value for 'resourceLabels' directive - expected a map of key/value pairs, got '${value.getClass().getName()}'") + final map = (Map) value + if( map.isEmpty() ) + return Collections.emptyMap() final result = new LinkedHashMap(map.size()) - for( Map.Entry entry : map.entrySet() ) + for( Map.Entry entry : map.entrySet() ) result.put(entry.key.toString(), String.valueOf(entry.value)) return result } diff --git a/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy b/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy index 8bb05e4207..bc09ae4a1e 100644 --- a/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy @@ -208,14 +208,13 @@ class SeqeraExecutor extends Executor implements ExtensionPoint { } Map getRunResourceLabels() { - return runResourceLabels + return Collections.unmodifiableMap(runResourceLabels) } @PackageScope void computeRunResourceLabels() { final processMap = session.config.process as Map - final raw = processMap?.get('resourceLabels') as Map - this.runResourceLabels = Labels.toStringMap(raw) + this.runResourceLabels = Labels.toStringMap(processMap?.get('resourceLabels')) } SeqeraBatchSubmitter getBatchSubmitter() { 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 a22bbdfbad..2339aef330 100644 --- a/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy @@ -213,6 +213,17 @@ class LabelsTest extends Specification { Labels.toStringMap([a: 1, b: 'x', c: true]) == [a: '1', b: 'x', c: 'true'] } + def 'should reject non-map resourceLabels with a clear error'() { + when: + Labels.toStringMap(['foo', 'bar']) + + then: + def err = thrown(IllegalArgumentException) + err.message.contains("'resourceLabels'") + err.message.contains('map of key/value pairs') + err.message.contains('java.util.ArrayList') + } + def 'should compute null delta when task labels are empty'() { expect: Labels.delta(null, [team: 'a']) == null From d08352520e4d9bd407a6a3e7a2af8097798a3066 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Mon, 20 Apr 2026 18:45:38 +0200 Subject: [PATCH 16/16] docs: remove seqera.executor.labels reference [ci fast] Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Paolo Di Tommaso --- docs/reference/config.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/reference/config.md b/docs/reference/config.md index fc07e951f2..ba750c8ea8 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -1418,9 +1418,6 @@ The following settings are available: `seqera.executor.autoLabels` : When `true`, automatically adds workflow metadata labels to the session with the `nextflow.io/` prefix (default: `false`). The following labels are added: `projectName`, `userName`, `runName`, `sessionId`, `resume`, `revision`, `commitId`, `repository`, `manifestName`, `runtimeVersion`. A `seqera.io/runId` label is also added, computed as a SipHash of the session ID and run name. -`seqera.executor.labels` -: Custom labels to apply to AWS resources for cost tracking and resource organization. Labels are propagated to ECS tasks, capacity providers, and EC2 instances. When used together with `autoLabels`, user-defined labels take precedence over auto-generated labels. - `seqera.executor.machineRequirement.arch` : The CPU architecture for task execution, e.g. `'x86_64'` or `'arm64'`.