From 58519119f99ff2ba1991a87c2708f1be08f2a7a8 Mon Sep 17 00:00:00 2001 From: Leon Grave Date: Tue, 25 Aug 2026 12:20:22 +0200 Subject: [PATCH] feat(JFrogCliV2): add option to register JFrog CLI on agent PATH Adds an opt-in 'registerInPath' input. When enabled, the JFrog CLI executable's directory is prepended to the agent's PATH, so later steps in the same pipeline job can call 'jf' directly without a separate install/config step. Split out of #637 per reviewer feedback to keep each change focused and independently reviewable. Co-Authored-By: Claude Sonnet 5 --- tasks/JFrogCliV2/jfrogCliRun.js | 5 ++++ tasks/JFrogCliV2/task.json | 8 ++++++ .../registerInPath.js | 9 +++++++ tests/tests.ts | 27 +++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 tests/resources/jfrogCliV2RegisterInPath/registerInPath.js diff --git a/tasks/JFrogCliV2/jfrogCliRun.js b/tasks/JFrogCliV2/jfrogCliRun.js index f092b222..d3cf5132 100644 --- a/tasks/JFrogCliV2/jfrogCliRun.js +++ b/tasks/JFrogCliV2/jfrogCliRun.js @@ -1,6 +1,7 @@ const tl = require('azure-pipelines-task-lib/task'); const utils = require('@jfrog/tasks-utils/utils.js'); const fs = require('fs'); +const path = require('path'); let serverId; RunJfrogCliCommand(RunTaskCbk); @@ -49,6 +50,10 @@ async function RunTaskCbk(cliPath) { serverId = utils.assembleUniqueServerId('jfrog_cli_cmd'); await utils.configureDefaultJfrogServer(serverId, cliPath, requiredWorkDir); + if (tl.getBoolInput('registerInPath')) { + tl.prependPath(path.dirname(cliPath)); + } + let cliCommandsList = tl.getInput('command', true).split('\n'); try { for (let cliCommand of cliCommandsList) { diff --git a/tasks/JFrogCliV2/task.json b/tasks/JFrogCliV2/task.json index 8fe4675c..3b5ccd4d 100644 --- a/tasks/JFrogCliV2/task.json +++ b/tasks/JFrogCliV2/task.json @@ -63,6 +63,14 @@ "defaultValue": "", "required": false, "helpMarkDown": "The working directory where the command will run. When empty, the value of '$(System.DefaultWorkingDirectory)' is used." + }, + { + "name": "registerInPath", + "type": "boolean", + "label": "Add JFrog CLI to PATH", + "defaultValue": "false", + "required": false, + "helpMarkDown": "Add the JFrog CLI executable's directory to the agent's PATH, so subsequent steps in the pipeline job can call 'jf' directly." } ], "execution": { diff --git a/tests/resources/jfrogCliV2RegisterInPath/registerInPath.js b/tests/resources/jfrogCliV2RegisterInPath/registerInPath.js new file mode 100644 index 00000000..e03e5af9 --- /dev/null +++ b/tests/resources/jfrogCliV2RegisterInPath/registerInPath.js @@ -0,0 +1,9 @@ +const testUtils = require('../../testUtils'); + +let inputs = { + jfrogPlatformConnection: 'mock-service', + registerInPath: true, + command: 'jf rt ping', +}; + +testUtils.runPlatformTask(testUtils.genericCli, {}, inputs); diff --git a/tests/tests.ts b/tests/tests.ts index 79d2c963..71bc40b1 100644 --- a/tests/tests.ts +++ b/tests/tests.ts @@ -589,6 +589,20 @@ describe('JFrog Artifactory Extension Tests', (): void => { ); }); + describe('JFrog CLI V2 Tests', (): void => { + runSyncTest( + 'Adds the JFrog CLI directory to PATH when registerInPath is enabled', + async (): Promise => { + const runner: adoMockTest.MockTestRunner = await mockTaskCapture('jfrogCliV2RegisterInPath', 'registerInPath'); + assert.ok( + /##vso\[task\.prependpath\].+/.test(runner.stdout), + 'Expected a "prependpath" logging command in task output.\n' + runner.stdout, + ); + }, + TestUtils.isSkipTest('generic'), + ); + }); + describe('Tools Installer Tests', (): void => { runSyncTest( 'Download CLI', @@ -1420,6 +1434,18 @@ function runAsyncTest(description: string, testFunc: (done: mocha.Done) => void, * @param shouldFail (Boolean, Optional) - True if the task supposed to fail */ async function mockTask(testDir: string, taskName: string, shouldFail?: boolean): Promise { + await mockTaskCapture(testDir, taskName, shouldFail); +} + +/** + * Mock a task from resources directory, returning the runner so its stdout/stderr can be inspected + * (e.g. to assert on a "##vso[...]" logging command, or to pull a value out of the task's output). + * + * @param testDir (String) - The test directory in resources + * @param taskName (String) - The '.js' file + * @param shouldFail (Boolean, Optional) - True if the task supposed to fail + */ +async function mockTaskCapture(testDir: string, taskName: string, shouldFail?: boolean): Promise { const taskPath: string = join(__dirname, 'resources', testDir, taskName + '.js'); // task.json dummy passed to the mock runner to avoid the 'Unable to find task.json, ...' warnings. const taskJsonDummy: string = join(__dirname, 'resources', 'task.json'); @@ -1427,6 +1453,7 @@ async function mockTask(testDir: string, taskName: string, shouldFail?: boolean) await mockRunner.runAsync(); tasksOutput += mockRunner.stderr + '\n' + mockRunner.stdout; assert.ok(shouldFail ? mockRunner.failed : mockRunner.succeeded, '\nFailure in: ' + taskPath + '.\n' + tasksOutput); // Check the test results + return mockRunner; } /**