From da7423c011299967aa0ac9eac1dd7d5dd46c8ba6 Mon Sep 17 00:00:00 2001 From: Robi Nino Date: Wed, 4 Dec 2024 10:59:07 +0200 Subject: [PATCH] Server configuration improvements (#227) --- README.md | 21 ++++++++++++++++ action.yml | 3 +++ lib/cleanup.js | 18 +++++++++++-- lib/utils.js | 59 ++++++++++++++++++++++++++++++++++++++++--- src/cleanup.ts | 19 ++++++++++++-- src/utils.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++--- test/main.spec.ts | 44 +++++++++++++++++++++++++++++--- 7 files changed, 212 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 1a1f4693..9a8747a8 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ - [Setting JFrog CLI Version](#setting-jfrog-cli-version) - [Setting the JFrog Project Key](#setting-the-jfrog-project-key) - [Downloading JFrog CLI from Artifactory](#downloading-jfrog-cli-from-artifactory) + - [Custom Server ID and Multi-Configuration](#custom-server-id-and-multi-configuration) - [JFrog Job Summary](#jfrog-job-summary) - [Code Scanning Alerts](#code-scanning-alerts) - [Example Projects](#example-projects) @@ -301,6 +302,26 @@ Here's how you do this: ``` +
+ Custom Server ID and Multi-Configuration + +### Custom Server ID and Multi-Configuration + +The action configures JFrog CLI with a default server ID, which is unique for each run of a workflow. + +You may override the default server ID by providing a custom server ID: + + ```yml + - uses: jfrog/setup-jfrog-cli@v4 + with: + custom-server-id: my-server + ``` + +You may also use multiple configurations in the same workflow by providing a custom server ID for each configuration. + +Alternating between configurations can be done by providing the `--server-id` option to JFrog CLI commands or by setting a default server using `jf c use `. +
+ ## JFrog Job Summary Workflows using this GitHub action will output a summary of some of the key commands that were performed using JFrog CLI. diff --git a/action.yml b/action.yml index 1a9a88d9..393cc15b 100644 --- a/action.yml +++ b/action.yml @@ -23,6 +23,9 @@ inputs: description: "By default, if the workflow completes with collected build-info that has not been published using the jf rt build-publish command, the build-info will be automatically published to Artifactory. Set this to true to disable the automatic publication of build-info at the end of the workflow." default: "false" required: false + custom-server-id: + description: "Custom JFrog CLI configuration server ID to use instead of the default one generated by the action." + required: false outputs: oidc-token: description: "JFrog OIDC token generated by the Setup JFrog CLI when setting oidc-provider-name." diff --git a/lib/cleanup.js b/lib/cleanup.js index 06492779..9d11dc0d 100644 --- a/lib/cleanup.js +++ b/lib/cleanup.js @@ -36,8 +36,7 @@ const core = __importStar(require("@actions/core")); const utils_1 = require("./utils"); function cleanup() { return __awaiter(this, void 0, void 0, function* () { - if (!utils_1.Utils.loadFromCache(core.getInput(utils_1.Utils.CLI_VERSION_ARG))) { - core.warning('Could not find JFrog CLI executable. Skipping cleanup.'); + if (yield shouldSkipCleanup()) { return; } // Run post tasks related to Build Info (auto build publish, job summary) @@ -198,4 +197,19 @@ function generateJobSummary() { } }); } +function shouldSkipCleanup() { + return __awaiter(this, void 0, void 0, function* () { + if (!utils_1.Utils.loadFromCache(core.getInput(utils_1.Utils.CLI_VERSION_ARG))) { + core.warning('Could not find JFrog CLI executable. Skipping cleanup.'); + return true; + } + // Skip cleanup if no servers are configured (already removed) + const servers = process.env[utils_1.Utils.JFROG_CLI_SERVER_IDS_ENV_VAR]; + if (!servers) { + core.debug('No servers are configured. Skipping cleanup.'); + return true; + } + return false; + }); +} cleanup(); diff --git a/lib/utils.js b/lib/utils.js index 6fd5fcad..ef210422 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -315,7 +315,7 @@ class Utils { let password = jfrogCredentials.password; let accessToken = jfrogCredentials.accessToken; if (url) { - let configCmd = [Utils.SETUP_JFROG_CLI_SERVER_ID, '--url', url, '--interactive=false', '--overwrite=true']; + let configCmd = [Utils.getServerIdForConfig(), '--url', url, '--interactive=false', '--overwrite=true']; if (accessToken) { configCmd.push('--access-token', accessToken); } @@ -325,6 +325,36 @@ class Utils { return configCmd; } } + /** + * Get server ID for JFrog CLI configuration. Save the server ID in the servers env var if it doesn't already exist. + */ + static getServerIdForConfig() { + let serverId = Utils.getCustomOrDefaultServerId(); + // Add new serverId to the servers env var if it doesn't already exist. + if (Utils.getConfiguredJFrogServers().includes(serverId)) { + return serverId; + } + const currentValue = process.env[Utils.JFROG_CLI_SERVER_IDS_ENV_VAR]; + const newVal = currentValue ? `${currentValue};${serverId}` : serverId; + core.exportVariable(Utils.JFROG_CLI_SERVER_IDS_ENV_VAR, newVal); + return serverId; + } + /** + * Return custom server ID if provided, or default server ID otherwise. + */ + static getCustomOrDefaultServerId() { + let customServerId = core.getInput(Utils.CUSTOM_SERVER_ID); + if (customServerId) { + return customServerId; + } + return Utils.getRunDefaultServerId(); + } + /** + * Return a server ID that is unique for this workflow run based on the GitHub repository and run ID. + */ + static getRunDefaultServerId() { + return [Utils.SETUP_JFROG_CLI_SERVER_ID_PREFIX, process.env.GITHUB_REPOSITORY, process.env.GITHUB_RUN_ID].join('-'); + } static setCliEnv() { Utils.exportVariableIfNotSet('JFROG_CLI_ENV_EXCLUDE', '*password*;*secret*;*key*;*token*;*auth*;JF_ARTIFACTORY_*;JF_ENV_*;JF_URL;JF_USER;JF_PASSWORD;JF_ACCESS_TOKEN'); Utils.exportVariableIfNotSet('JFROG_CLI_OFFER_CONFIG', 'false'); @@ -376,11 +406,28 @@ class Utils { } }); } + /** + * Removed configured JFrog CLI servers that are saved in the servers env var, and unset the env var. + */ static removeJFrogServers() { return __awaiter(this, void 0, void 0, function* () { - yield Utils.runCli(['c', 'rm', '--quiet']); + for (const serverId of Utils.getConfiguredJFrogServers()) { + core.debug(`Removing server ID: '${serverId}'...`); + yield Utils.runCli(['c', 'rm', serverId, '--quiet']); + } + core.exportVariable(Utils.JFROG_CLI_SERVER_IDS_ENV_VAR, ''); }); } + /** + * Split and return the configured JFrog CLI servers that are saved in the servers env var. + */ + static getConfiguredJFrogServers() { + const serversValue = process.env[Utils.JFROG_CLI_SERVER_IDS_ENV_VAR]; + if (!serversValue) { + return []; + } + return serversValue.split(';'); + } static getArchitecture() { if (Utils.isWindows()) { return 'windows-amd64'; @@ -726,8 +773,10 @@ Utils.LATEST_CLI_VERSION = 'latest'; Utils.LATEST_RELEASE_VERSION = '[RELEASE]'; // Placeholder CLI version to use to keep 'latest' in cache. Utils.LATEST_SEMVER = '100.100.100'; -// The default server id name for separate env config -Utils.SETUP_JFROG_CLI_SERVER_ID = 'setup-jfrog-cli-server'; +// The prefix for the default server id name for JFrog CLI config +Utils.SETUP_JFROG_CLI_SERVER_ID_PREFIX = 'setup-jfrog-cli-server'; +// Environment variable to hold all configured server IDs, separated by ';' +Utils.JFROG_CLI_SERVER_IDS_ENV_VAR = 'SETUP_JFROG_CLI_SERVER_IDS'; // Directory name which holds markdown files for the Workflow summary Utils.JOB_SUMMARY_DIR_NAME = 'jfrog-command-summary'; // Directory name which holds security command summary files @@ -753,6 +802,8 @@ Utils.OIDC_INTEGRATION_PROVIDER_NAME = 'oidc-provider-name'; Utils.JOB_SUMMARY_DISABLE = 'disable-job-summary'; // Disable auto build info publish feature flag Utils.AUTO_BUILD_PUBLISH_DISABLE = 'disable-auto-build-publish'; +// Custom server ID input +Utils.CUSTOM_SERVER_ID = 'custom-server-id'; // URL for the markdown header image // This is hosted statically because its usage is outside the context of the JFrog setup action. // It cannot be linked to the repository, as GitHub serves the image from a CDN, diff --git a/src/cleanup.ts b/src/cleanup.ts index 9c1ac8f5..281e7f4d 100644 --- a/src/cleanup.ts +++ b/src/cleanup.ts @@ -2,10 +2,10 @@ import * as core from '@actions/core'; import { Utils } from './utils'; async function cleanup() { - if (!Utils.loadFromCache(core.getInput(Utils.CLI_VERSION_ARG))) { - core.warning('Could not find JFrog CLI executable. Skipping cleanup.'); + if (await shouldSkipCleanup()) { return; } + // Run post tasks related to Build Info (auto build publish, job summary) await buildInfoPostTasks(); @@ -158,4 +158,19 @@ async function generateJobSummary() { } } +async function shouldSkipCleanup(): Promise { + if (!Utils.loadFromCache(core.getInput(Utils.CLI_VERSION_ARG))) { + core.warning('Could not find JFrog CLI executable. Skipping cleanup.'); + return true; + } + + // Skip cleanup if no servers are configured (already removed) + const servers: string | undefined = process.env[Utils.JFROG_CLI_SERVER_IDS_ENV_VAR]; + if (!servers) { + core.debug('No servers are configured. Skipping cleanup.'); + return true; + } + return false; +} + cleanup(); diff --git a/src/utils.ts b/src/utils.ts index 06a861d4..ed9680c6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -33,8 +33,10 @@ export class Utils { private static readonly LATEST_RELEASE_VERSION: string = '[RELEASE]'; // Placeholder CLI version to use to keep 'latest' in cache. public static readonly LATEST_SEMVER: string = '100.100.100'; - // The default server id name for separate env config - public static readonly SETUP_JFROG_CLI_SERVER_ID: string = 'setup-jfrog-cli-server'; + // The prefix for the default server id name for JFrog CLI config + public static readonly SETUP_JFROG_CLI_SERVER_ID_PREFIX: string = 'setup-jfrog-cli-server'; + // Environment variable to hold all configured server IDs, separated by ';' + public static readonly JFROG_CLI_SERVER_IDS_ENV_VAR: string = 'SETUP_JFROG_CLI_SERVER_IDS'; // Directory name which holds markdown files for the Workflow summary private static readonly JOB_SUMMARY_DIR_NAME: string = 'jfrog-command-summary'; // Directory name which holds security command summary files @@ -61,6 +63,8 @@ export class Utils { public static readonly JOB_SUMMARY_DISABLE: string = 'disable-job-summary'; // Disable auto build info publish feature flag public static readonly AUTO_BUILD_PUBLISH_DISABLE: string = 'disable-auto-build-publish'; + // Custom server ID input + private static readonly CUSTOM_SERVER_ID: string = 'custom-server-id'; // URL for the markdown header image // This is hosted statically because its usage is outside the context of the JFrog setup action. // It cannot be linked to the repository, as GitHub serves the image from a CDN, @@ -356,7 +360,7 @@ export class Utils { let accessToken: string | undefined = jfrogCredentials.accessToken; if (url) { - let configCmd: string[] = [Utils.SETUP_JFROG_CLI_SERVER_ID, '--url', url, '--interactive=false', '--overwrite=true']; + let configCmd: string[] = [Utils.getServerIdForConfig(), '--url', url, '--interactive=false', '--overwrite=true']; if (accessToken) { configCmd.push('--access-token', accessToken); } else if (user && password) { @@ -366,6 +370,40 @@ export class Utils { } } + /** + * Get server ID for JFrog CLI configuration. Save the server ID in the servers env var if it doesn't already exist. + */ + private static getServerIdForConfig(): string { + let serverId: string = Utils.getCustomOrDefaultServerId(); + + // Add new serverId to the servers env var if it doesn't already exist. + if (Utils.getConfiguredJFrogServers().includes(serverId)) { + return serverId; + } + const currentValue: string | undefined = process.env[Utils.JFROG_CLI_SERVER_IDS_ENV_VAR]; + const newVal: string = currentValue ? `${currentValue};${serverId}` : serverId; + core.exportVariable(Utils.JFROG_CLI_SERVER_IDS_ENV_VAR, newVal); + return serverId; + } + + /** + * Return custom server ID if provided, or default server ID otherwise. + */ + private static getCustomOrDefaultServerId(): string { + let customServerId: string = core.getInput(Utils.CUSTOM_SERVER_ID); + if (customServerId) { + return customServerId; + } + return Utils.getRunDefaultServerId(); + } + + /** + * Return a server ID that is unique for this workflow run based on the GitHub repository and run ID. + */ + static getRunDefaultServerId(): string { + return [Utils.SETUP_JFROG_CLI_SERVER_ID_PREFIX, process.env.GITHUB_REPOSITORY, process.env.GITHUB_RUN_ID].join('-'); + } + public static setCliEnv() { Utils.exportVariableIfNotSet( 'JFROG_CLI_ENV_EXCLUDE', @@ -428,8 +466,26 @@ export class Utils { } } + /** + * Removed configured JFrog CLI servers that are saved in the servers env var, and unset the env var. + */ public static async removeJFrogServers() { - await Utils.runCli(['c', 'rm', '--quiet']); + for (const serverId of Utils.getConfiguredJFrogServers()) { + core.debug(`Removing server ID: '${serverId}'...`); + await Utils.runCli(['c', 'rm', serverId, '--quiet']); + } + core.exportVariable(Utils.JFROG_CLI_SERVER_IDS_ENV_VAR, ''); + } + + /** + * Split and return the configured JFrog CLI servers that are saved in the servers env var. + */ + public static getConfiguredJFrogServers(): string[] { + const serversValue: string | undefined = process.env[Utils.JFROG_CLI_SERVER_IDS_ENV_VAR]; + if (!serversValue) { + return []; + } + return serversValue.split(';'); } public static getArchitecture() { diff --git a/test/main.spec.ts b/test/main.spec.ts index 98ae7c63..10a63b2e 100644 --- a/test/main.spec.ts +++ b/test/main.spec.ts @@ -111,7 +111,7 @@ describe('Collect JFrog Credentials from env vars exceptions', () => { }); }); -test('Get separate env config', async () => { +function testConfigCommand(expectedServerId: string) { // No url let configCommand: string[] | undefined = Utils.getSeparateEnvConfigArgs({} as JfrogCredentials); expect(configCommand).toBe(undefined); @@ -121,14 +121,14 @@ test('Get separate env config', async () => { // No credentials configCommand = Utils.getSeparateEnvConfigArgs(jfrogCredentials); - expect(configCommand).toStrictEqual([Utils.SETUP_JFROG_CLI_SERVER_ID, '--url', DEFAULT_CLI_URL, '--interactive=false', '--overwrite=true']); + expect(configCommand).toStrictEqual([expectedServerId, '--url', DEFAULT_CLI_URL, '--interactive=false', '--overwrite=true']); // Basic authentication jfrogCredentials.username = 'user'; jfrogCredentials.password = 'password'; configCommand = Utils.getSeparateEnvConfigArgs(jfrogCredentials); expect(configCommand).toStrictEqual([ - Utils.SETUP_JFROG_CLI_SERVER_ID, + expectedServerId, '--url', DEFAULT_CLI_URL, '--interactive=false', @@ -145,7 +145,7 @@ test('Get separate env config', async () => { jfrogCredentials.accessToken = 'accessToken'; configCommand = Utils.getSeparateEnvConfigArgs(jfrogCredentials); expect(configCommand).toStrictEqual([ - Utils.SETUP_JFROG_CLI_SERVER_ID, + expectedServerId, '--url', DEFAULT_CLI_URL, '--interactive=false', @@ -153,6 +153,42 @@ test('Get separate env config', async () => { '--access-token', 'accessToken', ]); +} + +describe('JFrog CLI Configuration', () => { + beforeAll(() => { + process.env.GITHUB_REPOSITORY = 'owner/repo'; + process.env.GITHUB_RUN_ID = '1'; + }); + + afterAll(() => { + ['GITHUB_REPOSITORY', 'GITHUB_RUN_ID', Utils.JFROG_CLI_SERVER_IDS_ENV_VAR].forEach((envKey) => { + delete process.env[envKey]; + }); + }); + const myCore: jest.Mocked = core as any; + + test('Get separate env config', async () => { + myCore.exportVariable = jest.fn().mockImplementation((name: string, val: string) => { + process.env[name] = val; + }); + + // Before setting a custom server ID, expect the default server ID to be used. + testConfigCommand(Utils.getRunDefaultServerId()); + + // Expect the custom server ID to be used. + let customServerId: string = 'custom-server-id'; + jest.spyOn(core, 'getInput').mockReturnValue(customServerId); + testConfigCommand(customServerId); + + // Expect the servers env var to include both servers. + const servers: string[] = Utils.getConfiguredJFrogServers(); + expect(servers).toStrictEqual([Utils.getRunDefaultServerId(), customServerId]); + }); + + test('Get default server ID', async () => { + expect(Utils.getRunDefaultServerId()).toStrictEqual('setup-jfrog-cli-server-owner/repo-1'); + }); }); describe('JFrog CLI V1 URL Tests', () => {