From bf88754223f1033483bc8a0eba7251d088ed72aa Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Fri, 13 Feb 2026 10:36:11 +0900 Subject: [PATCH 01/17] stop running build and deploy workflow after accepting PR for non-master branch --- .github/workflows/build_and_deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml index 47473dac3..8a43efac0 100644 --- a/.github/workflows/build_and_deploy.yml +++ b/.github/workflows/build_and_deploy.yml @@ -1,6 +1,8 @@ name: build and deploy container on: pull_request_target: + branches: + - master types: - closed jobs: From 06e58e2ec4a122f14efb365a0eeef95a3cc161c9 Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Fri, 13 Feb 2026 10:32:13 +0900 Subject: [PATCH 02/17] remoteJobExecuter handle task.sourceScript --- server/app/core/executerManager.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/app/core/executerManager.js b/server/app/core/executerManager.js index 103251ab0..61449476f 100644 --- a/server/app/core/executerManager.js +++ b/server/app/core/executerManager.js @@ -46,6 +46,18 @@ function isExceededLimit(JS, rt, outputText) { return false; } +/** + * make source command for remote execution + * @param {object} task - task component instance + * @returns {string} - source command string + */ +function source(task) { + if (typeof task.soruceScript === "undefined" || task.soruceScript === null || task.soruceScript.length === 0) { + return ""; + } + return `&& source ${task.soruceScript}`; +} + /** * convert env object to cmandline string * @param {object} task - task component instance @@ -273,7 +285,7 @@ class RemoteJobExecuter extends Executer { async exec(task) { const hostinfo = _internal.getSshHostinfo(task.projectRootDir, task.remotehostID); const submitOpt = task.submitOption ? task.submitOption : ""; - const submitCmd = `cd ${task.remoteWorkingDir} && ${makeEnv(task)} ${this.JS.submit} ${makeQueueOpt(task, this.JS, this.queues)} ${makeStepOpt(task)} ${makeBulkOpt(task)} ${submitOpt} ./${task.script}`; + const submitCmd = `cd ${task.remoteWorkingDir} ${source(task)} && ${makeEnv(task)} ${this.JS.submit} ${makeQueueOpt(task, this.JS, this.queues)} ${makeStepOpt(task)} ${makeBulkOpt(task)} ${submitOpt} ./${task.script}`; loggerWrapper.logDebug(task.projectRootDir, task.workingDir, "submitting job (remote):", submitCmd); await setTaskState(task, "running"); const ssh = getSsh(task.projectRootDir, task.remotehostID); From 71bdfe467a2ab495ab45d303cfdb6f88025e9888 Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Fri, 13 Feb 2026 17:24:49 +0900 Subject: [PATCH 03/17] format json files --- server/app/db/jobScheduler.json | 2 +- server/app/db/server.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/app/db/jobScheduler.json b/server/app/db/jobScheduler.json index aa56eed2a..0860bbc79 100644 --- a/server/app/db/jobScheduler.json +++ b/server/app/db/jobScheduler.json @@ -52,7 +52,7 @@ "reSubJobStatusCode": "(?:\\d+\\[\\d+\\]) *(?:CCL|ERR|EXT|RJT) *\\d+ *(\\d+)", "reExceededLimitError": "PJM 0072 pjsub Job exceeded the accept limit", "acceptableJobStatus": [ - 0,6 + 0, 6 ] } } diff --git a/server/app/db/server.json b/server/app/db/server.json index 7a489bd5e..eac8fdd8d 100644 --- a/server/app/db/server.json +++ b/server/app/db/server.json @@ -6,4 +6,4 @@ "withLogin": false, "userid": null, "enablePassword": true -} \ No newline at end of file +} From e67e77c7ee0c18606662a49b901ea82f65b4274c Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Fri, 13 Feb 2026 17:25:21 +0900 Subject: [PATCH 04/17] add sourceScript input field to task component's property sub-screen --- client/src/components/componentProperty.vue | 11 +++++++ test/cypress/e2e/components/task.cy.js | 35 +++++++++++++++++++++ test/wheel_config/server.json | 2 +- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/client/src/components/componentProperty.vue b/client/src/components/componentProperty.vue index 56b8d982a..9647bbb99 100644 --- a/client/src/components/componentProperty.vue +++ b/client/src/components/componentProperty.vue @@ -151,6 +151,17 @@ variant="outlined" data-cy="component_property-submit_option-text_field" /> + { .should("have.value", "test-a"); }); + /** + Task コンポーネントの基本機能動作確認 + Taskコンポーネント機能確認 + プロパティ設定確認 + source script表示確認 + 試験確認内容:source scriptセレクトボックスが表示されていることを確認 + */ + it("プロパティ設定確認-source script表示確認-source scriptセレクトボックスが表示されていることを確認", ()=>{ + const DATA_CY_STR = "[data-cy=\"component_property-source_script-autocomplete\"]"; + cy.confirmDisplayInProperty(DATA_CY_STR, true); + cy.get(DATA_CY_STR).find("input") + .should("be.disabled"); + }); + + /** + Task コンポーネントの基本機能動作確認 + Taskコンポーネント機能確認 + プロパティ設定確認 + source scriptファイル選択表示確認 + 試験確認内容:source scriptセレクトボックスで選択したファイルが表示されていることを確認 + */ + it("プロパティ設定確認-source scriptファイル選択表示確認-source scriptセレクトボックスで選択したファイルが表示されていることを確認", ()=>{ + const SWITCH_CY = "[data-cy=\"component_property-job_scheduler-switch\"]"; + const FIELD_CY = "[data-cy=\"component_property-source_script-autocomplete\"]"; + cy.get(SWITCH_CY).click({ force: true }); + cy.createDirOrFile(TYPE_FILE, "env.sh", true); + let targetDropBoxCy = FIELD_CY; + cy.selectValueFromDropdownList(targetDropBoxCy, 3, "env.sh"); + cy.get(FIELD_CY).find("input") + .should("have.value", "env.sh"); + cy.get(SWITCH_CY).click({ force: true }); + cy.get(FIELD_CY).find("input") + .should("be.disabled"); + }); + /** Task コンポーネントの基本機能動作確認 Taskコンポーネント機能確認 diff --git a/test/wheel_config/server.json b/test/wheel_config/server.json index 7a489bd5e..eac8fdd8d 100644 --- a/test/wheel_config/server.json +++ b/test/wheel_config/server.json @@ -6,4 +6,4 @@ "withLogin": false, "userid": null, "enablePassword": true -} \ No newline at end of file +} From 081c9c9bbd3b81d1b1b2e45b712de471af049008 Mon Sep 17 00:00:00 2001 From: "version-number-updater[bot]" Date: Fri, 13 Feb 2026 17:31:05 +0900 Subject: [PATCH 05/17] [skip ci] update version number --- server/app/db/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/db/version.json b/server/app/db/version.json index ff556c900..15a689e4d 100644 --- a/server/app/db/version.json +++ b/server/app/db/version.json @@ -1 +1 @@ -{"version": "2026-0213-103108" } \ No newline at end of file +{"version": "2026-0213-173105" } \ No newline at end of file From e94b2bf039c0d5561a6ba50aae9f64fd6c5682ef Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Fri, 13 Feb 2026 23:06:06 +0900 Subject: [PATCH 06/17] fix typo --- server/app/core/executerManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/app/core/executerManager.js b/server/app/core/executerManager.js index 61449476f..53606e02b 100644 --- a/server/app/core/executerManager.js +++ b/server/app/core/executerManager.js @@ -52,10 +52,10 @@ function isExceededLimit(JS, rt, outputText) { * @returns {string} - source command string */ function source(task) { - if (typeof task.soruceScript === "undefined" || task.soruceScript === null || task.soruceScript.length === 0) { + if (typeof task.sourceScript === "undefined" || task.sourceScript === null || task.sourceScript.length === 0) { return ""; } - return `&& source ${task.soruceScript}`; + return `&& source ${task.sourceScript}`; } /** From 8f461921e2d6c4829132c21bab74cf1209e8089f Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Fri, 13 Feb 2026 23:07:55 +0900 Subject: [PATCH 07/17] fix jsonSchema --- server/app/db/jsonSchemas.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/app/db/jsonSchemas.js b/server/app/db/jsonSchemas.js index ab9168058..aa6052ec1 100644 --- a/server/app/db/jsonSchemas.js +++ b/server/app/db/jsonSchemas.js @@ -260,6 +260,7 @@ class BulkjobTaskSchema extends TaskSchema { this.properties.endBulkNumber = { type: ["number", "null"], default: null }; this.properties.manualFinishCondition = { type: "boolean", default: false }; this.properties.condition = { type: ["string", "null"], default: null }; + this.properties.sourceScript = { type: ["string", "null"], default: null }; } } From ce94ea39f73ac461e324f6195eaa8aa420b640c7 Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Wed, 18 Feb 2026 18:54:59 +0900 Subject: [PATCH 08/17] implement checker script --- client/src/components/componentProperty.vue | 10 ++++ server/app/core/executerManager.js | 52 ++++++++++++++++- server/app/core/fileValidator.js | 27 +++++++++ server/app/core/taskValidator.js | 8 ++- server/app/db/jsonSchemas.js | 1 + server/app/db/version.json | 2 +- server/test/app/core/executerManager.js | 58 +++++++++++++++++++ server/test/app/core/taskValidator.js | 31 ++++++++++ test/cypress/e2e/components/bulkjobTask.cy.js | 13 +++++ test/cypress/e2e/components/stepjobTask.cy.js | 14 +++++ test/cypress/e2e/components/task.cy.js | 27 +++++++++ 11 files changed, 237 insertions(+), 6 deletions(-) diff --git a/client/src/components/componentProperty.vue b/client/src/components/componentProperty.vue index 9647bbb99..078f509db 100644 --- a/client/src/components/componentProperty.vue +++ b/client/src/components/componentProperty.vue @@ -105,6 +105,16 @@ variant="outlined" data-cy="component_property-script-autocomplete" /> + { + loggerWrapper.logSSHout(task.projectRootDir, task.workingDir, data); + }); + loggerWrapper.logDebug(task.projectRootDir, task.workingDir, "checker (remote) done. rt =", rt); + return rt; + } else { + const script = path.resolve(task.workingDir, task.checker); + await addX(script); + + const options = { + cwd: task.workingDir, + env: Object.assign({}, process.env, task.env), + shell: true + }; + + loggerWrapper.logDebug(task.projectRootDir, task.workingDir, "exec checker (local)", script); + const rt = await promisifiedSpawn(task, script, options); + loggerWrapper.logDebug(task.projectRootDir, task.workingDir, "checker (local) done. rt =", rt); + return rt; + } +} async function needsRetry(task) { if ((typeof task.retry === "undefined" || task.retryCondition === null) && (typeof task.retryCondition === "undefined" || task.retryCondition === null)) { @@ -202,9 +235,21 @@ class Executer { //record job finished time task.endTime = getDateString(true, true); + //run checker script if specified + let checkerRt = null; + if (task.checker) { + try { + checkerRt = await runChecker(task); + } catch (e) { + loggerWrapper.logWarn(task.projectRootDir, task.workingDir, "checker script execution failed", e); + } + } + //update task status let state; - if (task.manualFinishCondition) { + if (task.checker && checkerRt !== null) { + state = checkerRt === 0 ? "finished" : "failed"; + } else if (task.manualFinishCondition) { state = await decideFinishState(task) ? "finished" : "failed"; } else { state = task.rt === 0 ? "finished" : "failed"; @@ -622,6 +667,7 @@ export { makeBulkOpt, decideFinishState, needsRetry, + runChecker, promisifiedSpawn, getExecutersKey, getMaxNumJob, diff --git a/server/app/core/fileValidator.js b/server/app/core/fileValidator.js index a6d5d8a18..e18551a75 100644 --- a/server/app/core/fileValidator.js +++ b/server/app/core/fileValidator.js @@ -59,6 +59,33 @@ export async function checkScript(projectRootDir, component) { return true; } +/** + * check if checker property has valid value + * @param {string} projectRootDir - project's root path + * @param {object} component - component which will be tested + */ +export async function checkChecker(projectRootDir, component) { + if (typeof component.checker !== "string") { + return Promise.reject(new Error("checker is not specified")); + } + const componentDir = await _internal.getComponentDir(projectRootDir, component.ID, true); + const filename = path.resolve(componentDir, component.checker); + + let stat; + try { + stat = await fs.stat(filename); + } catch (e) { + if (e.code !== "ENOENT") { + throw e; + } + return Promise.reject(new Error(`checker is not existing file ${filename}`)); + } + if (!stat.isFile()) { + return Promise.reject(new Error(`checker is not file ${filename}`)); + } + return true; +} + /** * check if parameterFile property has valid value * @param {string} projectRootDir - project's root path diff --git a/server/app/core/taskValidator.js b/server/app/core/taskValidator.js index d270c19d8..01be4b0fe 100644 --- a/server/app/core/taskValidator.js +++ b/server/app/core/taskValidator.js @@ -7,7 +7,7 @@ import { isInitialComponent, isLocal } from "./workflowComponent.js"; import { remoteHost } from "../db/db.js"; import { jobScheduler } from "../db/db.js"; -import { checkScript } from "./fileValidator.js"; +import { checkScript, checkChecker } from "./fileValidator.js"; import { validateConditionalCheck } from "./componentResourceValidator.js"; const _internal = { @@ -48,7 +48,11 @@ export async function validateTask(projectRootDir, component) { } } } - return checkScript(projectRootDir, component); + await checkScript(projectRootDir, component); + if (component.checker) { + await checkChecker(projectRootDir, component); + } + return true; } /** diff --git a/server/app/db/jsonSchemas.js b/server/app/db/jsonSchemas.js index aa6052ec1..0af15be9f 100644 --- a/server/app/db/jsonSchemas.js +++ b/server/app/db/jsonSchemas.js @@ -113,6 +113,7 @@ class TaskSchema extends GeneralWorkflowComponentSchema { this.properties.retryCondition = { type: ["string", "null"], default: null }; this.properties.retry = { type: ["number", "null"], default: null }; this.properties.ignoreFailure = { type: "boolean", default: false }; + this.properties.checker = { type: ["string", "null"], default: null }; } } diff --git a/server/app/db/version.json b/server/app/db/version.json index 15a689e4d..41abef3eb 100644 --- a/server/app/db/version.json +++ b/server/app/db/version.json @@ -1 +1 @@ -{"version": "2026-0213-173105" } \ No newline at end of file +{"version": "2026-0213-173105" } diff --git a/server/test/app/core/executerManager.js b/server/test/app/core/executerManager.js index dc2ad91bd..bf99aff02 100644 --- a/server/test/app/core/executerManager.js +++ b/server/test/app/core/executerManager.js @@ -625,4 +625,62 @@ describe("UT for executerManager class", function () { ); }); }); + + describe("runChecker", ()=>{ + it("should return null if checker is not specified", async function () { + const { runChecker } = await import("../../../app/core/executerManager.js"); + const task = { + checker: null, + remotehostID: "localhost", + projectRootDir: "/tmp/test", + workingDir: "/tmp/test/task" + }; + const result = await runChecker(task); + expect(result).to.be.null; + }); + + it("should execute local checker and return exit code", async function () { + const { runChecker } = await import("../../../app/core/executerManager.js"); + const testDir = path.resolve(testDirRoot, "checkerTest"); + await fs.ensureDir(testDir); + const checkerScript = path.resolve(testDir, "checker.sh"); + await fs.writeFile(checkerScript, "#!/bin/bash\nexit 0"); + await fs.chmod(checkerScript, 0o755); + + const task = { + checker: "checker.sh", + remotehostID: "localhost", + projectRootDir: testDir, + workingDir: testDir, + env: {} + }; + + const result = await runChecker(task); + expect(result).to.equal(0); + + await fs.remove(testDir); + }); + + it("should execute local checker and return non-zero exit code on failure", async function () { + const { runChecker } = await import("../../../app/core/executerManager.js"); + const testDir = path.resolve(testDirRoot, "checkerTestFail"); + await fs.ensureDir(testDir); + const checkerScript = path.resolve(testDir, "checker.sh"); + await fs.writeFile(checkerScript, "#!/bin/bash\nexit 1"); + await fs.chmod(checkerScript, 0o755); + + const task = { + checker: "checker.sh", + remotehostID: "localhost", + projectRootDir: testDir, + workingDir: testDir, + env: {} + }; + + const result = await runChecker(task); + expect(result).to.equal(1); + + await fs.remove(testDir); + }); + }); }); diff --git a/server/test/app/core/taskValidator.js b/server/test/app/core/taskValidator.js index 2cfd0a88e..29db8e119 100644 --- a/server/test/app/core/taskValidator.js +++ b/server/test/app/core/taskValidator.js @@ -140,6 +140,37 @@ describe("taskValidator UT", function () { testTask.submitOption = "-p high -t 10:00"; expect(await validateTask(projectRootDir, testTask)).to.be.true; }); + + it("should be rejected if checker is specified but does not exist", async function () { + const testTask = await createNewComponent(projectRootDir, projectRootDir, "task", { x: 0, y: 0 }); + testTask.script = "script.sh"; + const scriptPath = path.resolve(projectRootDir, testTask.name, "script.sh"); + await fs.writeFile(scriptPath, "#!/bin/bash\necho 'Hello'"); + testTask.checker = "nonexistent_checker.sh"; + await expect(validateTask(projectRootDir, testTask)).to.be.rejectedWith(/checker is not existing file/); + }); + + it("should be rejected if checker is not a file", async function () { + const testTask = await createNewComponent(projectRootDir, projectRootDir, "task", { x: 0, y: 0 }); + testTask.script = "script.sh"; + const scriptPath = path.resolve(projectRootDir, testTask.name, "script.sh"); + await fs.writeFile(scriptPath, "#!/bin/bash\necho 'Hello'"); + testTask.checker = "checker_dir"; + const checkerDirPath = path.resolve(projectRootDir, testTask.name, "checker_dir"); + await fs.mkdir(checkerDirPath); + await expect(validateTask(projectRootDir, testTask)).to.be.rejectedWith(/checker is not file/); + }); + + it("should be resolved with true if checker is specified and exists", async function () { + const testTask = await createNewComponent(projectRootDir, projectRootDir, "task", { x: 0, y: 0 }); + testTask.script = "script.sh"; + const scriptPath = path.resolve(projectRootDir, testTask.name, "script.sh"); + await fs.writeFile(scriptPath, "#!/bin/bash\necho 'Hello'"); + testTask.checker = "checker.sh"; + const checkerPath = path.resolve(projectRootDir, testTask.name, "checker.sh"); + await fs.writeFile(checkerPath, "#!/bin/bash\nexit 0"); + expect(await validateTask(projectRootDir, testTask)).to.be.true; + }); }); describe("validateStepjobTask", ()=>{ diff --git a/test/cypress/e2e/components/bulkjobTask.cy.js b/test/cypress/e2e/components/bulkjobTask.cy.js index 1246a9181..e8b4bef48 100644 --- a/test/cypress/e2e/components/bulkjobTask.cy.js +++ b/test/cypress/e2e/components/bulkjobTask.cy.js @@ -617,6 +617,19 @@ describe("components", ()=>{ cy.confirmDisplayInProperty(DATA_CY_STR, true); }); + /** + BulkjobTask コンポーネントの基本機能動作確認 + BulkjobTaskコンポーネント機能確認 + プロパティ設定確認 + checker script非表示確認 + 試験確認内容:checker scriptセレクトボックスが表示されていないことを確認 + */ + it("プロパティ設定確認-checker script非表示確認-checker scriptセレクトボックスが表示されていないことを確認", ()=>{ + cy.createComponent(DEF_COMPONENT_BJ_TASK, BJ_TASK_NAME_0, 501, 500); + const DATA_CY_STR = "[data-cy=\"component_property-checker-autocomplete\"]"; + cy.get(DATA_CY_STR).should("not.exist"); + }); + /** コンポーネントの基本機能動作確認 BulkjobTaskコンポーネント共通機能確認 diff --git a/test/cypress/e2e/components/stepjobTask.cy.js b/test/cypress/e2e/components/stepjobTask.cy.js index e55602f10..854047ca8 100644 --- a/test/cypress/e2e/components/stepjobTask.cy.js +++ b/test/cypress/e2e/components/stepjobTask.cy.js @@ -468,6 +468,20 @@ describe("components", ()=>{ cy.confirmDisplayInProperty(DATA_CY_STR, true); }); + /** + StepjobTask コンポーネントの基本機能動作確認 + StepjobTaskコンポーネント機能確認 + プロパティ設定確認 + checker script非表示確認 + 試験確認内容:checker scriptセレクトボックスが表示されていないことを確認 + */ + it("プロパティ設定確認-checker script非表示確認-checker scriptセレクトボックスが表示されていないことを確認", ()=>{ + cy.createStepjobComponentAndDoubleClick(DEF_COMPONENT_STEPJOB, STEPJOB_NAME_0, 501, 500); + cy.createComponent(DEF_COMPONENT_STEPJOB_TASK, STEPJOB_TASK_NAME_0, 501, 500); + const DATA_CY_STR = "[data-cy=\"component_property-checker-autocomplete\"]"; + cy.get(DATA_CY_STR).should("not.exist"); + }); + /** コンポーネントの基本機能動作確認 StepjobTaskコンポーネント共通機能確認 diff --git a/test/cypress/e2e/components/task.cy.js b/test/cypress/e2e/components/task.cy.js index e684313cd..4b80d5072 100644 --- a/test/cypress/e2e/components/task.cy.js +++ b/test/cypress/e2e/components/task.cy.js @@ -621,6 +621,33 @@ describe("components", ()=>{ Task コンポーネントの基本機能動作確認 Taskコンポーネント機能確認 プロパティ設定確認 + checker script表示確認 + 試験確認内容:checker scriptセレクトボックスが表示されていることを確認 + */ + it("プロパティ設定確認-checker script表示確認-checker scriptセレクトボックスが表示されていることを確認", ()=>{ + const DATA_CY_STR = "[data-cy=\"component_property-checker-autocomplete\"]"; + cy.confirmDisplayInProperty(DATA_CY_STR, true); + }); + + /** + Task コンポーネントの基本機能動作確認 + Taskコンポーネント機能確認 + プロパティ設定確認 + checker scriptファイル選択表示確認 + 試験確認内容:checker scriptセレクトボックスで選択したファイルが表示されていることを確認 + */ + it("プロパティ設定確認-checker scriptファイル選択表示確認-checker scriptセレクトボックスで選択したファイルが表示されていることを確認", ()=>{ + cy.createDirOrFile(TYPE_FILE, "test-checker", true); + let targetDropBoxCy = "[data-cy=\"component_property-checker-autocomplete\"]"; + cy.selectValueFromDropdownList(targetDropBoxCy, 3, "test-checker"); + cy.get("[data-cy=\"component_property-checker-autocomplete\"]").find("input") + .should("have.value", "test-checker"); + }); + + /** + Task コンポーネントの基本機能動作確認 + Taskコンポーネント機能確認 + プロパティ設定確認 source script表示確認 試験確認内容:source scriptセレクトボックスが表示されていることを確認 */ From c65f64dfa4a4c50dff9e96725224338780c1022d Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Wed, 18 Feb 2026 21:36:39 +0900 Subject: [PATCH 09/17] Fix checker and source behavior --- client/src/components/componentProperty.vue | 5 +-- server/app/core/dispatcher.js | 9 +++++ server/app/core/executerManager.js | 1 - server/app/core/fileValidator.js | 7 ++++ server/app/db/jsonSchemas.js | 2 +- server/test/app/core/taskValidator.js | 18 +++++++++ test/wheel_config/remotehost.json | 41 +-------------------- 7 files changed, 38 insertions(+), 45 deletions(-) diff --git a/client/src/components/componentProperty.vue b/client/src/components/componentProperty.vue index 078f509db..ce0ce6e57 100644 --- a/client/src/components/componentProperty.vue +++ b/client/src/components/componentProperty.vue @@ -161,16 +161,15 @@ variant="outlined" data-cy="component_property-submit_option-text_field" /> - { logWarn(this.projectRootDir, `${this.cwfDir}/${component.name}`, "failed. rt=", component.rt); logTrace(this.projectRootDir, component.workingDir, "failed due to", e); diff --git a/server/app/core/executerManager.js b/server/app/core/executerManager.js index 06e286cd8..1ed2a80f9 100644 --- a/server/app/core/executerManager.js +++ b/server/app/core/executerManager.js @@ -172,7 +172,6 @@ async function runChecker(task) { return rt; } else { const script = path.resolve(task.workingDir, task.checker); - await addX(script); const options = { cwd: task.workingDir, diff --git a/server/app/core/fileValidator.js b/server/app/core/fileValidator.js index e18551a75..50dcac00c 100644 --- a/server/app/core/fileValidator.js +++ b/server/app/core/fileValidator.js @@ -68,6 +68,13 @@ export async function checkChecker(projectRootDir, component) { if (typeof component.checker !== "string") { return Promise.reject(new Error("checker is not specified")); } + + //Checker must be a filename (not absolute path) under component directory + if (path.isAbsolute(component.checker)) { + return Promise.reject(new Error("checker must be a filename under component directory, not an absolute path")); + } + + //Check if file exists in component directory const componentDir = await _internal.getComponentDir(projectRootDir, component.ID, true); const filename = path.resolve(componentDir, component.checker); diff --git a/server/app/db/jsonSchemas.js b/server/app/db/jsonSchemas.js index 0af15be9f..655a9d638 100644 --- a/server/app/db/jsonSchemas.js +++ b/server/app/db/jsonSchemas.js @@ -107,6 +107,7 @@ class TaskSchema extends GeneralWorkflowComponentSchema { this.properties.useJobScheduler = { type: "boolean", default: false }; this.properties.queue = { type: ["string", "null"], default: null }; this.properties.submitOption = { type: ["string", "null"], default: null }; + this.properties.sourceScript = { type: ["string", "null"], default: null }; this.properties.include = stringArraySchema; this.properties.exclude = stringArraySchema; this.properties.state.enum.push(...["stage-in", "waiting", "queued", "stage-out"]); @@ -261,7 +262,6 @@ class BulkjobTaskSchema extends TaskSchema { this.properties.endBulkNumber = { type: ["number", "null"], default: null }; this.properties.manualFinishCondition = { type: "boolean", default: false }; this.properties.condition = { type: ["string", "null"], default: null }; - this.properties.sourceScript = { type: ["string", "null"], default: null }; } } diff --git a/server/test/app/core/taskValidator.js b/server/test/app/core/taskValidator.js index 29db8e119..9461752fa 100644 --- a/server/test/app/core/taskValidator.js +++ b/server/test/app/core/taskValidator.js @@ -171,6 +171,24 @@ describe("taskValidator UT", function () { await fs.writeFile(checkerPath, "#!/bin/bash\nexit 0"); expect(await validateTask(projectRootDir, testTask)).to.be.true; }); + + it("should be rejected if checker is an absolute path", async function () { + const testTask = await createNewComponent(projectRootDir, projectRootDir, "task", { x: 0, y: 0 }); + testTask.script = "script.sh"; + const scriptPath = path.resolve(projectRootDir, testTask.name, "script.sh"); + await fs.writeFile(scriptPath, "#!/bin/bash\necho 'Hello'"); + testTask.checker = "/usr/local/bin/checker.sh"; + await expect(validateTask(projectRootDir, testTask)).to.be.rejectedWith(/checker must be a filename under component directory/); + }); + + it("should be rejected if checker is an absolute path on remote host", async function () { + const testTask = await createNewComponent(projectRootDir, projectRootDir, "task", { x: 0, y: 0 }); + testTask.script = "script.sh"; + const scriptPath = path.resolve(projectRootDir, testTask.name, "script.sh"); + await fs.writeFile(scriptPath, "#!/bin/bash\necho 'Hello'"); + testTask.checker = "/home/user/checker.sh"; + await expect(validateTask(projectRootDir, testTask)).to.be.rejectedWith(/checker must be a filename under component directory/); + }); }); describe("validateStepjobTask", ()=>{ diff --git a/test/wheel_config/remotehost.json b/test/wheel_config/remotehost.json index 482f375c2..fe51488c7 100644 --- a/test/wheel_config/remotehost.json +++ b/test/wheel_config/remotehost.json @@ -1,40 +1 @@ -[ - { - name: "componentTestLabel", - host: "TestHostname", - port: 8000, - user: "testUser", - numJob: 5, - jobScheduler: "PBSPro", - useBulkjob: true, - useStepjob: true, - queue: "testQueues", - sharedHost: "", - sharedPath: "", - renewInterval: 0, - statusCheckInterval: 60, - maxStatusCheckError: 10, - rcfile: "/etc/profile", - useGfarm: true, - id: "545dd3e0-b26d-11f0-bbe8-272d67e9e112" - }, - { - name: "testServer", - host: "wheel_release_test_server", - port: 8000, - numJob: 5, - queue: "workq", - jobScheduler: "PBSPro", - useBulkjob: false, - useStepjob: false, - sharedHost: "", - sharedPath: "", - renewInterval: 0, - statusCheckInterval: 60, - maxStatusCheckError: 10, - id: "91041660-c8a4-11ee-8b76-87edc1fbf31a", - user: "testuser", - rcfile: "/etc/profile", - prependCmd: "" - } -]; +[] From 468131e3f26656c50218cc1fb78cb3c1cfa54b13 Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Fri, 20 Feb 2026 14:02:25 +0900 Subject: [PATCH 10/17] add componnent test information --- AGENTS.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index da840452a..3d9eec10c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,12 +2,13 @@ The directory structure of this project is organized as follows: ``` -client/ client js codes -common/ JS code shared between client and server -documentMD/ markdown documents -server/ server js codes -server/tetst/ server unit test -test/ end-to-end test with cypress +client/ client js codes +common/ JS code shared between client and server +documentMD/ markdown documents +server/ server js codes +server/tetst/ server unit test +test/cypress/e2e end-to-end test with cypress +test/cypress/component component test with cypress ``` ## npm scripts From b140bd7ea7e5fe9975ecb457d900e609ee2409c9 Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Tue, 24 Feb 2026 19:58:31 +0900 Subject: [PATCH 11/17] retry script can use task's rt value as environment variable --- .../_reference/4_component/01_Task.en.md | 24 ++++++++++ .../_reference/4_component/01_Task.md | 24 ++++++++++ server/app/core/executerManager.js | 23 ++++++++-- server/test/app/core/executerManager.js | 44 +++++++++++++++++++ server/test/app/core/projectController.js | 32 ++++++++++++++ 5 files changed, 143 insertions(+), 4 deletions(-) diff --git a/documentMD/user_guide/_reference/4_component/01_Task.en.md b/documentMD/user_guide/_reference/4_component/01_Task.en.md index 629b2ffc1..12cf6f7eb 100644 --- a/documentMD/user_guide/_reference/4_component/01_Task.en.md +++ b/documentMD/user_guide/_reference/4_component/01_Task.en.md @@ -79,6 +79,30 @@ The expression you enter here is evaluated after the Task component has finished If both the script name and the javascript expression are not set and you set only the number of retry values, repeat the retry until the script terminates normally or reaches the number of retry settings. +__Available Environment Variables__ +The following environment variables are available in the script or javascript expression used to determine whether to retry: +- For shell scripts: `WHEEL_TASK_RT` +- For javascript expressions: `wheelTaskRT` + +These variables contain the return value of the script specified in the script property. +However, if the checker property is set, the return value of the checker script takes precedence. + +Example: Retry only if the return value is 10 (shell script) +```bash +#!/bin/bash +if [ "$WHEEL_TASK_RT" = "10" ]; then + exit 0 +else + exit 1 +fi +``` + +Example: Retry only if the return value is less than 5 (javascript expression) +```javascript +wheelTaskRT < 5 +``` +{: .notice--info} + ### include, exclude ![img](./img/include_exclude.png "include, exclude") diff --git a/documentMD/user_guide/_reference/4_component/01_Task.md b/documentMD/user_guide/_reference/4_component/01_Task.md index 8946a1f69..c09f78b38 100644 --- a/documentMD/user_guide/_reference/4_component/01_Task.md +++ b/documentMD/user_guide/_reference/4_component/01_Task.md @@ -79,6 +79,30 @@ Taskコンポーネントの成功 / 失敗を判定するのにjavascript式を スクリプト名、javascript式ともに未設定で、number of retryの値のみを設定していた場合は、スクリプトが正常終了するか、retryに設定した回数に達するまで再実行を繰り返します。 +__利用可能な環境変数について__ +再実行の判定を行うスクリプトやjavascript式では、以下の環境変数を利用できます。 +- シェルスクリプトの場合: `WHEEL_TASK_RT` +- javascript式の場合: `wheelTaskRT` + +これらの変数には、scriptプロパティで指定されたスクリプトの戻り値が設定されます。 +ただし、checkerプロパティが設定されている場合は、checkerスクリプトの戻り値が優先して使用されます。 + +例: シェルスクリプトで戻り値が10の場合のみ再実行する +```bash +#!/bin/bash +if [ "$WHEEL_TASK_RT" = "10" ]; then + exit 0 +else + exit 1 +fi +``` + +例: javascript式で戻り値が5未満の場合のみ再実行する +```javascript +wheelTaskRT < 5 +``` +{: .notice--info} + ### include, exclude ![img](./img/include_exclude.png "include, exclude") diff --git a/server/app/core/executerManager.js b/server/app/core/executerManager.js index 1ed2a80f9..bea616e70 100644 --- a/server/app/core/executerManager.js +++ b/server/app/core/executerManager.js @@ -140,7 +140,12 @@ function makeBulkOpt(task) { async function decideFinishState(task) { let rt = false; try { - rt = await _internal.evalCondition(task.projectRootDir, task.condition, task.workingDir, task.currentIndex); + const env = Object.assign({}, task.env || {}); + // Use checkerRt if available, otherwise use task.rt + const effectiveRt = (task.checkerRt !== null && typeof task.checkerRt !== "undefined") ? task.checkerRt : task.rt; + env.WHEEL_TASK_RT = effectiveRt; + env.wheelTaskRT = effectiveRt; + rt = await _internal.evalCondition(task.projectRootDir, task.condition, task.workingDir, env); } catch { loggerWrapper.logInfo(task.projectRootDir, task.workingDir, "manualFinishCondition is set but exception occurred while evaluting it."); return false; @@ -185,7 +190,7 @@ async function runChecker(task) { return rt; } } -async function needsRetry(task) { +async function needsRetry(task, checkerRt) { if ((typeof task.retry === "undefined" || task.retryCondition === null) && (typeof task.retryCondition === "undefined" || task.retryCondition === null)) { return false; @@ -195,7 +200,16 @@ async function needsRetry(task) { return Number.isInteger(task.retry) && task.retry > 0; } try { - rt = await _internal.evalCondition(task.projectRootDir, task.retryCondition, task.workingDir, task.currentIndex); + const env = Object.assign({}, task.env || {}); + + // Use checkerRt if available, otherwise use task.rt + const actualCheckerRt = checkerRt !== undefined ? checkerRt : task.checkerRt; + const effectiveRt = (actualCheckerRt !== null && typeof actualCheckerRt !== "undefined") ? actualCheckerRt : task.rt; + + env.WHEEL_TASK_RT = effectiveRt; + env.wheelTaskRT = effectiveRt; + + rt = await _internal.evalCondition(task.projectRootDir, task.retryCondition, task.workingDir, env); } catch { loggerWrapper.logInfo(task.projectRootDir, task.workingDir, "retryCondition is set but exception occurred while evaluting it. so give up retring"); return false; @@ -239,6 +253,7 @@ class Executer { if (task.checker) { try { checkerRt = await runChecker(task); + task.checkerRt = checkerRt; } catch (e) { loggerWrapper.logWarn(task.projectRootDir, task.workingDir, "checker script execution failed", e); } @@ -256,7 +271,7 @@ class Executer { await setTaskState(task, state); //exec useualy returns task.state but to use it in retry function //to use task in retry function, exec() will be rejected with task object if failed - if (state === "failed" && await needsRetry(task)) { + if (state === "failed" && await needsRetry(task, checkerRt)) { return Promise.reject(task); } return state; diff --git a/server/test/app/core/executerManager.js b/server/test/app/core/executerManager.js index bf99aff02..aaf77117c 100644 --- a/server/test/app/core/executerManager.js +++ b/server/test/app/core/executerManager.js @@ -292,6 +292,17 @@ describe("UT for executerManager class", function () { expect(result).to.be.false; expect(loggerInfoStub).to.have.been.called; }); + it("should pass WHEEL_TASK_RT and wheelTaskRT in env to evalCondition", async function () { + evalConditionStub.resolves(true); + const taskWithRt = { ...mockTask, rt: 42, env: { EXISTING: "value" } }; + await decideFinishState(taskWithRt); + expect(evalConditionStub).to.have.been.calledWith( + mockTask.projectRootDir, + "mock condition", + mockTask.workingDir, + sinon.match({ WHEEL_TASK_RT: 42, wheelTaskRT: 42, EXISTING: "value" }) + ); + }); }); describe("needsRetry", function () { const mockTask = { @@ -354,6 +365,39 @@ describe("UT for executerManager class", function () { expect(result).to.be.true; expect(loggerInfoStub).to.have.been.calledWith(mockTask.projectRootDir, mockTask.workingDir, "failed but retring"); }); + it("should pass WHEEL_TASK_RT and wheelTaskRT in env to evalCondition", async function () { + evalConditionStub.resolves(true); + const taskWithCondition = { ...mockTask, retryCondition: "mock condition", rt: 42, env: { EXISTING: "value" } }; + await needsRetry(taskWithCondition); + expect(evalConditionStub).to.have.been.calledWith( + mockTask.projectRootDir, + "mock condition", + mockTask.workingDir, + sinon.match({ WHEEL_TASK_RT: 42, wheelTaskRT: 42, EXISTING: "value" }) + ); + }); + it("should use checker return value as WHEEL_TASK_RT when checkerRt is provided", async function () { + evalConditionStub.resolves(true); + const taskWithCondition = { ...mockTask, retryCondition: "mock condition", rt: 42 }; + await needsRetry(taskWithCondition, 10); + expect(evalConditionStub).to.have.been.calledWith( + mockTask.projectRootDir, + "mock condition", + mockTask.workingDir, + sinon.match({ WHEEL_TASK_RT: 10, wheelTaskRT: 10 }) + ); + }); + it("should use task.checkerRt as WHEEL_TASK_RT if checkerRt parameter is undefined", async function () { + evalConditionStub.resolves(true); + const taskWithCondition = { ...mockTask, retryCondition: "mock condition", rt: 42, checkerRt: 20 }; + await needsRetry(taskWithCondition); + expect(evalConditionStub).to.have.been.calledWith( + mockTask.projectRootDir, + "mock condition", + mockTask.workingDir, + sinon.match({ WHEEL_TASK_RT: 20, wheelTaskRT: 20 }) + ); + }); }); describe("promisifiedSpawn", function () { diff --git a/server/test/app/core/projectController.js b/server/test/app/core/projectController.js index 0e38bdefe..2bc1087ef 100644 --- a/server/test/app/core/projectController.js +++ b/server/test/app/core/projectController.js @@ -91,6 +91,38 @@ describe("project Controller UT", function () { expect(ajv.validate(task0JsonSchema, task0Json)).to.be.true; expect(fs.readFileSync(path.resolve(projectRootDir, "task0", statusFilename), "utf-8")).to.equal("failed\n10\nundefined"); }); + it("should use WHEEL_TASK_RT in retryCondition shell script", async ()=>{ + await updateComponentProperty(projectRootDir, task0.ID, "retry", 2); + // Create a retry condition script that checks if WHEEL_TASK_RT equals 10 + const retryScript = "retry.sh"; + const retryConditionContent = `${scriptHeader}\nif [ "${referenceEnv("WHEEL_TASK_RT")}" = "10" ]; then\n ${exit(0)}\nelse\n ${exit(1)}\nfi`; + await fs.outputFile(path.join(projectRootDir, "task0", retryScript), retryConditionContent); + await updateComponentProperty(projectRootDir, task0.ID, "retryCondition", retryScript); + await fs.outputFile(path.join(projectRootDir, "task0", scriptName), `${scriptPwd}\n${exit(10)}`); + await runProject(projectRootDir); + + const projectJson = await fs.readJson(path.resolve(projectRootDir, projectJsonFilename)); + const projectJsonSchema = { required: ["state"], properties: { state: { enum: ["failed"] } } }; + expect(ajv.validate(projectJsonSchema, projectJson)).to.be.true; + const task0Json = await fs.readJson(path.resolve(projectRootDir, "task0", componentJsonFilename)); + expect(task0Json.state).to.equal("failed"); + // Verify retry was attempted (statusFile should exist with rt=10) + expect(fs.readFileSync(path.resolve(projectRootDir, "task0", statusFilename), "utf-8")).to.equal("failed\n10\nundefined"); + }); + it("should use wheelTaskRT in retryCondition javascript expression", async ()=>{ + await updateComponentProperty(projectRootDir, task0.ID, "retry", 2); + // Create a retry condition that checks if wheelTaskRT equals 5 + await updateComponentProperty(projectRootDir, task0.ID, "retryCondition", "wheelTaskRT === 5"); + await fs.outputFile(path.join(projectRootDir, "task0", scriptName), `${scriptPwd}\n${exit(5)}`); + await runProject(projectRootDir); + + const projectJson = await fs.readJson(path.resolve(projectRootDir, projectJsonFilename)); + const projectJsonSchema = { required: ["state"], properties: { state: { enum: ["failed"] } } }; + expect(ajv.validate(projectJsonSchema, projectJson)).to.be.true; + const task0Json = await fs.readJson(path.resolve(projectRootDir, "task0", componentJsonFilename)); + expect(task0Json.state).to.equal("failed"); + expect(fs.readFileSync(path.resolve(projectRootDir, "task0", statusFilename), "utf-8")).to.equal("failed\n5\nundefined"); + }); it("should run project and fail", async ()=>{ await fs.outputFile(path.join(projectRootDir, "task0", scriptName), `${scriptPwd}\n${exit(10)}`); await runProject(projectRootDir); From 6f792aeaee3768e2e4809fea852ac5010d46b231 Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Tue, 24 Feb 2026 20:18:28 +0900 Subject: [PATCH 12/17] add checker and source documentation --- .../_reference/4_component/01_Task.en.md | 30 +++++++++++++++++++ .../_reference/4_component/01_Task.md | 30 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/documentMD/user_guide/_reference/4_component/01_Task.en.md b/documentMD/user_guide/_reference/4_component/01_Task.en.md index 12cf6f7eb..3332ca0c0 100644 --- a/documentMD/user_guide/_reference/4_component/01_Task.en.md +++ b/documentMD/user_guide/_reference/4_component/01_Task.en.md @@ -57,6 +57,36 @@ Therefore, it cannot be changed here. ### submit option Sets additional options to be specified when the job is submitted. +### source script +Specifies an environment configuration file to be loaded before executing the script on the remote host. +The specified script is loaded using the `source` command. + +This is useful when you need to set specific environment variables or use module systems (such as Environment Modules). + +Example: +```bash +# Contents of setup.sh +module load intel/2021 +export MY_VAR=value +``` + +### checker script +Specifies a dedicated script to determine the success/failure of the Task component. +The checker script is executed after the Task component finishes, and its return value (0: success, non-zero: failure) determines the final status of the Task. + +When a checker script is set, the return value of the script specified in the script property is ignored, and the checker script's return value is used instead. + +Example: +```bash +#!/bin/bash +# Check if output file exists +if [ -f output.dat ]; then + exit 0 +else + exit 1 +fi +``` + ### number of retry Specifies the number of times the Task component automatically reruns if it fails to run. If none is specified, the command is not re-executed. diff --git a/documentMD/user_guide/_reference/4_component/01_Task.md b/documentMD/user_guide/_reference/4_component/01_Task.md index c09f78b38..e441fdd8a 100644 --- a/documentMD/user_guide/_reference/4_component/01_Task.md +++ b/documentMD/user_guide/_reference/4_component/01_Task.md @@ -57,6 +57,36 @@ use job schedulerを有効にしたときのみ、次のqueue, submit optionプ ### submit option ジョブ投入時に追加で指定するオプションを設定します。 +### source script +リモートホストでスクリプトを実行する前に読み込む環境設定ファイルを指定します。 +ここで指定したスクリプトは`source`コマンドで読み込まれます。 + +例えば、特定の環境変数を設定したり、モジュールシステム(Environment Modules等)を使用する場合に利用します。 + +例: +```bash +# setup.sh の内容 +module load intel/2021 +export MY_VAR=value +``` + +### checker script +Taskコンポーネントの成功/失敗を判定するための専用スクリプトを指定します。 +checkerスクリプトはTaskコンポーネント実行終了後に実行され、その戻り値(0:成功、0以外:失敗)でTaskの最終的な成否が決定されます。 + +checkerスクリプトが設定されている場合、scriptプロパティで指定したスクリプトの戻り値は無視され、checkerスクリプトの戻り値が使用されます。 + +例: +```bash +#!/bin/bash +# 出力ファイルの存在確認 +if [ -f output.dat ]; then + exit 0 +else + exit 1 +fi +``` + ### number of retry Taskコンポーネントの実行に失敗したときに、自動的に再実行する回数を指定します。 無指定時は再実行しません。 From 9632af0e1bbf5096cbf78e05fe1aea36c86e9a9b Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Tue, 24 Feb 2026 21:42:02 +0900 Subject: [PATCH 13/17] close remote file setting panel when host is set to localhost --- client/src/components/componentProperty.vue | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/client/src/components/componentProperty.vue b/client/src/components/componentProperty.vue index ce0ce6e57..f4d24fdcb 100644 --- a/client/src/components/componentProperty.vue +++ b/client/src/components/componentProperty.vue @@ -830,16 +830,31 @@ export default { } const JS = currentHostSetting.jobScheduler; return JS ? this.jobScheduler[JS].submit : null; + }, + remoteFileSettingPanelIndex() { + //Remote file setting panel only appears for task, bulkjobTask, and stepjobTask + //For these component types, it is always at index 3 + if (this.isTask || this.isBulkjobTask || this.isStepjobTask) { + return 3; + } + return null; } }, watch: { retryByJS() { this.copySelectedComponent.retryCondition = null; }, - "copySelectedComponent.host"(newValue) { + "copySelectedComponent.host"(newValue, oldValue) { if (newValue === "localhost" && !this.isBulkjobTask && !this.isStepjobTask && this.isStepjob) { this.copySelectedComponent.useJobScheduler = false; } + //Close remote file setting panel if changing to localhost + if (newValue === "localhost") { + //Remove remote file setting panel from openPanels + this.openPanels = this.openPanels.filter((idx)=>{ + return idx !== this.remoteFileSettingPanelIndex; + }); + } }, open(newValue) { //another component is selected while componentProperty is open From ab0249606fd1ab1bb7e53d6699d3155cd4d1fd17 Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Tue, 24 Feb 2026 21:42:15 +0900 Subject: [PATCH 14/17] cosmetic change --- server/app/core/executerManager.js | 10 +++++----- server/test/app/core/projectController.js | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/app/core/executerManager.js b/server/app/core/executerManager.js index bea616e70..4dc78ea48 100644 --- a/server/app/core/executerManager.js +++ b/server/app/core/executerManager.js @@ -141,7 +141,7 @@ async function decideFinishState(task) { let rt = false; try { const env = Object.assign({}, task.env || {}); - // Use checkerRt if available, otherwise use task.rt + //Use checkerRt if available, otherwise use task.rt const effectiveRt = (task.checkerRt !== null && typeof task.checkerRt !== "undefined") ? task.checkerRt : task.rt; env.WHEEL_TASK_RT = effectiveRt; env.wheelTaskRT = effectiveRt; @@ -201,14 +201,14 @@ async function needsRetry(task, checkerRt) { } try { const env = Object.assign({}, task.env || {}); - - // Use checkerRt if available, otherwise use task.rt + + //Use checkerRt if available, otherwise use task.rt const actualCheckerRt = checkerRt !== undefined ? checkerRt : task.checkerRt; const effectiveRt = (actualCheckerRt !== null && typeof actualCheckerRt !== "undefined") ? actualCheckerRt : task.rt; - + env.WHEEL_TASK_RT = effectiveRt; env.wheelTaskRT = effectiveRt; - + rt = await _internal.evalCondition(task.projectRootDir, task.retryCondition, task.workingDir, env); } catch { loggerWrapper.logInfo(task.projectRootDir, task.workingDir, "retryCondition is set but exception occurred while evaluting it. so give up retring"); diff --git a/server/test/app/core/projectController.js b/server/test/app/core/projectController.js index 2bc1087ef..497219780 100644 --- a/server/test/app/core/projectController.js +++ b/server/test/app/core/projectController.js @@ -93,7 +93,7 @@ describe("project Controller UT", function () { }); it("should use WHEEL_TASK_RT in retryCondition shell script", async ()=>{ await updateComponentProperty(projectRootDir, task0.ID, "retry", 2); - // Create a retry condition script that checks if WHEEL_TASK_RT equals 10 + //Create a retry condition script that checks if WHEEL_TASK_RT equals 10 const retryScript = "retry.sh"; const retryConditionContent = `${scriptHeader}\nif [ "${referenceEnv("WHEEL_TASK_RT")}" = "10" ]; then\n ${exit(0)}\nelse\n ${exit(1)}\nfi`; await fs.outputFile(path.join(projectRootDir, "task0", retryScript), retryConditionContent); @@ -106,12 +106,12 @@ describe("project Controller UT", function () { expect(ajv.validate(projectJsonSchema, projectJson)).to.be.true; const task0Json = await fs.readJson(path.resolve(projectRootDir, "task0", componentJsonFilename)); expect(task0Json.state).to.equal("failed"); - // Verify retry was attempted (statusFile should exist with rt=10) + //Verify retry was attempted (statusFile should exist with rt=10) expect(fs.readFileSync(path.resolve(projectRootDir, "task0", statusFilename), "utf-8")).to.equal("failed\n10\nundefined"); }); it("should use wheelTaskRT in retryCondition javascript expression", async ()=>{ await updateComponentProperty(projectRootDir, task0.ID, "retry", 2); - // Create a retry condition that checks if wheelTaskRT equals 5 + //Create a retry condition that checks if wheelTaskRT equals 5 await updateComponentProperty(projectRootDir, task0.ID, "retryCondition", "wheelTaskRT === 5"); await fs.outputFile(path.join(projectRootDir, "task0", scriptName), `${scriptPwd}\n${exit(5)}`); await runProject(projectRootDir); From c0b9d7720f6711179c83ed347af7744d50a016f9 Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Tue, 24 Feb 2026 21:46:01 +0900 Subject: [PATCH 15/17] remove job specific field when use job scheduler is false --- client/src/components/componentProperty.vue | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/client/src/components/componentProperty.vue b/client/src/components/componentProperty.vue index f4d24fdcb..acc84a68c 100644 --- a/client/src/components/componentProperty.vue +++ b/client/src/components/componentProperty.vue @@ -134,39 +134,35 @@ data-cy="component_property-job_scheduler-switch" /> Date: Tue, 24 Feb 2026 21:57:38 +0900 Subject: [PATCH 16/17] add default value of checker and sourceScript --- server/app/core/workflowComponent.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/app/core/workflowComponent.js b/server/app/core/workflowComponent.js index 4ce5e8281..321b5399f 100644 --- a/server/app/core/workflowComponent.js +++ b/server/app/core/workflowComponent.js @@ -179,6 +179,12 @@ class Task extends GeneralComponent { //if true, project will continue after failing this task. this.ignoreFailure = false; + + //checker script to determine task success/failure + this.checker = null; + + //source script to be loaded before executing on remote host + this.sourceScript = null; } } From 0b2e67fcd1ba0c2c732945ac316691eea68808b6 Mon Sep 17 00:00:00 2001 From: "version-number-updater[bot]" Date: Tue, 24 Feb 2026 22:03:08 +0900 Subject: [PATCH 17/17] [skip ci] update version number --- server/app/db/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/db/version.json b/server/app/db/version.json index 41abef3eb..76f6687ba 100644 --- a/server/app/db/version.json +++ b/server/app/db/version.json @@ -1 +1 @@ -{"version": "2026-0213-173105" } +{"version": "2026-0224-220307" } \ No newline at end of file