Skip to content

Commit

Permalink
Automatic build-add-git & build-publish on post job run (#185)
Browse files Browse the repository at this point in the history
  • Loading branch information
sverdlov93 authored Sep 1, 2024
1 parent 2a8318a commit a978f6e
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 28 deletions.
77 changes: 77 additions & 0 deletions .github/workflows/auto-build-publish-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Auto Build Publish Test
on:
push:
branches:
- master
# Triggers the workflow on labeled PRs only.
pull_request_target:
types: [ labeled ]
# Ensures that only the latest commit is running for each PR at a time.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.ref }}
cancel-in-progress: true

jobs:
Auto-Build-Publish-Test:
if: contains(github.event.pull_request.labels.*.name, 'safe to test') || github.event_name == 'push'
strategy:
fail-fast: false
matrix:
os: [ ubuntu, windows, macos ]
runs-on: ${{ matrix.os }}-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Setup Go with cache
uses: jfrog/.github/actions/install-go-with-cache@main

- name: Install local Artifactory
uses: jfrog/.github/actions/install-local-artifactory@main
with:
RTLIC: ${{ secrets.RTLIC }}

- name: Post Step to Test the Auto Build-Publish post step
uses: gacts/run-and-post-run@v1
with:
post: |
jf c add --artifactory-url http://localhost:8081/artifactory --user admin --password password; \
count=$(jf rt search "artifactory-build-info/${GITHUB_WORKFLOW}/${GITHUB_RUN_NUMBER}-*" --count 2>stderr.log); \
exit_code=$?; \
cat stderr.log; \
if [ $exit_code -ne 0 ]; then \
echo "Command failed with exit code $exit_code."; \
exit 1; \
fi; \
if [ "$count" -eq 0 ]; then \
echo "No build info found."; \
exit 1; \
fi
- name: Setup JFrog CLI
id: setup-jfrog-cli
uses: ./
env:
JF_URL: http://localhost:8081/
JF_USER: admin
JF_PASSWORD: password

- name: Create NPM Remote Repository JSON
uses: jsdaniell/[email protected]
with:
name: "npm-remote-template.json"
json: '{
"key": "npm-remote",
"rclass": "remote",
"packageType": "npm",
"url": "https://registry.npmjs.org"
}'

- name: Configure Artifactory NPM Remote Repository
run: jf rt repo-create npm-remote-template.json
shell: bash

- name: Add npm modules to local build-info
run: |
jf npm-config --repo-resolve npm-remote
jf npm install
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ inputs:
description: "Set to true to disable the generation of Job Summaries."
default: "false"
required: false
disable-auto-build-publish:
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
outputs:
oidc-token:
description: "JFrog OIDC token generated by the Setup JFrog CLI when setting oidc-provider-name."
Expand Down
73 changes: 67 additions & 6 deletions lib/cleanup.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,20 @@ const core = __importStar(require("@actions/core"));
const utils_1 = require("./utils");
function cleanup() {
return __awaiter(this, void 0, void 0, function* () {
if (!addCachedJfToPath()) {
core.error('Could not find JFrog CLI path in the step state. Skipping cleanup.');
return;
}
try {
if (!addCachedJfToPath()) {
core.error('Could not find JFrog CLI path in the step state. Skipping cleanup.');
return;
if (!core.getBooleanInput(utils_1.Utils.AUTO_BUILD_PUBLISH_DISABLE)) {
yield collectAndPublishBuildInfoIfNeeded();
}
}
catch (error) {
core.warning('failed while attempting to publish build info: ' + error);
}
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();
Expand All @@ -56,12 +65,64 @@ function cleanup() {
}
function addCachedJfToPath() {
// Get the JFrog CLI path from step state. saveState/getState are methods to pass data between a step, and it's cleanup function.
const jfrogCliPath = core.getState(utils_1.Utils.JF_CLI_PATH_STATE);
if (!jfrogCliPath) {
const jfCliPath = core.getState(utils_1.Utils.JF_CLI_PATH_STATE);
if (!jfCliPath) {
// This means that the JFrog CLI was not installed in the first place, because there was a failure in the installation step.
return false;
}
core.addPath(jfrogCliPath);
core.addPath(jfCliPath);
return true;
}
function hasUnpublishedModules(workingDirectory) {
return __awaiter(this, void 0, void 0, function* () {
// Save the old value of the environment variable to revert it later
const origValue = process.env[utils_1.Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV];
try {
// Avoid saving a command summary for this dry-run command
core.exportVariable(utils_1.Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV, '');
// Running build-publish command with a dry-run flag to check if there are any unpublished modules, 'silent' to avoid polluting the logs
const responseStr = yield utils_1.Utils.runCliAndGetOutput(['rt', 'build-publish', '--dry-run'], { silent: true, cwd: workingDirectory });
// Parse the JSON string to an object
const response = JSON.parse(responseStr);
// Check if the "modules" key exists and if it's an array with more than one item
return response.modules != undefined && Array.isArray(response.modules) && response.modules.length > 0;
}
catch (error) {
core.error('Failed to parse JSON: ' + error);
return false; // Return false if parsing fails
}
finally {
core.exportVariable(utils_1.Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV, origValue);
}
});
}
function collectAndPublishBuildInfoIfNeeded() {
return __awaiter(this, void 0, void 0, function* () {
const workingDirectory = getWorkingDirectory();
// Check if there are any unpublished modules
if (!(yield hasUnpublishedModules(workingDirectory))) {
return;
}
// The flow here is to collect Git information before publishing the build info.
// We allow this step to fail, and we don't want to fail the entire build publish if they do.
try {
core.startGroup('Collect the Git information');
yield utils_1.Utils.runCli(['rt', 'build-add-git'], { cwd: workingDirectory });
} catch (error) {
core.warning('failed while attempting to collect Git information: ' + error);
} finally {
core.endGroup();
}
core.startGroup('Publish the build info to JFrog Artifactory');
yield utils_1.Utils.runCli(['rt', 'build-publish'], { cwd: workingDirectory });
core.endGroup();
});
}
function getWorkingDirectory() {
const workingDirectory = process.env.GITHUB_WORKSPACE;
if (!workingDirectory) {
throw new Error('GITHUB_WORKSPACE is not defined.');
}
return workingDirectory;
}
cleanup();
40 changes: 34 additions & 6 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ class Utils {
static enableJobSummaries() {
let commandSummariesOutputDir = process.env.RUNNER_TEMP;
if (commandSummariesOutputDir) {
Utils.exportVariableIfNotSet('JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR', commandSummariesOutputDir);
Utils.exportVariableIfNotSet(Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV, commandSummariesOutputDir);
}
}
static exportVariableIfNotSet(key, value) {
Expand Down Expand Up @@ -394,15 +394,39 @@ class Utils {
* This GitHub Action downloads the requested 'jfrog' executable and stores it as 'jfrog' and 'jf'.
* Therefore, the 'jf' executable is expected to be in the path also for older CLI versions.
* @param args - CLI arguments
* @param options - Execution options
*/
static runCli(args) {
static runCli(args, options) {
return __awaiter(this, void 0, void 0, function* () {
let res = yield (0, exec_1.exec)('jf', args);
let res = yield (0, exec_1.exec)('jf', args, options);
if (res !== core.ExitCode.Success) {
throw new Error('JFrog CLI exited with exit code ' + res);
}
});
}
/**
* Execute JFrog CLI command and capture its output.
* This GitHub Action downloads the requested 'jfrog' executable and stores it as 'jfrog' and 'jf'.
* Therefore, the 'jf' executable is expected to be in the path also for older CLI versions.
* The command's output is captured and returned as a string.
* The command is executed silently, meaning its output will not be printed to the console.
* If the command fails (i.e., exits with a non-success code), an error is thrown.
* @param args - CLI arguments
* @param options
* @returns The standard output of the CLI command as a string.
* @throws An error if the JFrog CLI command exits with a non-success code.
*/
static runCliAndGetOutput(args, options) {
return __awaiter(this, void 0, void 0, function* () {
let output = yield (0, exec_1.getExecOutput)('jf', args, options);
if (output.exitCode !== core.ExitCode.Success) {
core.info(output.stdout);
core.info(output.stderr);
throw new Error('JFrog CLI exited with exit code ' + output.exitCode);
}
return output.stdout;
});
}
/**
* If repository input was set, extract CLI download details,
* from either a Config Token with a JF_ENV_ prefix or separate env config (JF_URL, JF_USER, JF_PASSWORD, JF_ACCESS_TOKEN).
Expand Down Expand Up @@ -561,9 +585,9 @@ class Utils {
return `<a href="${projectPackagesUrl}">📦 Project ${projectKey} packages </a>` + '\n\n';
}
static getJobOutputDirectoryPath() {
const outputDir = process.env.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR;
const outputDir = process.env[Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV];
if (!outputDir) {
throw new Error('Jobs home directory is undefined, JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR is not set.');
throw new Error('Jobs home directory is undefined, ' + Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV + ' is not set.');
}
return path.join(outputDir, Utils.JOB_SUMMARY_DIR_NAME);
}
Expand Down Expand Up @@ -620,6 +644,8 @@ Utils.JF_CLI_PATH_STATE = 'JF_CLI_PATH_STATE';
Utils.SETUP_JFROG_CLI_SERVER_ID = 'setup-jfrog-cli-server';
// Directory name which holds markdown files for the Workflow summary
Utils.JOB_SUMMARY_DIR_NAME = 'jfrog-command-summary';
// JFrog CLI command summary output directory environment variable
Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV = 'JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR';
// Workflow summary section files. Order of sections in this array impacts the order in the final markdown.
Utils.JOB_SUMMARY_MARKDOWN_SECTIONS_NAMES = [
MarkdownSection.Security,
Expand All @@ -635,5 +661,7 @@ Utils.CLI_REMOTE_ARG = 'download-repository';
Utils.OIDC_AUDIENCE_ARG = 'oidc-audience';
// OpenID Connect provider_name input
Utils.OIDC_INTEGRATION_PROVIDER_NAME = 'oidc-provider-name';
// Job Summaries feature disable flag
// Disable Job Summaries feature flag
Utils.JOB_SUMMARY_DISABLE = 'disable-job-summary';
// Disable auto build info publish feature flag
Utils.AUTO_BUILD_PUBLISH_DISABLE = 'disable-auto-build-publish';
78 changes: 72 additions & 6 deletions src/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ import * as core from '@actions/core';
import { Utils } from './utils';

async function cleanup() {
if (!addCachedJfToPath()) {
core.error('Could not find JFrog CLI path in the step state. Skipping cleanup.');
return;
}
try {
if (!addCachedJfToPath()) {
core.error('Could not find JFrog CLI path in the step state. Skipping cleanup.');
return;
if (!core.getBooleanInput(Utils.AUTO_BUILD_PUBLISH_DISABLE)) {
await collectAndPublishBuildInfoIfNeeded();
}
} catch (error) {
core.warning('failed while attempting to publish build info: ' + error);
}

try {
core.startGroup('Cleanup JFrog CLI servers configuration');
await Utils.removeJFrogServers();
if (!core.getBooleanInput(Utils.JOB_SUMMARY_DISABLE)) {
await Utils.generateWorkflowSummaryMarkdown();
Expand All @@ -21,13 +29,71 @@ async function cleanup() {

function addCachedJfToPath(): boolean {
// Get the JFrog CLI path from step state. saveState/getState are methods to pass data between a step, and it's cleanup function.
const jfrogCliPath: string = core.getState(Utils.JF_CLI_PATH_STATE);
if (!jfrogCliPath) {
const jfCliPath: string = core.getState(Utils.JF_CLI_PATH_STATE);
if (!jfCliPath) {
// This means that the JFrog CLI was not installed in the first place, because there was a failure in the installation step.
return false;
}
core.addPath(jfrogCliPath);
core.addPath(jfCliPath);
return true;
}

interface BuildPublishResponse {
modules: any[];
}

async function hasUnpublishedModules(workingDirectory: string): Promise<boolean> {
// Save the old value of the environment variable to revert it later
const origValue: string | undefined = process.env[Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV];
try {
// Avoid saving a command summary for this dry-run command
core.exportVariable(Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV, '');

// Running build-publish command with a dry-run flag to check if there are any unpublished modules, 'silent' to avoid polluting the logs
const responseStr: string = await Utils.runCliAndGetOutput(['rt', 'build-publish', '--dry-run'], { silent: true, cwd: workingDirectory });

// Parse the JSON string to an object
const response: BuildPublishResponse = JSON.parse(responseStr);
// Check if the "modules" key exists and if it's an array with more than one item
return response.modules != undefined && Array.isArray(response.modules) && response.modules.length > 0;
} catch (error) {
core.error('Failed to parse JSON: ' + error);
return false; // Return false if parsing fails
} finally {
core.exportVariable(Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV, origValue);
}
}

async function collectAndPublishBuildInfoIfNeeded() {
const workingDirectory: string = getWorkingDirectory();
// Check if there are any unpublished modules
if (!(await hasUnpublishedModules(workingDirectory))) {
return;
}

// The flow here is to collect Git information before publishing the build info.
// We allow this step to fail, and we don't want to fail the entire build publish if they do.

try {
core.startGroup('Collect the Git information');
await Utils.runCli(['rt', 'build-add-git'], { cwd: workingDirectory });
} catch (error) {
core.warning('failed while attempting to collect Git information: ' + error);
} finally {
core.endGroup();
}

core.startGroup('Publish the build info to JFrog Artifactory');
await Utils.runCli(['rt', 'build-publish'], { cwd: workingDirectory });
core.endGroup();
}

function getWorkingDirectory(): string {
const workingDirectory: string | undefined = process.env.GITHUB_WORKSPACE;
if (!workingDirectory) {
throw new Error('GITHUB_WORKSPACE is not defined.');
}
return workingDirectory;
}

cleanup();
Loading

0 comments on commit a978f6e

Please sign in to comment.