diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b77ea2bca..62ef62546 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,6 @@ jobs: # Checkout and install prerequisites - name: Checkout uses: actions/checkout@v4 - - name: Setup NodeJS uses: actions/setup-node@v4 with: diff --git a/action.yml b/action.yml index c6be346a1..8a8d7c409 100644 --- a/action.yml +++ b/action.yml @@ -4,7 +4,7 @@ author: "JFrog" inputs: version: description: "JFrog CLI Version" - default: "2.63.2" + default: "2.66.0" required: false download-repository: description: "Remote repository in Artifactory pointing to 'https://releases.jfrog.io/artifactory/jfrog-cli'. Use this parameter in case you don't have an Internet access." diff --git a/images/job_summary.png b/images/job_summary.png index 2fc02dbd8..99b1805e2 100644 Binary files a/images/job_summary.png and b/images/job_summary.png differ diff --git a/images/summary_header.png b/images/summary_header.png new file mode 100644 index 000000000..e0272c632 Binary files /dev/null and b/images/summary_header.png differ diff --git a/lib/cleanup.js b/lib/cleanup.js index 36e6cb6c2..3b34ce679 100644 --- a/lib/cleanup.js +++ b/lib/cleanup.js @@ -40,21 +40,34 @@ function cleanup() { core.error('Could not find JFrog CLI path in the step state. Skipping cleanup.'); return; } + // Auto-publish build info if needed try { if (!core.getBooleanInput(utils_1.Utils.AUTO_BUILD_PUBLISH_DISABLE)) { + core.startGroup('Build Info Auto-Publish'); yield collectAndPublishBuildInfoIfNeeded(); + core.endGroup(); } } catch (error) { core.warning('failed while attempting to publish build info: ' + error); } + // Generate job summary try { - core.startGroup('Cleanup JFrog CLI servers configuration'); - yield utils_1.Utils.removeJFrogServers(); if (!core.getBooleanInput(utils_1.Utils.JOB_SUMMARY_DISABLE)) { - yield utils_1.Utils.generateWorkflowSummaryMarkdown(); + core.startGroup('Generating Job Summary'); + yield utils_1.Utils.runCli(['generate-summary-markdown']); + yield utils_1.Utils.setMarkdownAsJobSummary(); + core.endGroup(); } } + catch (error) { + core.warning('failed while attempting to generate job summary: ' + error); + } + // Cleanup JFrog CLI servers configuration + try { + core.startGroup('Cleanup JFrog CLI servers configuration'); + yield utils_1.Utils.removeJFrogServers(); + } catch (error) { core.setFailed(error.message); } @@ -108,9 +121,11 @@ function collectAndPublishBuildInfoIfNeeded() { try { core.startGroup('Collect the Git information'); yield utils_1.Utils.runCli(['rt', 'build-add-git'], { cwd: workingDirectory }); - } catch (error) { + } + catch (error) { core.warning('failed while attempting to collect Git information: ' + error); - } finally { + } + finally { core.endGroup(); } core.startGroup('Publish the build info to JFrog Artifactory'); diff --git a/lib/utils.js b/lib/utils.js index 727dd7934..cc7b8297e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -475,18 +475,18 @@ class Utils { return; } /** - * Generates GitHub workflow Summary Markdown. + * Generates GitHub workflow unified Summary report. * This function runs as part of post-workflow cleanup function, * collects existing section markdown files generated by the CLI, - * and constructs a single markdown file, to be displayed in the GitHub UI. + * and constructs a single Markdown file, to be displayed in the GitHub UI. */ - static generateWorkflowSummaryMarkdown() { + static setMarkdownAsJobSummary() { return __awaiter(this, void 0, void 0, function* () { try { - // Read all sections and construct the final markdown file - const markdownContent = yield this.readCLIMarkdownSectionsAndWrap(); + // Read all sections and construct the final Markdown file + const markdownContent = yield this.readCommandSummaryMarkdown(); if (markdownContent.length == 0) { - core.debug('No job summaries sections found. Workflow summary will not be generated.'); + core.debug('No job summary file found. Workflow summary will not be generated.'); return; } // Write to GitHub's job summary @@ -505,68 +505,37 @@ class Utils { * This function reads each section file and wraps it with a markdown header * @returns the content of the markdown file as string, warped in a collapsable section. */ - static readCLIMarkdownSectionsAndWrap() { + static readCommandSummaryMarkdown() { return __awaiter(this, void 0, void 0, function* () { - const outputDir = Utils.getJobOutputDirectoryPath(); - let markdownContent = ''; - const sectionContents = {}; - // Read all sections. - for (const sectionName of Utils.JOB_SUMMARY_MARKDOWN_SECTIONS_NAMES) { - const fullPath = path.join(outputDir, sectionName, 'markdown.md'); - if ((0, fs_1.existsSync)(fullPath)) { - sectionContents[sectionName] = yield Utils.readSummarySection(fullPath, sectionName); - } - } - // If build info was published, remove generic upload section to avoid duplications with generic modules. - if (sectionContents[MarkdownSection.BuildInfo] != '') { - sectionContents[MarkdownSection.Upload] = ''; - } - // Append sections in order. - for (const sectionName of Utils.JOB_SUMMARY_MARKDOWN_SECTIONS_NAMES) { - markdownContent += sectionContents[sectionName] || ''; + let markdownContent = yield Utils.readMarkdownContent(); + if (markdownContent === '') { + return ''; } - return markdownContent ? Utils.wrapContent(markdownContent) : ''; + // Check if the header can be accessed via the internet to decide if to use the image or the text header + this.isSummaryHeaderAccessible = yield this.isHeaderPngAccessible(); + core.debug('Header image is accessible: ' + this.isSummaryHeaderAccessible); + return Utils.wrapContent(markdownContent); }); } - static readSummarySection(fullPath, section) { + static readMarkdownContent() { return __awaiter(this, void 0, void 0, function* () { - let content = ''; - try { - content = yield fs_1.promises.readFile(fullPath, 'utf-8'); - return Utils.wrapCollapsableSection(section, content); - } - catch (error) { - throw new Error('failed to read section file: ' + fullPath + ' ' + error); + const outputDir = Utils.getJobOutputDirectoryPath(); + if ((0, fs_1.existsSync)(outputDir)) { + return yield fs_1.promises.readFile(path.join(outputDir, 'markdown.md'), 'utf-8'); } + return ''; }); } static getMarkdownHeader() { let mainTitle; - if (Utils.isColorSchemeSupported()) { - mainTitle = `# $\\textcolor{green}{\\textsf{ 🐸 JFrog Job Summary}}$` + '\n\n'; + if (this.isSummaryHeaderAccessible) { + mainTitle = `Summary-Header` + '\n\n'; } else { mainTitle = `# 🐸 JFrog Job Summary` + '\n\n'; } return mainTitle + Utils.getProjectPackagesLink(); } - /** - * Check if the color scheme is supported in the GitHub UI. - * Currently, GitHub enterprise does not support the color LaTex scheme $\textcolor{}. - * This scheme is part of the LaTeX/Mathematics scheme. - * - * Currently, the scheme is not supported by GitHub Enterprise version 3.13, - * which is the latest version at the time of writing this comment. - * - * For more info about the scheme see: - * https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/writing-mathematical-expressions - * - * @returns true if the color scheme is supported, false otherwise. - */ - static isColorSchemeSupported() { - let serverUrl = process.env.GITHUB_SERVER_URL || ''; - return serverUrl.startsWith('https://github.com'); - } /** * Gets the project packages link to be displayed in the summary * If the project is undefined, it will resolve to 'all' section in the UI. @@ -582,7 +551,7 @@ class Utils { } let projectKey = process.env.JF_PROJECT ? process.env.JF_PROJECT : ''; let projectPackagesUrl = platformUrl + 'ui/packages' + '?projectKey=' + projectKey; - return `📦 Project ${projectKey} packages ` + '\n\n'; + return ` 🐸 View package details on the JFrog platform ` + '\n\n'; } static getJobOutputDirectoryPath() { const outputDir = process.env[Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV]; @@ -598,29 +567,29 @@ class Utils { yield fs_1.promises.rm(outputDir, { recursive: true }); }); } - static wrapCollapsableSection(section, markdown) { - let sectionTitle; - switch (section) { - case MarkdownSection.Upload: - sectionTitle = `📁 Files uploaded to Artifactory by this workflow`; - break; - case MarkdownSection.BuildInfo: - sectionTitle = `📦 Build info published to Artifactory by this workflow`; - break; - case MarkdownSection.Security: - sectionTitle = `🔒 Security Status`; - break; - default: - throw new Error(`Failed to get unknown section: ${section}, title.`); - } - return `\n\n\n
\n\n ${sectionTitle}

\n\n ${markdown} \n\n
\n\n\n`; - } static wrapContent(fileContent) { return Utils.getMarkdownHeader() + fileContent + Utils.getMarkdownFooter(); } static getMarkdownFooter() { return '\n\n # \n\n The above Job Summary was generated by the Setup JFrog CLI GitHub Action '; } + static isHeaderPngAccessible() { + return __awaiter(this, void 0, void 0, function* () { + const url = this.MARKDOWN_HEADER_PNG_URL; + const httpClient = new http_client_1.HttpClient(); + try { + const response = yield httpClient.head(url); + return response.message.statusCode === 200; + } + catch (error) { + core.warning('No internet access to the header image, using the text header instead.'); + return false; + } + finally { + httpClient.dispose(); + } + }); + } } exports.Utils = Utils; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -665,3 +634,5 @@ 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'; +// Source URL holding the markdown header image +Utils.MARKDOWN_HEADER_PNG_URL = 'https://media.jfrog.com/wp-content/uploads/2024/09/01120106/summary_header.png'; diff --git a/src/cleanup.ts b/src/cleanup.ts index ebf8171c2..f508781c0 100644 --- a/src/cleanup.ts +++ b/src/cleanup.ts @@ -6,20 +6,31 @@ async function cleanup() { core.error('Could not find JFrog CLI path in the step state. Skipping cleanup.'); return; } + // Auto-publish build info if needed try { if (!core.getBooleanInput(Utils.AUTO_BUILD_PUBLISH_DISABLE)) { + core.startGroup('Build Info Auto-Publish'); await collectAndPublishBuildInfoIfNeeded(); + core.endGroup(); } } catch (error) { core.warning('failed while attempting to publish build info: ' + error); } - + // Generate job summary try { - core.startGroup('Cleanup JFrog CLI servers configuration'); - await Utils.removeJFrogServers(); if (!core.getBooleanInput(Utils.JOB_SUMMARY_DISABLE)) { - await Utils.generateWorkflowSummaryMarkdown(); + core.startGroup('Generating Job Summary'); + await Utils.runCli(['generate-summary-markdown']); + await Utils.setMarkdownAsJobSummary(); + core.endGroup(); } + } catch (error) { + core.warning('failed while attempting to generate job summary: ' + error); + } + // Cleanup JFrog CLI servers configuration + try { + core.startGroup('Cleanup JFrog CLI servers configuration'); + await Utils.removeJFrogServers(); } catch (error) { core.setFailed((error).message); } finally { diff --git a/src/utils.ts b/src/utils.ts index 8f7c83b9e..809c1be32 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -61,6 +61,9 @@ 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'; + // Source URL holding the markdown header image + private static MARKDOWN_HEADER_PNG_URL: string = 'https://media.jfrog.com/wp-content/uploads/2024/09/01120106/summary_header.png'; + private static isSummaryHeaderAccessible: boolean; /** * Retrieves server credentials for accessing JFrog's server @@ -525,17 +528,17 @@ export class Utils { } /** - * Generates GitHub workflow Summary Markdown. + * Generates GitHub workflow unified Summary report. * This function runs as part of post-workflow cleanup function, * collects existing section markdown files generated by the CLI, - * and constructs a single markdown file, to be displayed in the GitHub UI. + * and constructs a single Markdown file, to be displayed in the GitHub UI. */ - public static async generateWorkflowSummaryMarkdown() { + public static async setMarkdownAsJobSummary() { try { - // Read all sections and construct the final markdown file - const markdownContent: string = await this.readCLIMarkdownSectionsAndWrap(); + // Read all sections and construct the final Markdown file + const markdownContent: string = await this.readCommandSummaryMarkdown(); if (markdownContent.length == 0) { - core.debug('No job summaries sections found. Workflow summary will not be generated.'); + core.debug('No job summary file found. Workflow summary will not be generated.'); return; } // Write to GitHub's job summary @@ -553,70 +556,35 @@ export class Utils { * This function reads each section file and wraps it with a markdown header * @returns the content of the markdown file as string, warped in a collapsable section. */ - private static async readCLIMarkdownSectionsAndWrap(): Promise { - const outputDir: string = Utils.getJobOutputDirectoryPath(); - let markdownContent: string = ''; - const sectionContents: { [key: string]: string } = {}; - - // Read all sections. - for (const sectionName of Utils.JOB_SUMMARY_MARKDOWN_SECTIONS_NAMES) { - const fullPath: string = path.join(outputDir, sectionName, 'markdown.md'); - if (existsSync(fullPath)) { - sectionContents[sectionName] = await Utils.readSummarySection(fullPath, sectionName); - } - } - - // If build info was published, remove generic upload section to avoid duplications with generic modules. - if (sectionContents[MarkdownSection.BuildInfo] != '') { - sectionContents[MarkdownSection.Upload] = ''; - } - - // Append sections in order. - for (const sectionName of Utils.JOB_SUMMARY_MARKDOWN_SECTIONS_NAMES) { - markdownContent += sectionContents[sectionName] || ''; + private static async readCommandSummaryMarkdown(): Promise { + let markdownContent: string = await Utils.readMarkdownContent(); + if (markdownContent === '') { + return ''; } - - return markdownContent ? Utils.wrapContent(markdownContent) : ''; + // Check if the header can be accessed via the internet to decide if to use the image or the text header + this.isSummaryHeaderAccessible = await this.isHeaderPngAccessible(); + core.debug('Header image is accessible: ' + this.isSummaryHeaderAccessible); + return Utils.wrapContent(markdownContent); } - private static async readSummarySection(fullPath: string, section: MarkdownSection) { - let content: string = ''; - try { - content = await fs.readFile(fullPath, 'utf-8'); - return Utils.wrapCollapsableSection(section, content); - } catch (error) { - throw new Error('failed to read section file: ' + fullPath + ' ' + error); + private static async readMarkdownContent() { + const outputDir: string = Utils.getJobOutputDirectoryPath(); + if (existsSync(outputDir)) { + return await fs.readFile(path.join(outputDir, 'markdown.md'), 'utf-8'); } + return ''; } private static getMarkdownHeader(): string { let mainTitle: string; - if (Utils.isColorSchemeSupported()) { - mainTitle = `# $\\textcolor{green}{\\textsf{ 🐸 JFrog Job Summary}}$` + '\n\n'; + if (this.isSummaryHeaderAccessible) { + mainTitle = `Summary-Header` + '\n\n'; } else { mainTitle = `# 🐸 JFrog Job Summary` + '\n\n'; } return mainTitle + Utils.getProjectPackagesLink(); } - /** - * Check if the color scheme is supported in the GitHub UI. - * Currently, GitHub enterprise does not support the color LaTex scheme $\textcolor{}. - * This scheme is part of the LaTeX/Mathematics scheme. - * - * Currently, the scheme is not supported by GitHub Enterprise version 3.13, - * which is the latest version at the time of writing this comment. - * - * For more info about the scheme see: - * https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/writing-mathematical-expressions - * - * @returns true if the color scheme is supported, false otherwise. - */ - static isColorSchemeSupported(): boolean { - let serverUrl: string = process.env.GITHUB_SERVER_URL || ''; - return serverUrl.startsWith('https://github.com'); - } - /** * Gets the project packages link to be displayed in the summary * If the project is undefined, it will resolve to 'all' section in the UI. @@ -632,7 +600,7 @@ export class Utils { } let projectKey: string = process.env.JF_PROJECT ? process.env.JF_PROJECT : ''; let projectPackagesUrl: string = platformUrl + 'ui/packages' + '?projectKey=' + projectKey; - return `📦 Project ${projectKey} packages ` + '\n\n'; + return ` 🐸 View package details on the JFrog platform ` + '\n\n'; } private static getJobOutputDirectoryPath(): string { @@ -649,24 +617,6 @@ export class Utils { await fs.rm(outputDir, { recursive: true }); } - private static wrapCollapsableSection(section: MarkdownSection, markdown: string): string { - let sectionTitle: string; - switch (section) { - case MarkdownSection.Upload: - sectionTitle = `📁 Files uploaded to Artifactory by this workflow`; - break; - case MarkdownSection.BuildInfo: - sectionTitle = `📦 Build info published to Artifactory by this workflow`; - break; - case MarkdownSection.Security: - sectionTitle = `🔒 Security Status`; - break; - default: - throw new Error(`Failed to get unknown section: ${section}, title.`); - } - return `\n\n\n
\n\n ${sectionTitle}

\n\n ${markdown} \n\n
\n\n\n`; - } - private static wrapContent(fileContent: string) { return Utils.getMarkdownHeader() + fileContent + Utils.getMarkdownFooter(); } @@ -674,6 +624,20 @@ export class Utils { private static getMarkdownFooter() { return '\n\n # \n\n The above Job Summary was generated by the Setup JFrog CLI GitHub Action '; } + + private static async isHeaderPngAccessible(): Promise { + const url: string = this.MARKDOWN_HEADER_PNG_URL; + const httpClient: HttpClient = new HttpClient(); + try { + const response: HttpClientResponse = await httpClient.head(url); + return response.message.statusCode === 200; + } catch (error) { + core.warning('No internet access to the header image, using the text header instead.'); + return false; + } finally { + httpClient.dispose(); + } + } } export interface DownloadDetails { diff --git a/test/main.spec.ts b/test/main.spec.ts index 40b3f844c..ca864b635 100644 --- a/test/main.spec.ts +++ b/test/main.spec.ts @@ -298,7 +298,7 @@ describe('decodeOidcToken', () => { describe('Job Summaries', () => { describe('Job summaries sanity', () => { it('should not crash if no files were found', async () => { - expect(async () => await Utils.generateWorkflowSummaryMarkdown()).not.toThrow(); + expect(async () => await Utils.setMarkdownAsJobSummary()).not.toThrow(); }); }); describe('Command Summaries Disable Flag', () => { @@ -329,20 +329,3 @@ describe('Job Summaries', () => { }); }); }); - -describe('isColorSchemeSupported', () => { - it('should return true if GITHUB_SERVER_URL includes github.com', () => { - process.env.GITHUB_SERVER_URL = 'https://github.com'; - expect(Utils.isColorSchemeSupported()).toBe(true); - }); - - it('should return false if GITHUB_SERVER_URL does not include github.com', () => { - process.env.GITHUB_SERVER_URL = 'https://enterprise.github.com'; - expect(Utils.isColorSchemeSupported()).toBe(false); - }); - - it('should return false if GITHUB_SERVER_URL is undefined', () => { - delete process.env.GITHUB_SERVER_URL; - expect(Utils.isColorSchemeSupported()).toBe(false); - }); -});