Add debug/replay mode (MINOR)

This commit is contained in:
Paul Hatcherian 2023-08-20 21:33:26 -04:00
parent 4f07cfb9e0
commit d93d2fb887
20 changed files with 480 additions and 56 deletions

View file

@ -30,4 +30,8 @@ export class ActionConfig {
public enablePrereleaseMode: boolean = false;
/** If bump_each_commit is also set to true, setting this value will cause the version to increment only if the pattern specified is matched. */
public bumpEachCommitPatchPattern: string = "";
/** If enabled, diagnostic information will be added to the action output. */
public debug: boolean = false;
/** Diagnostics to replay */
public replay: string = '';
}

View file

@ -1,8 +1,15 @@
// Using require instead of import to support integration testing
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import { DebugManager } from './DebugManager';
const debugManager = DebugManager.getInstance();
export const cmd = async (command: string, ...args: any): Promise<string> => {
if (debugManager.isReplayMode()) {
return debugManager.replayCommand(command, args);
}
let output = '', errors = '';
const options = {
silent: true,
@ -14,15 +21,14 @@ export const cmd = async (command: string, ...args: any): Promise<string> => {
}
};
let caughtError: any = null;
try {
await exec.exec(command, args, options);
} catch (err) {
//core.info(`The command cd '${command} ${args.join(' ')}' failed: ${err}`);
caughtError = err;
}
if (errors !== '') {
//core.info(`stderr: ${errors}`);
}
debugManager.recordCommand(command, args, output, errors, caughtError);
return output;
};

View file

@ -15,11 +15,13 @@ import { DefaultLastReleaseResolver } from './providers/DefaultLastReleaseResolv
import { VersionClassifier } from './providers/VersionClassifier'
import { BumpAlwaysVersionClassifier } from './providers/BumpAlwaysVersionClassifier'
import { ActionConfig } from './ActionConfig';
import { DebugManager } from './DebugManager';
export class ConfigurationProvider {
constructor(config: ActionConfig) {
this.config = config;
DebugManager.getInstance().initializeConfig(config);
}
private config: ActionConfig;

111
src/DebugManager.ts Normal file
View file

@ -0,0 +1,111 @@
import exp from "constants";
import { ActionConfig } from "./ActionConfig";
/** Utility class for managing debug mode and diagnostic information */
export class DebugManager {
private constructor() { }
private static instance: DebugManager;
/** Returns the singleton instance of the DebugManager */
public static getInstance(): DebugManager {
if (!DebugManager.instance) {
DebugManager.instance = new DebugManager();
}
return DebugManager.instance;
}
private debugEnabled: boolean = false;
private replayMode: boolean = false;
private diagnosticInfo: DiagnosticInfo | null = null;
/** Returns true if debug mode is enabled */
public isDebugEnabled(): boolean {
return this.debugEnabled;
}
/** Returns true if replay mode is enabled */
public isReplayMode(): boolean {
return this.replayMode;
}
initializeConfig(config: ActionConfig) {
if (config.debug) {
this.setDebugEnabled(true);
} else if (config.replay.length > 0) {
this.replayFromDiagnostics(config.replay);
}
}
/** Enables or disables debug mode, also clears any existing diagnostics info */
public setDebugEnabled(enableDebug: boolean = true): void {
this.debugEnabled = enableDebug;
this.replayMode = false;
this.diagnosticInfo = new DiagnosticInfo();
};
/** Enables replay mode and loads the diagnostic information from the specified string */
public replayFromDiagnostics(diagnostics: string): void {
this.debugEnabled = false
this.replayMode = true;
this.diagnosticInfo = JSON.parse(diagnostics);
}
/** Returns a JSON string containing the diagnostic information for this run */
public getDebugOutput(emptyRepo: boolean = false): string {
return this.isDebugEnabled() ? JSON.stringify(this.diagnosticInfo) : '';
}
/** Records a command and its output for diagnostic purposes */
public recordCommand(command: string, args: any[], output: string, stderr: string, error: any): void {
if (this.isDebugEnabled()) {
this.diagnosticInfo?.recordCommand(command, args, output, stderr, error);
}
}
/** Replays the specified command and returns the output */
public replayCommand(command: string, args: any[]): string {
if (this.diagnosticInfo === null) {
throw new Error('No diagnostic information available for replay');
}
const commandResult = this.diagnosticInfo.commands.find(c => c.command === command && JSON.stringify(c.args) === JSON.stringify(args));
if (!commandResult) {
throw new Error(`No result found in diagnostic for command "${command}"`);
}
if (commandResult.error) {
throw commandResult.error;
}
if (commandResult.stderr) {
console.error(commandResult.stderr);
}
return commandResult.output;
}
}
/** Represents a CLI command result */
class CommandResult {
public command: string;
public args: any[];
public output: string;
public stderr: string;
public error: any;
public constructor(command: string, args: any[], output: string, stderr: string, error: any) {
this.command = command;
this.args = args;
this.output = output;
this.stderr = stderr;
this.error = error;
}
}
/** Represents the result of the commands executed for a run */
class DiagnosticInfo {
public commands: CommandResult[] = [];
public empty: boolean = false;
public recordCommand(command: string, args: any[], output: string, stderr: string, error: any): void {
this.commands.push(new CommandResult(command, args, output, stderr, error));
}
}

View file

@ -18,6 +18,7 @@ export class VersionResult {
* @param currentCommit - The current commit hash
* @param previousCommit - The previous commit hash
* @param previousVersion - The previous version
* @param debugOutput - Diagnostic information, if debug is enabled
*/
constructor(
public major: number,
@ -32,5 +33,6 @@ export class VersionResult {
public authors: string,
public currentCommit: string,
public previousCommit: string,
public previousVersion: string) { }
public previousVersion: string,
public debugOutput: string) { }
}

View file

@ -3,6 +3,7 @@ import { VersionResult } from './VersionResult';
import { VersionType } from './providers/VersionType';
import { UserInfo } from './providers/UserInfo';
import { VersionInformation } from './providers/VersionInformation';
import { DebugManager } from './DebugManager';
export async function runAction(configurationProvider: ConfigurationProvider): Promise<VersionResult> {
@ -14,6 +15,8 @@ export async function runAction(configurationProvider: ConfigurationProvider): P
const tagFormatter = configurationProvider.GetTagFormatter();
const userFormatter = configurationProvider.GetUserFormatter();
const debugManager = DebugManager.getInstance();
if (await currentCommitResolver.IsEmptyRepoAsync()) {
const versionInfo = new VersionInformation(0, 0, 0, 0, VersionType.None, [], false, false);
return new VersionResult(
@ -29,7 +32,8 @@ export async function runAction(configurationProvider: ConfigurationProvider): P
userFormatter.Format('author', []),
'',
'',
'0.0.0'
'0.0.0',
debugManager.getDebugOutput(true)
);
}
@ -71,6 +75,7 @@ export async function runAction(configurationProvider: ConfigurationProvider): P
userFormatter.Format('author', authors),
currentCommit,
lastRelease.hash,
`${lastRelease.major}.${lastRelease.minor}.${lastRelease.patch}`
`${lastRelease.major}.${lastRelease.minor}.${lastRelease.patch}`,
debugManager.getDebugOutput()
);
}

View file

@ -6,6 +6,7 @@ import { expect, test } from '@jest/globals'
import { runAction } from '../src/action';
import { ConfigurationProvider } from './ConfigurationProvider';
import { ActionConfig } from './ActionConfig';
import exp from 'constants';
const windows = process.platform === "win32";
const timeout = 30000;
@ -829,7 +830,7 @@ test('Patch does not increment on bump each commit if a patch pattern is set', a
test('Patch pattern increment is correct on empty repo', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}+${increment}", bumpEachCommit: true, bumpEachCommitPatchPattern: '(PATCH)' });
const initialResult = await repo.runAction();
repo.makeCommit('Initial Commit');
const firstResult = await repo.runAction();
@ -851,7 +852,7 @@ test('Patch pattern increment is correct on empty repo', async () => {
test('Patch pattern increment is correct/matches non-bumped on empty repo', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}+${increment}", bumpEachCommit: true, bumpEachCommitPatchPattern: '(PATCH)' });
repo.makeCommit('Initial Commit');
const firstResult = await repo.runAction();
repo.makeCommit('Second Commit');
@ -871,7 +872,7 @@ test('Patch pattern increment is correct/matches non-bumped on empty repo', asyn
test('Patch pattern applied when present', async () => {
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}+${increment}", bumpEachCommit: true, bumpEachCommitPatchPattern: '(PATCH)' });
repo.makeCommit('Initial Commit');
const firstResult = await repo.runAction();
repo.makeCommit('Second Commit');
@ -928,11 +929,11 @@ test('Prerelease tags are ignored on current commit', async () => {
repo.makeCommit(`Commit ${i++}`);
await validate('0.1.0+1');
repo.exec('git tag v1.0.0-rc2');
await validate('0.1.0+1');
await validate('0.1.0+1');
repo.makeCommit(`Commit ${i++}`);
await validate('0.1.0+2');
await validate('0.1.0+2');
repo.exec('git tag v1.0.0-rc3');
await validate('0.1.0+2');
await validate('0.1.0+2');
repo.makeCommit(`Commit ${i++}`);
await validate('0.1.0+3');
repo.exec('git tag v1.0.0');
@ -961,14 +962,14 @@ test('Pre-release mode does not update major version if major version is 0', asy
const repo = createTestRepo({ tagPrefix: '', versionFormat: "${major}.${minor}.${patch}", enablePrereleaseMode: true });
repo.makeCommit('Initial Commit');
expect(( await repo.runAction()).formattedVersion).toBe('0.0.1');
expect((await repo.runAction()).formattedVersion).toBe('0.0.1');
repo.makeCommit('Second Commit (MINOR)');
expect(( await repo.runAction()).formattedVersion).toBe('0.0.1');
expect((await repo.runAction()).formattedVersion).toBe('0.0.1');
repo.makeCommit('Third Commit (MAJOR)');
expect(( await repo.runAction()).formattedVersion).toBe('0.1.0');
expect((await repo.runAction()).formattedVersion).toBe('0.1.0');
repo.exec('git tag 0.1.0');
repo.makeCommit('Fourth Commit (MAJOR)');
expect(( await repo.runAction()).formattedVersion).toBe('0.2.0');
expect((await repo.runAction()).formattedVersion).toBe('0.2.0');
}, timeout);
test('Pre-release mode updates major version if major version is not 0', async () => {
@ -977,14 +978,14 @@ test('Pre-release mode updates major version if major version is not 0', async (
repo.makeCommit('Initial Commit');
repo.exec('git tag 1.0.0');
repo.makeCommit('Second Commit');
expect(( await repo.runAction()).formattedVersion).toBe('1.0.1');
expect((await repo.runAction()).formattedVersion).toBe('1.0.1');
repo.makeCommit('Third Commit (MINOR)');
expect(( await repo.runAction()).formattedVersion).toBe('1.1.0');
expect((await repo.runAction()).formattedVersion).toBe('1.1.0');
repo.makeCommit('Fourth Commit (MAJOR)');
expect(( await repo.runAction()).formattedVersion).toBe('2.0.0');
expect((await repo.runAction()).formattedVersion).toBe('2.0.0');
repo.exec('git tag 2.0.0');
repo.makeCommit('Fifth Commit (MAJOR)');
expect(( await repo.runAction()).formattedVersion).toBe('3.0.0');
expect((await repo.runAction()).formattedVersion).toBe('3.0.0');
}, timeout);
test('Tagged commit is flagged as release', async () => {
@ -1020,7 +1021,7 @@ test('Highest tag is chosen when multiple tags are present', async () => {
repo.makeCommit('Initial Commit');
repo.exec('git tag v2.0.0');
repo.exec('git tag v1.0.0');
repo.exec('git tag v1.5.0');
repo.exec('git tag v1.5.0');
var result = await repo.runAction();
expect(result.formattedVersion).toBe('2.0.0+0');
@ -1033,4 +1034,27 @@ test('Highest tag is chosen when multiple tags are present', async () => {
result = await repo.runAction();
expect(result.formattedVersion).toBe('3.0.1+0');
}, timeout);
}, timeout);
test('Debug records and replays expected data', async () => {
const repo = createTestRepo();
repo.makeCommit('Initial Commit');
repo.exec('git tag v3.0.0');
repo.makeCommit(`Second Commit`);
const result = await repo.runAction({ debug: true });
repo.makeCommit(`Breaking Commit`);
repo.exec('git tag v9.9.9');
var replayResult = await repo.runAction({ replay: result.debugOutput });
expect(replayResult.major).toBe(result.major);
expect(replayResult.minor).toBe(result.minor);
expect(replayResult.patch).toBe(result.patch);
expect(replayResult.increment).toBe(result.increment);
}, timeout);

View file

@ -51,7 +51,9 @@ export async function run() {
searchCommitBody: core.getInput('search_commit_body') === 'true',
userFormatType: core.getInput('user_format_type'),
enablePrereleaseMode: core.getInput('enable_prerelease_mode') === 'true',
bumpEachCommitPatchPattern: core.getInput('bump_each_commit_patch_pattern')
bumpEachCommitPatchPattern: core.getInput('bump_each_commit_patch_pattern'),
debug: core.getInput('debug') === 'true',
replay: ''
};
if (config.versionFormat === '' && core.getInput('format') !== '') {