diff --git a/nodes/workflow-launch.html b/nodes/workflow-launch.html
index 96717b9..36942df 100644
--- a/nodes/workflow-launch.html
+++ b/nodes/workflow-launch.html
@@ -24,6 +24,12 @@
+
@@ -60,6 +66,7 @@
: Launchpad (string) : The human-readable name of a pipeline in the launchpad to launch. Supports autocomplete.
: Run name (string) : Custom name for the workflow run (optional, defaults to auto-generated name).
: Resume from (string) : Workflow ID from a previous run to resume (optional). Can be extracted from workflow monitor output using `msg.workflowId`.
+: Labels (string) : Comma-separated label names to assign to the workflow run (optional), e.g., `production,rnaseq,urgent`. Labels are automatically created if they don't exist (requires API token with Maintain role or higher). The node resolves names to IDs automatically.
: Parameters (array) : Individual parameter key-value pairs added via the editable list. Each parameter can be configured with a name and a value of any type (string, number, boolean, JSON, etc.). These take highest precedence when merging.
: Params JSON (object) : A JSON object containing multiple parameters to merge with the launchpad's default parameters. Merged before individual parameters.
: Workspace ID (string) : Override the workspace ID from the Seqera config node (optional).
@@ -95,6 +102,16 @@
The resumed workflow must use the same work directory as the original run, so ensure your compute environment has access to the cached results.
+#### Labels
+
+Labels help organize and track workflow runs in your workspace. Simply provide comma-separated label names (e.g., `production,rnaseq,urgent`) and the node will:
+
+1. Check if labels exist in the workspace
+2. Automatically create missing labels (requires Maintain role or higher)
+3. Resolve names to IDs and apply them to the workflow run
+
+**Token permissions:** Your API token needs Maintain role or higher to create labels automatically. Without sufficient permissions, you'll need to create labels manually in the Seqera UI first.
+
### References
- [Seqera Platform API docs](https://docs.seqera.io/platform/latest/api) - information about launching workflows
@@ -125,6 +142,8 @@
runNameType: { value: "str" },
resumeWorkflowId: { value: "" },
resumeWorkflowIdType: { value: "str" },
+ labels: { value: "" },
+ labelsType: { value: "str" },
workspaceId: { value: "" },
workspaceIdType: { value: "str" },
sourceWorkspaceId: { value: "" },
@@ -156,6 +175,14 @@
$("#node-input-resumeWorkflowId").typedInput("value", this.resumeWorkflowId || "");
$("#node-input-resumeWorkflowId").typedInput("type", this.resumeWorkflowIdType || "str");
+ // Initialize labels typedInput (comma-separated string by default)
+ $("#node-input-labels").typedInput({
+ default: "str",
+ types: ["str", "msg", "flow", "global", "env", "jsonata", "json"],
+ });
+ $("#node-input-labels").typedInput("value", this.labels || "");
+ $("#node-input-labels").typedInput("type", this.labelsType || "str");
+
ti("#node-input-workspaceId", this.workspaceId || "", this.workspaceIdType || "str");
ti("#node-input-sourceWorkspaceId", this.sourceWorkspaceId || "", this.sourceWorkspaceIdType || "str");
@@ -298,6 +325,7 @@
save.call(this, "#node-input-paramsKey", "paramsKey", "paramsKeyType");
save.call(this, "#node-input-runName", "runName", "runNameType");
save.call(this, "#node-input-resumeWorkflowId", "resumeWorkflowId", "resumeWorkflowIdType");
+ save.call(this, "#node-input-labels", "labels", "labelsType");
save.call(this, "#node-input-workspaceId", "workspaceId", "workspaceIdType");
save.call(this, "#node-input-sourceWorkspaceId", "sourceWorkspaceId", "sourceWorkspaceIdType");
diff --git a/nodes/workflow-launch.js b/nodes/workflow-launch.js
index 1709ea9..342c3ef 100644
--- a/nodes/workflow-launch.js
+++ b/nodes/workflow-launch.js
@@ -97,6 +97,8 @@ module.exports = function (RED) {
node.sourceWorkspaceIdPropType = config.sourceWorkspaceIdType;
node.resumeWorkflowIdProp = config.resumeWorkflowId;
node.resumeWorkflowIdPropType = config.resumeWorkflowIdType;
+ node.labelsProp = config.labels;
+ node.labelsPropType = config.labelsType;
node.seqeraConfig = RED.nodes.getNode(config.seqera);
node.defaultBaseUrl = (node.seqeraConfig && node.seqeraConfig.baseUrl) || "https://api.cloud.seqera.io";
@@ -113,6 +115,74 @@ module.exports = function (RED) {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${d.toLocaleTimeString()}`;
};
+ // Helper function to resolve label names to IDs, creating labels if needed
+ const resolveLabelIds = async (labelInput, workspaceId, baseUrl, msg) => {
+ if (!labelInput || !workspaceId) {
+ return [];
+ }
+
+ // Parse label names from comma-separated string (typedInput always returns string)
+ // Normalize to lowercase since platform treats labels as case-insensitive
+ const labelNames = String(labelInput)
+ .split(",")
+ .map((l) => l.trim().toLowerCase())
+ .filter(Boolean);
+
+ if (labelNames.length === 0) {
+ return [];
+ }
+
+ const labelIds = [];
+
+ for (const labelName of labelNames) {
+ // Query for specific label by name using search parameter to avoid pagination issues
+ // Note: API search returns partial matches, so we use find() to ensure exact match
+ const searchUrl = `${baseUrl.replace(/\/$/, "")}/labels?workspaceId=${workspaceId}&search=${encodeURIComponent(
+ labelName,
+ )}`;
+ const searchResp = await apiCall(node, "get", searchUrl);
+ const labels = searchResp.data?.labels || [];
+ // Case-insensitive comparison since platform normalizes label names
+ let match = labels.find((l) => l.name.toLowerCase() === labelName);
+
+ if (!match) {
+ // Label doesn't exist, try to create it
+ try {
+ const createUrl = `${baseUrl.replace(/\/$/, "")}/labels?workspaceId=${workspaceId}`;
+ const createResp = await apiCall(node, "post", createUrl, {
+ data: { name: labelName },
+ });
+
+ if (createResp.data?.id) {
+ match = createResp.data;
+ node.log(`Created label '${labelName}' (ID: ${match.id})`);
+ } else {
+ throw new Error(`Failed to create label '${labelName}': API returned no ID`);
+ }
+ } catch (errCreate) {
+ const errorStatus = errCreate.response?.status;
+ const errorDetail = errCreate.response?.data?.message || errCreate.response?.data || errCreate.message;
+
+ if (errorStatus === 403) {
+ throw new Error(
+ `Cannot create label '${labelName}': API token requires 'Maintain' role or higher. Use a token with sufficient permissions or create labels manually in Seqera UI.`,
+ );
+ } else {
+ throw new Error(`Failed to create label '${labelName}': ${errorDetail}`);
+ }
+ }
+ }
+
+ if (match?.id) {
+ labelIds.push(String(match.id));
+ } else {
+ throw new Error(`Failed to resolve label '${labelName}': No ID found`);
+ }
+ }
+
+ return labelIds;
+ };
+
node.on("input", async function (msg, send, done) {
node.status({ fill: "blue", shape: "ring", text: `launching: ${formatDateTime()}` });
@@ -137,6 +207,7 @@ module.exports = function (RED) {
const workspaceIdOverride = await evalProp(node.workspaceIdProp, node.workspaceIdPropType);
const sourceWorkspaceIdOverride = await evalProp(node.sourceWorkspaceIdProp, node.sourceWorkspaceIdPropType);
const resumeWorkflowId = await evalProp(node.resumeWorkflowIdProp, node.resumeWorkflowIdPropType);
+ const labels = await evalProp(node.labelsProp, node.labelsPropType);
const baseUrl = baseUrlOverride || (node.seqeraConfig && node.seqeraConfig.baseUrl) || node.defaultBaseUrl;
const workspaceId = workspaceIdOverride || (node.seqeraConfig && node.seqeraConfig.workspaceId) || null;
@@ -292,6 +363,22 @@ module.exports = function (RED) {
}
}
+ // Resolve label names to IDs if provided, creating labels as needed
+ // Applied after both regular launch and resume paths have set up body.launch
+ if (labels) {
+ body.launch = body.launch || {};
+ try {
+ const labelIds = await resolveLabelIds(labels, workspaceId, baseUrl, msg);
+ if (labelIds.length > 0) {
+ body.launch.labelIds = labelIds;
+ }
+ } catch (errLabels) {
+ node.error(`Failed to resolve labels: ${errLabels.message}`, msg);
+ node.status({ fill: "red", shape: "ring", text: `error: ${formatDateTime()}` });
+ return;
+ }
+ }
+
// Build URL with query params
let url = `${baseUrl.replace(/\/$/, "")}/workflow/launch`;
const qs = new URLSearchParams();
@@ -313,7 +400,15 @@ module.exports = function (RED) {
send(outMsg);
if (done) done();
} catch (err) {
- node.error(`Seqera API request failed: ${err.message}\nRequest: POST ${url}`, msg);
+ const errorDetail = err.response?.data?.message || err.response?.data || err.message;
+ const errorStatus = err.response?.status;
+ let errorMsg = `Seqera API request failed: ${errorDetail}\nRequest: POST ${url}`;
+
+ if (errorStatus === 403) {
+ errorMsg += `\n\nPermission denied. Please check:\n1. API token has 'Launch' or 'Maintain' role\n2. Workspace ID ${workspaceId} is correct\n3. Token has access to this workspace`;
+ }
+
+ node.error(errorMsg, msg);
node.status({ fill: "red", shape: "ring", text: `error: ${formatDateTime()}` });
return;
}
@@ -341,6 +436,8 @@ module.exports = function (RED) {
sourceWorkspaceIdType: { value: "str" },
resumeWorkflowId: { value: "" },
resumeWorkflowIdType: { value: "str" },
+ labels: { value: "" },
+ labelsType: { value: "str" },
token: { value: "token" },
tokenType: { value: "str" },
},
diff --git a/test/helper.js b/test/helper.js
index 3beef44..9bec45b 100644
--- a/test/helper.js
+++ b/test/helper.js
@@ -272,6 +272,44 @@ function createWorkspacesResponse(workspaces = []) {
};
}
+/**
+ * Creates a mock labels response object.
+ *
+ * @param {Array} labels - Labels to include
+ * @returns {Object} A labels response object
+ */
+function createLabelsResponse(labels = []) {
+ const defaultLabels = [
+ {
+ id: "1",
+ name: "production",
+ resource: true,
+ isDefault: false,
+ lastUsed: "2024-01-01T00:00:00Z",
+ },
+ ];
+
+ return {
+ labels: labels.length > 0 ? labels : defaultLabels,
+ };
+}
+
+/**
+ * Creates a mock label creation response object.
+ *
+ * @param {Object} overrides - Properties to override
+ * @returns {Object} A label response object
+ */
+function createLabelResponse(overrides = {}) {
+ return {
+ id: "1",
+ name: "test-label",
+ resource: true,
+ isDefault: false,
+ ...overrides,
+ };
+}
+
/**
* Helper to wait for a node to emit a message.
* Wraps the assertion in try/catch to properly report failures.
@@ -334,6 +372,8 @@ module.exports = {
createUserInfoResponse,
createOrganizationsResponse,
createWorkspacesResponse,
+ createLabelsResponse,
+ createLabelResponse,
expectMessage,
expectMessages,
};
diff --git a/test/workflow-launch_spec.js b/test/workflow-launch_spec.js
index ee9e65a..17e1ac5 100644
--- a/test/workflow-launch_spec.js
+++ b/test/workflow-launch_spec.js
@@ -6,6 +6,7 @@
* - Parameter merging (paramsJSON and params array)
* - Resume workflow functionality
* - Custom run names
+ * - Label resolution and creation
* - Error handling
*/
@@ -19,6 +20,8 @@ const {
createPipelinesResponse,
createLaunchConfigResponse,
createWorkflowResponse,
+ createLabelsResponse,
+ createLabelResponse,
} = require("./helper");
const { expect } = require("chai");
@@ -567,6 +570,513 @@ describe("seqera-workflow-launch Node", function () {
});
});
+ describe("labels", function () {
+ it("should resolve existing label names to IDs and launch with labels", function (done) {
+ const flow = [
+ createConfigNode(),
+ {
+ id: "launch1",
+ type: "seqera-workflow-launch",
+ name: "Test Launch",
+ seqera: "config-node-1",
+ launchpadName: "my-pipeline",
+ launchpadNameType: "str",
+ labels: "production,rnaseq",
+ labelsType: "str",
+ wires: [["helper1"]],
+ },
+ { id: "helper1", type: "helper" },
+ ];
+
+ // Mock pipelines search
+ nock(DEFAULT_BASE_URL)
+ .get("/pipelines")
+ .query(true)
+ .reply(200, createPipelinesResponse([{ pipelineId: 42, name: "my-pipeline" }]));
+
+ // Mock launch config fetch
+ nock(DEFAULT_BASE_URL).get(`/pipelines/42/launch`).query(true).reply(200, createLaunchConfigResponse());
+
+ // Mock label search for "production"
+ nock(DEFAULT_BASE_URL)
+ .get("/labels")
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID, search: "production" })
+ .reply(200, createLabelsResponse([{ id: "101", name: "production" }]));
+
+ // Mock label search for "rnaseq"
+ nock(DEFAULT_BASE_URL)
+ .get("/labels")
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID, search: "rnaseq" })
+ .reply(200, createLabelsResponse([{ id: "102", name: "rnaseq" }]));
+
+ // Mock workflow launch - verify labelIds are included
+ nock(DEFAULT_BASE_URL)
+ .post("/workflow/launch", (body) => {
+ return (
+ body.launch.labelIds &&
+ body.launch.labelIds.length === 2 &&
+ body.launch.labelIds.includes("101") &&
+ body.launch.labelIds.includes("102")
+ );
+ })
+ .query(true)
+ .reply(200, { workflowId: "wf-12345" });
+
+ helper.load([configNode, workflowLaunchNode], flow, createCredentials(), function () {
+ const launchNode = helper.getNode("launch1");
+ const helperNode = helper.getNode("helper1");
+
+ helperNode.on("input", function (msg) {
+ try {
+ expect(msg.workflowId).to.equal("wf-12345");
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+
+ launchNode.receive({ payload: {} });
+ });
+ });
+
+ it("should create missing labels automatically when they don't exist", function (done) {
+ const flow = [
+ createConfigNode(),
+ {
+ id: "launch1",
+ type: "seqera-workflow-launch",
+ name: "Test Launch",
+ seqera: "config-node-1",
+ launchpadName: "my-pipeline",
+ launchpadNameType: "str",
+ labels: "newlabel",
+ labelsType: "str",
+ wires: [["helper1"]],
+ },
+ { id: "helper1", type: "helper" },
+ ];
+
+ // Mock pipelines search
+ nock(DEFAULT_BASE_URL)
+ .get("/pipelines")
+ .query(true)
+ .reply(200, createPipelinesResponse([{ pipelineId: 42, name: "my-pipeline" }]));
+
+ // Mock launch config fetch
+ nock(DEFAULT_BASE_URL).get(`/pipelines/42/launch`).query(true).reply(200, createLaunchConfigResponse());
+
+ // Mock label search - return empty results
+ nock(DEFAULT_BASE_URL)
+ .get("/labels")
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID, search: "newlabel" })
+ .reply(200, createLabelsResponse([]));
+
+ // Mock label creation
+ nock(DEFAULT_BASE_URL)
+ .post("/labels", { name: "newlabel" })
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID })
+ .reply(200, createLabelResponse({ id: "201", name: "newlabel" }));
+
+ // Mock workflow launch
+ nock(DEFAULT_BASE_URL)
+ .post("/workflow/launch", (body) => {
+ return body.launch.labelIds && body.launch.labelIds.includes("201");
+ })
+ .query(true)
+ .reply(200, { workflowId: "wf-12345" });
+
+ helper.load([configNode, workflowLaunchNode], flow, createCredentials(), function () {
+ const launchNode = helper.getNode("launch1");
+ const helperNode = helper.getNode("helper1");
+
+ helperNode.on("input", function (msg) {
+ try {
+ expect(msg.workflowId).to.equal("wf-12345");
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+
+ launchNode.receive({ payload: {} });
+ });
+ });
+
+ it("should handle case-insensitive label matching", function (done) {
+ const flow = [
+ createConfigNode(),
+ {
+ id: "launch1",
+ type: "seqera-workflow-launch",
+ name: "Test Launch",
+ seqera: "config-node-1",
+ launchpadName: "my-pipeline",
+ launchpadNameType: "str",
+ labels: "Production",
+ labelsType: "str",
+ wires: [["helper1"]],
+ },
+ { id: "helper1", type: "helper" },
+ ];
+
+ // Mock pipelines search
+ nock(DEFAULT_BASE_URL)
+ .get("/pipelines")
+ .query(true)
+ .reply(200, createPipelinesResponse([{ pipelineId: 42, name: "my-pipeline" }]));
+
+ // Mock launch config fetch
+ nock(DEFAULT_BASE_URL).get(`/pipelines/42/launch`).query(true).reply(200, createLaunchConfigResponse());
+
+ // Mock label search - API normalizes to lowercase
+ nock(DEFAULT_BASE_URL)
+ .get("/labels")
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID, search: "production" })
+ .reply(200, createLabelsResponse([{ id: "101", name: "production" }]));
+
+ // Mock workflow launch
+ nock(DEFAULT_BASE_URL)
+ .post("/workflow/launch", (body) => {
+ return body.launch.labelIds && body.launch.labelIds.includes("101");
+ })
+ .query(true)
+ .reply(200, { workflowId: "wf-12345" });
+
+ helper.load([configNode, workflowLaunchNode], flow, createCredentials(), function () {
+ const launchNode = helper.getNode("launch1");
+ const helperNode = helper.getNode("helper1");
+
+ helperNode.on("input", function (msg) {
+ try {
+ expect(msg.workflowId).to.equal("wf-12345");
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+
+ launchNode.receive({ payload: {} });
+ });
+ });
+
+ it("should handle empty labels gracefully", function (done) {
+ const flow = [
+ createConfigNode(),
+ {
+ id: "launch1",
+ type: "seqera-workflow-launch",
+ name: "Test Launch",
+ seqera: "config-node-1",
+ launchpadName: "my-pipeline",
+ launchpadNameType: "str",
+ labels: "",
+ labelsType: "str",
+ wires: [["helper1"]],
+ },
+ { id: "helper1", type: "helper" },
+ ];
+
+ // Mock pipelines search
+ nock(DEFAULT_BASE_URL)
+ .get("/pipelines")
+ .query(true)
+ .reply(200, createPipelinesResponse([{ pipelineId: 42, name: "my-pipeline" }]));
+
+ // Mock launch config fetch
+ nock(DEFAULT_BASE_URL).get(`/pipelines/42/launch`).query(true).reply(200, createLaunchConfigResponse());
+
+ // Mock workflow launch - should not include labelIds
+ nock(DEFAULT_BASE_URL)
+ .post("/workflow/launch", (body) => {
+ return !body.launch.labelIds || body.launch.labelIds.length === 0;
+ })
+ .query(true)
+ .reply(200, { workflowId: "wf-12345" });
+
+ helper.load([configNode, workflowLaunchNode], flow, createCredentials(), function () {
+ const launchNode = helper.getNode("launch1");
+ const helperNode = helper.getNode("helper1");
+
+ helperNode.on("input", function (msg) {
+ try {
+ expect(msg.workflowId).to.equal("wf-12345");
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+
+ launchNode.receive({ payload: {} });
+ });
+ });
+
+ it("should handle whitespace in comma-separated labels", function (done) {
+ const flow = [
+ createConfigNode(),
+ {
+ id: "launch1",
+ type: "seqera-workflow-launch",
+ name: "Test Launch",
+ seqera: "config-node-1",
+ launchpadName: "my-pipeline",
+ launchpadNameType: "str",
+ labels: " production , rnaseq , urgent ",
+ labelsType: "str",
+ wires: [["helper1"]],
+ },
+ { id: "helper1", type: "helper" },
+ ];
+
+ // Mock pipelines search
+ nock(DEFAULT_BASE_URL)
+ .get("/pipelines")
+ .query(true)
+ .reply(200, createPipelinesResponse([{ pipelineId: 42, name: "my-pipeline" }]));
+
+ // Mock launch config fetch
+ nock(DEFAULT_BASE_URL).get(`/pipelines/42/launch`).query(true).reply(200, createLaunchConfigResponse());
+
+ // Mock label searches - should trim whitespace
+ nock(DEFAULT_BASE_URL)
+ .get("/labels")
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID, search: "production" })
+ .reply(200, createLabelsResponse([{ id: "101", name: "production" }]));
+
+ nock(DEFAULT_BASE_URL)
+ .get("/labels")
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID, search: "rnaseq" })
+ .reply(200, createLabelsResponse([{ id: "102", name: "rnaseq" }]));
+
+ nock(DEFAULT_BASE_URL)
+ .get("/labels")
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID, search: "urgent" })
+ .reply(200, createLabelsResponse([{ id: "103", name: "urgent" }]));
+
+ // Mock workflow launch
+ nock(DEFAULT_BASE_URL)
+ .post("/workflow/launch", (body) => {
+ return body.launch.labelIds && body.launch.labelIds.length === 3;
+ })
+ .query(true)
+ .reply(200, { workflowId: "wf-12345" });
+
+ helper.load([configNode, workflowLaunchNode], flow, createCredentials(), function () {
+ const launchNode = helper.getNode("launch1");
+ const helperNode = helper.getNode("helper1");
+
+ helperNode.on("input", function (msg) {
+ try {
+ expect(msg.workflowId).to.equal("wf-12345");
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+
+ launchNode.receive({ payload: {} });
+ });
+ });
+
+ it("should error when lacking permission to create labels", function (done) {
+ const flow = [
+ createConfigNode(),
+ {
+ id: "launch1",
+ type: "seqera-workflow-launch",
+ name: "Test Launch",
+ seqera: "config-node-1",
+ launchpadName: "my-pipeline",
+ launchpadNameType: "str",
+ labels: "newlabel",
+ labelsType: "str",
+ wires: [["helper1"]],
+ },
+ { id: "helper1", type: "helper" },
+ ];
+
+ // Mock pipelines search
+ nock(DEFAULT_BASE_URL)
+ .get("/pipelines")
+ .query(true)
+ .reply(200, createPipelinesResponse([{ pipelineId: 42, name: "my-pipeline" }]));
+
+ // Mock launch config fetch
+ nock(DEFAULT_BASE_URL).get(`/pipelines/42/launch`).query(true).reply(200, createLaunchConfigResponse());
+
+ // Mock label search - return empty results
+ nock(DEFAULT_BASE_URL)
+ .get("/labels")
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID, search: "newlabel" })
+ .reply(200, createLabelsResponse([]));
+
+ // Mock label creation - return 403 forbidden
+ nock(DEFAULT_BASE_URL)
+ .post("/labels", { name: "newlabel" })
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID })
+ .reply(403, { message: "Insufficient permissions" });
+
+ helper.load([configNode, workflowLaunchNode], flow, createCredentials(), function () {
+ const launchNode = helper.getNode("launch1");
+
+ launchNode.on("call:error", function (call) {
+ try {
+ expect(call.firstArg).to.include("Cannot create label 'newlabel'");
+ expect(call.firstArg).to.include("Maintain");
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+
+ launchNode.receive({ payload: {} });
+ });
+ });
+
+ it("should work with labels in resume workflow", function (done) {
+ const flow = [
+ createConfigNode(),
+ {
+ id: "launch1",
+ type: "seqera-workflow-launch",
+ name: "Test Launch",
+ seqera: "config-node-1",
+ resumeWorkflowId: "wf-original-123",
+ resumeWorkflowIdType: "str",
+ labels: "resumed,retry",
+ labelsType: "str",
+ wires: [["helper1"]],
+ },
+ { id: "helper1", type: "helper" },
+ ];
+
+ // Mock get workflow details
+ nock(DEFAULT_BASE_URL)
+ .get("/workflow/wf-original-123")
+ .query(true)
+ .reply(
+ 200,
+ createWorkflowResponse({
+ id: "wf-original-123",
+ commitId: "abc123",
+ }),
+ );
+
+ // Mock get workflow launch config
+ nock(DEFAULT_BASE_URL)
+ .get("/workflow/wf-original-123/launch")
+ .query(true)
+ .reply(
+ 200,
+ createLaunchConfigResponse({
+ sessionId: "session-123",
+ resumeCommitId: "abc123",
+ }),
+ );
+
+ // Mock label searches
+ nock(DEFAULT_BASE_URL)
+ .get("/labels")
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID, search: "resumed" })
+ .reply(200, createLabelsResponse([{ id: "301", name: "resumed" }]));
+
+ nock(DEFAULT_BASE_URL)
+ .get("/labels")
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID, search: "retry" })
+ .reply(200, createLabelsResponse([{ id: "302", name: "retry" }]));
+
+ // Mock workflow launch with resume and labels
+ nock(DEFAULT_BASE_URL)
+ .post("/workflow/launch", (body) => {
+ return (
+ body.launch.resume === true &&
+ body.launch.labelIds &&
+ body.launch.labelIds.includes("301") &&
+ body.launch.labelIds.includes("302")
+ );
+ })
+ .query(true)
+ .reply(200, { workflowId: "wf-resumed-456" });
+
+ helper.load([configNode, workflowLaunchNode], flow, createCredentials(), function () {
+ const launchNode = helper.getNode("launch1");
+ const helperNode = helper.getNode("helper1");
+
+ helperNode.on("input", function (msg) {
+ try {
+ expect(msg.workflowId).to.equal("wf-resumed-456");
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+
+ launchNode.receive({
+ payload: { launch: {} },
+ });
+ });
+ });
+
+ it("should work with labels from message property", function (done) {
+ const flow = [
+ createConfigNode(),
+ {
+ id: "launch1",
+ type: "seqera-workflow-launch",
+ name: "Test Launch",
+ seqera: "config-node-1",
+ launchpadName: "my-pipeline",
+ launchpadNameType: "str",
+ labels: "customLabels",
+ labelsType: "msg",
+ wires: [["helper1"]],
+ },
+ { id: "helper1", type: "helper" },
+ ];
+
+ // Mock pipelines search
+ nock(DEFAULT_BASE_URL)
+ .get("/pipelines")
+ .query(true)
+ .reply(200, createPipelinesResponse([{ pipelineId: 42, name: "my-pipeline" }]));
+
+ // Mock launch config fetch
+ nock(DEFAULT_BASE_URL).get(`/pipelines/42/launch`).query(true).reply(200, createLaunchConfigResponse());
+
+ // Mock label search
+ nock(DEFAULT_BASE_URL)
+ .get("/labels")
+ .query({ workspaceId: DEFAULT_WORKSPACE_ID, search: "dynamic" })
+ .reply(200, createLabelsResponse([{ id: "401", name: "dynamic" }]));
+
+ // Mock workflow launch
+ nock(DEFAULT_BASE_URL)
+ .post("/workflow/launch", (body) => {
+ return body.launch.labelIds && body.launch.labelIds.includes("401");
+ })
+ .query(true)
+ .reply(200, { workflowId: "wf-12345" });
+
+ helper.load([configNode, workflowLaunchNode], flow, createCredentials(), function () {
+ const launchNode = helper.getNode("launch1");
+ const helperNode = helper.getNode("helper1");
+
+ helperNode.on("input", function (msg) {
+ try {
+ expect(msg.workflowId).to.equal("wf-12345");
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+
+ launchNode.receive({
+ payload: {},
+ customLabels: "dynamic",
+ });
+ });
+ });
+ });
+
describe("error handling", function () {
it("should report error when no body and no launchpad name", function (done) {
const flow = [