From ee8c11710bb4f9e5515a42fee007ed488e2fbb75 Mon Sep 17 00:00:00 2001 From: Liang Zhu Date: Fri, 18 May 2018 17:01:17 -0700 Subject: [PATCH] A few bug fixes 1) normalize file path 2) skip webpage control 3) skip non-custom type of pages 4) allow rule import failure as optional parameter 5) allow user choose if import control/group/form contributions 6) add jsonc support for config file --- .vscode/launch.json | 4 +-- README.md | 4 +-- package-lock.json | 7 ++++- package.json | 3 +- src/Common/Constants.ts | 25 ++++++++------- src/Common/Interfaces.ts | 3 ++ src/Common/ProcessExporter.ts | 1 - src/Common/ProcessImporter.ts | 47 ++++++++++++++++++++-------- src/Common/Utilities.ts | 38 +++++++++++++++------- src/NodeJs/ConfigurationProcessor.ts | 12 ++++--- src/NodeJs/Main.ts | 6 ++-- src/NodeJs/NodeJsUtilities.ts | 4 +-- 12 files changed, 101 insertions(+), 53 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 320bae6..b2290f0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,11 +5,11 @@ "type": "node", "request": "launch", "name": "Launch process import/export ", - "program": "${workspaceFolder}/build/main.js", + "program": "${workspaceFolder}/build/nodejs/nodejs/main.js", "sourceMaps": true, "args": [ "--mode=both", - "--config=configuration.json", + "--config=output\\repro.json", "--overwriteProcessOnTarget" ] } diff --git a/README.md b/README.md index 4e589b1..c1aefa6 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ NOTE: This only works with 'Inherited Process', for 'XML process' you can upload ## Run - Install npm if haven't - [link](https://www.npmjs.com/get-npm) -- Install this package through `npm install vsts-process-import-export -g` +- Install this package through `npm install process-import-export -g` - Create a configuration.json, see [doc section](#documentation) for explanation on details -- Run `vstspie --mode= [--config=] [--overwriteProcessOnTarget]` +- Run `pie --mode= [--config=] [--overwriteProcessOnTarget]` ## Contribute diff --git a/package-lock.json b/package-lock.json index de5c76e..81bc0a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "process-import-export", - "version": "0.9.0", + "version": "0.9.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -68,6 +68,11 @@ "resolved": "https://witiq.pkgs.visualstudio.com/_packaging/processimportexport/npm/registry/guid-typescript/-/guid-typescript-1.0.7.tgz", "integrity": "sha512-j1XPiaDUuNa0PO8EyGAikyKMpAnGbSDzr81UAzXz3H2xAoQW3c2hH8RS7Omo+DEgtD9emYdgJaxnfKZPYtDNRQ==" }, + "jsonc-parser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.0.0.tgz", + "integrity": "sha512-gYk8VcFDwky0AjrKeJSWgCm/lYGteP9hszGWtgg67Elz4owvhJF9qATjuIRAk5jgBMGM65MPAc+I4RTeoqoykA==" + }, "minimist": { "version": "1.2.0", "resolved": "https://witiq.pkgs.visualstudio.com/_packaging/processimportexport/npm/registry/minimist/-/minimist-1.2.0.tgz", diff --git a/package.json b/package.json index 0a04e81..d0ba976 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "process-import-export", - "version": "0.9.0", + "version": "0.9.2", "description": "Proces import/export Node.js application", "main": "", "bin": { @@ -35,6 +35,7 @@ }, "dependencies": { "guid-typescript": "^1.0.7", + "jsonc-parser": "^2.0.0", "minimist": "^1.2.0", "mkdirp": "^0.5.1", "url": "^0.11.0", diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index d8a361d..8495888 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -1,24 +1,27 @@ export const PICKLIST_NO_ACTION = "PICKLIST_NO_ACTION"; export const defaultEncoding = "utf-8"; export const defaultConfigurationFilename = "configuration.json"; -export const defaultLogFileName = "output\\process_import_export.log"; +export const defaultLogFileName = "output\\pie.log"; export const defaultProcessFilename = "output\\process.json"; export const paramMode = "mode"; export const paramConfig = "config"; export const paramOverwriteProcessOnTarget = "overwriteProcessOnTarget"; export const defaultConfiguration = - { + `{ "sourceAccountUrl": "", - "sourceAccountToken": "", + "sourceAccountToken": "", "targetAccountUrl": "", - "targetAccountToken": "", - "sourceProcessName": "Process name for export, optional in import only mode, required in export/both mode", - "targetProcessName": "Set to override process name on import, remove from param name", + "targetAccountToken": "", + "sourceProcessName": "Process name for export, ignored in import only mode, required in export/both mode", + // "targetProcessName": "Set to override process name on import", "options": { - "processFilename": "Set to override default export file name, remove from param name", - "logLevel": "Set to override default log level (Information), remove from param name", - "logFilename": "Set to override default log file name, remove from param name", - "overwritePicklist": false + // "processFilename": "Default is 'output/process.json', set to override default export file name", + // "logLevel": "Default is information, set to override log level, possible values are verbose/information/warning/error", + // "logFilename": "Default is 'output/pie.log', set to override default log file name", + // "continueOnRuleImportFailure": "Default is false, Set true to continue import on failure importing rules, warning will still be provided" + // "skipImportControlContributions": "Default is false, Set true to skip import control contributions on work item form." + // "skipImportGroupOrPageContributions": "Default is true, Set false to allow import group/page contributions on work item form. This should only be used when you want to hide contribution group/page." + // "overwritePicklist": "Default is false, Set true to overwrite picklist if exist on target account." } - }; + }`; export const regexRemoveHypen = new RegExp("-","g"); \ No newline at end of file diff --git a/src/Common/Interfaces.ts b/src/Common/Interfaces.ts index 5fe693d..72d04fa 100644 --- a/src/Common/Interfaces.ts +++ b/src/Common/Interfaces.ts @@ -43,6 +43,9 @@ export interface IConfigurationOptions { logFilename?: string; processFilename?: string; overwritePicklist?: boolean; + continueOnRuleImportFailure?: boolean; + skipImportControlContributions?: boolean; + skipImportGroupOrPageContributions?: boolean; } export interface IImportConfiguration extends IConfigurationFile { diff --git a/src/Common/ProcessExporter.ts b/src/Common/ProcessExporter.ts index c0a8684..f45917c 100644 --- a/src/Common/ProcessExporter.ts +++ b/src/Common/ProcessExporter.ts @@ -1,4 +1,3 @@ -import { existsSync, writeFileSync } from "fs"; import * as WITProcessDefinitionsInterfaces from "vso-node-api/interfaces/WorkItemTrackingProcessDefinitionsInterfaces"; import * as WITProcessInterfaces from "vso-node-api/interfaces/WorkItemTrackingProcessInterfaces"; diff --git a/src/Common/ProcessImporter.ts b/src/Common/ProcessImporter.ts index 45a2791..bb6abd6 100644 --- a/src/Common/ProcessImporter.ts +++ b/src/Common/ProcessImporter.ts @@ -8,7 +8,7 @@ import { IWorkItemTrackingProcessDefinitionsApi as WITProcessDefinitionApi_NOREQ import { IWorkItemTrackingProcessApi as WITProcessApi_NOREQUIRE } from "vso-node-api/WorkItemTrackingProcessApi"; import { IWorkItemTrackingApi as WITApi_NOREQUIRE } from "vso-node-api/WorkItemTrackingApi"; -import { PICKLIST_NO_ACTION, regexRemoveHypen } from "./Constants"; +import { PICKLIST_NO_ACTION } from "./Constants"; import { Engine } from "./Engine"; import { ImportError, ValidationError } from "./Errors"; import { ICommandLineOptions, IConfigurationFile, IDictionaryStringTo, IProcessPayload, IWITLayout, IWITRules, IWITStates, IRestClients } from "./Interfaces"; @@ -188,7 +188,11 @@ export class ProcessImporter { private async _importPage(targetLayout: WITProcessDefinitionsInterfaces.FormLayout, witLayout: IWITLayout, page: WITProcessDefinitionsInterfaces.Page, payload: IProcessPayload) { if (!page) { - throw new ImportError(`Encourtered null page in work item type '${witLayout.workItemTypeRefName}'`); + throw new ImportError(`Encountered null page in work item type '${witLayout.workItemTypeRefName}'`); + } + + if (page.isContribution && !this._config.options.skipImportGroupOrPageContributions !== false) { + // skip import page contriubtion unless user explicitly asks so } let newPage: WITProcessDefinitionsInterfaces.Page; //The newly created page, contains the pageId required to create groups. @@ -214,6 +218,10 @@ export class ProcessImporter { for (const group of section.groups) { let newGroup: WITProcessDefinitionsInterfaces.Group; + if (group.isContribution === true && !this._config.options.skipImportGroupOrPageContributions !== false) { + // skip import group contriubtion unless user explicitly asks so + } + if (group.controls.length !== 0 && group.controls[0].controlType === "HtmlFieldControl") { //Handle groups with HTML Controls const createGroup: WITProcessDefinitionsInterfaces.Group = Utility.toCreateGroup(group); @@ -272,20 +280,27 @@ export class ProcessImporter { try { let createControl: WITProcessDefinitionsInterfaces.Control = Utility.toCreateControl(control); + if (control.controlType === "WebpageControl" || (control.isContribution === true && this._config.options.skipImportControlContributions)) { + // Skip web page control for now since not supported in inherited process. + continue; + } + if (control.inherited) { if (control.overridden) { //edit - await this._witProcessDefinitionApi.editControl(createControl, payload.process.typeId, witLayout.workItemTypeRefName, group.id, control.id); + await Engine.Task(() => this._witProcessDefinitionApi.editControl(createControl, payload.process.typeId, witLayout.workItemTypeRefName, group.id, control.id), + `Edit control '${control.id}' in group '${group.id}' in page '${page.id}' in work item type '${witLayout.workItemTypeRefName}'.`); } } else { //create - await this._witProcessDefinitionApi.addControlToGroup(createControl, payload.process.typeId, witLayout.workItemTypeRefName, group.id); + await Engine.Task(() => this._witProcessDefinitionApi.addControlToGroup(createControl, payload.process.typeId, witLayout.workItemTypeRefName, group.id), + `Create control '${control.id}' in group '${group.id}' in page '${page.id}' in work item type '${witLayout.workItemTypeRefName}'.`); } } catch (error) { Utility.handleKnownError(error); - throw new ImportError(`Unable to add '${control}' control to page '${page}' in '${witLayout.workItemTypeRefName}'. ${error}`); + throw new ImportError(`Unable to add '${control.id}' control to group '${group.id}' in page '${page.id}' in '${witLayout.workItemTypeRefName}'. ${error}`); } } } @@ -304,7 +319,9 @@ export class ProcessImporter { () => this._witProcessDefinitionApi.getFormLayout(payload.process.typeId, witLayoutEntry.workItemTypeRefName), `Get layout on target process for work item type '${witLayoutEntry.workItemTypeRefName}'`); for (const page of witLayoutEntry.layout.pages) { - await this._importPage(targetLayout, witLayoutEntry, page, payload); + if (page.pageType === WITProcessDefinitionsInterfaces.PageType.Custom) { + await this._importPage(targetLayout, witLayoutEntry, page, payload); + } } } } @@ -329,7 +346,7 @@ export class ProcessImporter { const existingStates: WITProcessDefinitionsInterfaces.WorkItemStateResultModel[] = targetWITStates.filter(targetState => sourceState.name === targetState.name); if (existingStates.length === 0) { // does not exist on target const createdState = await Engine.Task( - () => this._witProcessDefinitionApi.createStateDefinition(sourceState, payload.process.typeId, witStateEntry.workItemTypeRefName), + () => this._witProcessDefinitionApi.createStateDefinition(Utility.toCreateOrUpdateStateDefinition(sourceState), payload.process.typeId, witStateEntry.workItemTypeRefName), `Create state '${sourceState.name}' in '${witStateEntry.workItemTypeRefName}' work item type`); if (!createdState || !createdState.id) { throw new ImportError(`Unable to create state '${sourceState.name}' in '${witStateEntry.workItemTypeRefName}' work item type, server returned empty result or id.`); @@ -349,7 +366,7 @@ export class ProcessImporter { if (sourceState.color !== existingState.color || sourceState.stateCategory !== existingState.stateCategory || sourceState.name !== existingState.name) { // Inherited state can be edited in custom work item types. const updatedState = await Engine.Task( - () => this._witProcessDefinitionApi.updateStateDefinition(Utility.toUdpateStateDefinition(sourceState), payload.process.typeId, witStateEntry.workItemTypeRefName, existingState.id), + () => this._witProcessDefinitionApi.updateStateDefinition(Utility.toCreateOrUpdateStateDefinition(sourceState), payload.process.typeId, witStateEntry.workItemTypeRefName, existingState.id), `Update state '${sourceState.name}' in '${witStateEntry.workItemTypeRefName}' work item type`); if (!updatedState || updatedState.name !== sourceState.name) { throw new ImportError(`Unable to update state '${sourceState.name}' in '${witStateEntry.workItemTypeRefName}' work item type, server returned empty result, id or state is not hidden.`); @@ -377,7 +394,6 @@ export class ProcessImporter { } } - private async _importStates(payload: IProcessPayload): Promise { for (const witStateEntry of payload.states) { await this._importWITStates(witStateEntry, payload); @@ -395,8 +411,13 @@ export class ProcessImporter { } } catch (error) { - Utility.handleKnownError(error); - throw new ImportError(`Unable to create rule '${rule.id}' in work item type '${witRulesEntry.workItemTypeRefName}', see logs for details.`); + if (this._config.options.continueOnRuleImportFailure === true) { + logger.logWarning(`Failed to import rule below, continue importing rest of process.\r\n:Error:${error}\r\n${JSON.stringify(rule, null, 2)}`); + } + else { + Utility.handleKnownError(error); + throw new ImportError(`Unable to create rule '${rule.id}' in work item type '${witRulesEntry.workItemTypeRefName}', see logs for details.`); + } } } @@ -427,7 +448,7 @@ export class ProcessImporter { const createBehavior: WITProcessDefinitionsInterfaces.BehaviorCreateModel = Utility.toCreateBehavior(behavior); // Use a random name to avoid conflict on scenarios involving a name swap behaviorIdToRealNameBehavior[behavior.id] = Utility.toReplaceBehavior(behavior); - createBehavior.name = Guid.create().toString().replace(regexRemoveHypen, ""); + createBehavior.name = Utility.createGuidWithoutHyphen(); const createdBehavior = await Engine.Task( () => this._witProcessDefinitionApi.createBehavior(createBehavior, payload.process.typeId), `Create behavior '${behavior.id}' with fake name '${behavior.name}'`); @@ -438,7 +459,7 @@ export class ProcessImporter { else { const replaceBehavior: WITProcessDefinitionsInterfaces.BehaviorReplaceModel = Utility.toReplaceBehavior(behavior); behaviorIdToRealNameBehavior[behavior.id] = Utility.toReplaceBehavior(behavior); - replaceBehavior.name = Guid.create().toString().replace(regexRemoveHypen, ""); + replaceBehavior.name = Utility.createGuidWithoutHyphen(); const replacedBehavior = await Engine.Task( () => this._witProcessDefinitionApi.replaceBehavior(replaceBehavior, payload.process.typeId, behavior.id), `Replace behavior '${behavior.id}' with fake name '${behavior.name}'`); diff --git a/src/Common/Utilities.ts b/src/Common/Utilities.ts index 4b015b3..45efe6e 100644 --- a/src/Common/Utilities.ts +++ b/src/Common/Utilities.ts @@ -5,6 +5,8 @@ import { KnownError } from "./Errors"; import { logger } from "./Logger"; import { Modes, IConfigurationFile, LogLevel } from "./Interfaces"; import * as url from "url"; +import { Guid } from "guid-typescript"; +import { regexRemoveHypen } from "./Constants"; export class Utility { /** Convert from WITProcess FieldModel to WITProcessDefinitions FieldModel @@ -56,7 +58,7 @@ export class Utility { description: processModel.description, name: processModel.name, parentProcessTypeId: processModel.properties.parentProcessTypeId, - referenceName: processModel.referenceName + referenceName: Utility.createGuidWithoutHyphen() // Reference name does not really matter since we already have typeId }; return createModel; } @@ -64,7 +66,7 @@ export class Utility { /**Convert group from getLayout group interface to WITProcessDefinitionsInterfaces.Group * @param group */ - public static toCreateGroup(group: any/*TODO: Change this type, not any*/): WITProcessDefinitionsInterfaces.Group { + public static toCreateGroup(group: WITProcessDefinitionsInterfaces.Group): WITProcessDefinitionsInterfaces.Group { let createGroup: WITProcessDefinitionsInterfaces.Group = { id: group.id, inherited: group.inherited, @@ -72,8 +74,8 @@ export class Utility { isContribution: group.isContribution, visible: group.visible, controls: null, - contribution: null, - height: null, + contribution: group.contribution, + height: group.height, order: null, overridden: null } @@ -83,7 +85,7 @@ export class Utility { /**Convert control from getLayout control interface to WITProcessDefinitionsInterfaces.Control * @param control */ - public static toCreateControl(control: any/*TODO: Change this type, not any*/): WITProcessDefinitionsInterfaces.Control { + public static toCreateControl(control: WITProcessDefinitionsInterfaces.Control): WITProcessDefinitionsInterfaces.Control { let createControl: WITProcessDefinitionsInterfaces.Control = { id: control.id, inherited: control.inherited, @@ -94,8 +96,8 @@ export class Utility { metadata: control.metadata, visible: control.visible, isContribution: control.isContribution, - contribution: null, - height: null, + contribution: control.contribution, + height: control.height, order: null, overridden: null } @@ -105,17 +107,17 @@ export class Utility { /**Convert page from getLayout page interface to WITProcessDefinitionsInterfaces.Page * @param control */ - public static toCreatePage(page: any/*TODO: Change this type, not any*/): WITProcessDefinitionsInterfaces.Page { + public static toCreatePage(page: WITProcessDefinitionsInterfaces.Page): WITProcessDefinitionsInterfaces.Page { let createPage: WITProcessDefinitionsInterfaces.Page = { id: page.id, inherited: page.inherited, label: page.label, pageType: page.pageType, - locked: page.loacked, + locked: page.locked, visible: page.visible, isContribution: page.isContribution, - sections: null,//yeah?? - contribution: null, + sections: null, + contribution: page.contribution, order: null, overridden: null } @@ -125,7 +127,7 @@ export class Utility { /**Convert a state result to state input * @param group */ - public static toUdpateStateDefinition(state: WITProcessInterfaces.WorkItemStateResultModel): WITProcessDefinitionsInterfaces.WorkItemStateInputModel { + public static toCreateOrUpdateStateDefinition(state: WITProcessInterfaces.WorkItemStateResultModel): WITProcessDefinitionsInterfaces.WorkItemStateInputModel { const updateState: WITProcessDefinitionsInterfaces.WorkItemStateInputModel = { color: state.color, name: state.name, @@ -198,6 +200,14 @@ export class Utility { logger.logError(`[Configuration validation] Option 'overwritePicklist' is not a valid boolean.`); return false; } + if (configuration.options && configuration.options.continueOnRuleImportFailure && (configuration.options.continueOnRuleImportFailure !== true && configuration.options.continueOnRuleImportFailure !== false)) { + logger.logError(`[Configuration validation] Option 'continueOnRuleImportFailure' is not a valid boolean.`); + return false; + } + if (configuration.options && configuration.options.skipImportControlContributions && (configuration.options.skipImportControlContributions !== true && configuration.options.skipImportControlContributions !== false)) { + logger.logError(`[Configuration validation] Option 'skipImportControlContributions' is not a valid boolean.`); + return false; + } } if (configuration.options && configuration.options.logLevel && LogLevel[configuration.options.logLevel] === undefined) { @@ -212,5 +222,9 @@ export class Utility { return Utility.isCancelled; } + public static createGuidWithoutHyphen(): string { + return Guid.create().toString().replace(regexRemoveHypen, ""); + } + protected static isCancelled = false; } diff --git a/src/NodeJs/ConfigurationProcessor.ts b/src/NodeJs/ConfigurationProcessor.ts index e3d3559..7daed7d 100644 --- a/src/NodeJs/ConfigurationProcessor.ts +++ b/src/NodeJs/ConfigurationProcessor.ts @@ -1,10 +1,12 @@ import { existsSync, readFileSync, writeFileSync } from "fs"; +import { normalize } from "path"; import * as minimist from "minimist"; import * as url from "url"; import { defaultConfiguration, defaultConfigurationFilename, defaultEncoding, paramConfig, paramMode, paramOverwriteProcessOnTarget } from "../common/Constants"; import { IConfigurationFile, LogLevel, Modes, ICommandLineOptions } from "../common/Interfaces"; import { logger } from "../common/Logger"; import { Utility } from "../common/Utilities"; +import { parse as jsoncParse } from "jsonc-parser"; export function ProcesCommandLine(): ICommandLineOptions { const parseOptions: minimist.Opts = { @@ -22,7 +24,7 @@ export function ProcesCommandLine(): ICommandLineOptions { process.exit(0); } - const configFileName = parsedArgs[paramConfig] || defaultConfigurationFilename; + const configFileName = parsedArgs[paramConfig] || normalize(defaultConfigurationFilename); const userSpecifiedMode = parsedArgs[paramMode] as string; let mode; @@ -38,12 +40,12 @@ export function ProcesCommandLine(): ICommandLineOptions { mode = Modes.both; } - const ret = {}; + const ret = {}; ret[paramMode] = mode; ret[paramConfig] = configFileName; ret[paramOverwriteProcessOnTarget] = !!parsedArgs[paramOverwriteProcessOnTarget]; - return ret; + return ret; } export async function ProcessConfigurationFile(configFilename: string, mode: Modes): Promise { @@ -56,8 +58,8 @@ export async function ProcessConfigurationFile(configFilename: string, mode: Mod } process.exit(1); } - - const configuration = JSON.parse(await readFileSync(configFilename, defaultEncoding)) as IConfigurationFile; + + const configuration = jsoncParse(readFileSync(configFilename, defaultEncoding)) as IConfigurationFile; if (!Utility.validateConfiguration(configuration, mode)) { process.exit(1); } diff --git a/src/NodeJs/Main.ts b/src/NodeJs/Main.ts index e2869ef..2039a9b 100644 --- a/src/NodeJs/Main.ts +++ b/src/NodeJs/Main.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node import { existsSync, readFileSync } from "fs"; -import { resolve } from "path"; +import { resolve, normalize } from "path"; import { ProcesCommandLine, ProcessConfigurationFile } from "./ConfigurationProcessor"; import { defaultEncoding, defaultProcessFilename } from "../common/Constants"; import { ImportError, KnownError } from "../common/Errors"; @@ -40,7 +40,7 @@ async function main() { const exporter: ProcessExporter = new ProcessExporter(sourceRestClients, configuration); processPayload = await exporter.exportProcess(); - const exportFilename = (configuration.options && configuration.options.processFilename) || defaultProcessFilename; + const exportFilename = (configuration.options && configuration.options.processFilename) || normalize(defaultProcessFilename); await Engine.Task(() => NodeJsUtility.writeJsonToFile(exportFilename, processPayload), "Write process payload to file") logger.logInfo(`Export process completed successfully to '${resolve(exportFilename)}'.`); } @@ -48,7 +48,7 @@ async function main() { // Import if (mode === Modes.both || mode === Modes.import) { if (mode === Modes.import) { // Read payload from file instead - const processFileName = (configuration.options && configuration.options.processFilename) || defaultProcessFilename; + const processFileName = (configuration.options && configuration.options.processFilename) || normalize(defaultProcessFilename); if (!existsSync(processFileName)) { throw new ImportError(`Process payload file '${processFileName}' does not exist.`) } diff --git a/src/NodeJs/NodeJsUtilities.ts b/src/NodeJs/NodeJsUtilities.ts index 6be3c86..eb8d3d1 100644 --- a/src/NodeJs/NodeJsUtilities.ts +++ b/src/NodeJs/NodeJsUtilities.ts @@ -1,6 +1,6 @@ import * as vsts from "vso-node-api/WebApi"; import { existsSync, writeFileSync } from "fs"; -import { dirname } from "path"; +import { dirname, normalize } from "path"; import { sync as mkdirpSync } from "mkdirp"; import * as readline from "readline"; import { isFunction } from "util"; @@ -33,7 +33,7 @@ export class NodeJsUtility extends Utility { } public static getLogFilePath(options: IConfigurationOptions): string { - return options.logFilename ? options.logFilename : defaultLogFileName; + return options.logFilename ? options.logFilename : normalize(defaultLogFileName); } public static async getRestClients(accountUrl: string, PAT: string): Promise {