diff --git a/action.yml b/action.yml new file mode 100644 index 00000000..c1ae95cd --- /dev/null +++ b/action.yml @@ -0,0 +1,24 @@ +# Azure PowerShell Action +name: 'Azure PowerShell Action' +description: 'Automate your GitHub workflows using Azure PowerShell scripts.' +inputs: + inlineScript: + description: 'Specify the Az PowerShell script here.' + required: true + azPSVersion: + description: 'Azure PS version to be used to execute the script, example: 1.8.0, 2.8.0, 3.4.0. To use the latest version, specify "latest".' + required: true + errorActionPreference: + description: 'Select the value of the ErrorActionPreference variable for executing the script. Options: stop, continue, silentlyContinue. Default is Stop.' + required: false + default: 'Stop' + failOnStandardError: + description: 'If this is true, this task will fail if any errors are written to the error pipeline, or if any data is written to the Standard Error stream.' + required: false + default: 'false' +branding: + icon: 'login.svg' + color: 'blue' +runs: + using: 'node12' + main: 'lib/main.js' \ No newline at end of file diff --git a/lib/Constants.js b/lib/Constants.js new file mode 100644 index 00000000..fee3439c --- /dev/null +++ b/lib/Constants.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +class Constants { +} +exports.default = Constants; +Constants.prefix = "az_"; +Constants.moduleName = "Az"; +Constants.versionPattern = /[0-9]\.[0-9]\.[0-9]/; +Constants.Success = "Success"; +Constants.Error = "Error"; +Constants.AzVersion = "AzVersion"; +Constants.versionExists = "versionExists"; diff --git a/lib/InitializeAzure.js b/lib/InitializeAzure.js new file mode 100644 index 00000000..79b353ca --- /dev/null +++ b/lib/InitializeAzure.js @@ -0,0 +1,40 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const Utils_1 = __importDefault(require("./Utilities/Utils")); +const Constants_1 = __importDefault(require("./Constants")); +class InitializeAzure { + static importAzModule(azPSVersion) { + return __awaiter(this, void 0, void 0, function* () { + Utils_1.default.setPSModulePath(); + if (azPSVersion === "latest") { + azPSVersion = yield Utils_1.default.getLatestModule(Constants_1.default.moduleName); + } + else { + yield Utils_1.default.checkModuleVersion(Constants_1.default.moduleName, azPSVersion); + } + core.debug(`Az Module version used: ${azPSVersion}`); + Utils_1.default.setPSModulePath(`${Constants_1.default.prefix}${azPSVersion}`); + }); + } +} +exports.default = InitializeAzure; diff --git a/lib/ScriptRunner.js b/lib/ScriptRunner.js new file mode 100644 index 00000000..ce4ada1e --- /dev/null +++ b/lib/ScriptRunner.js @@ -0,0 +1,70 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const FileUtils_1 = __importDefault(require("./Utilities/FileUtils")); +const PowerShellToolRunner_1 = __importDefault(require("./Utilities/PowerShellToolRunner")); +const ScriptBuilder_1 = __importDefault(require("./Utilities/ScriptBuilder")); +class ScriptRunner { + constructor(inlineScript, errorActionPreference, failOnStandardErr) { + this.inlineScript = inlineScript; + this.errorActionPreference = errorActionPreference; + this.failOnStandardErr = failOnStandardErr; + } + executeFile() { + return __awaiter(this, void 0, void 0, function* () { + const error = []; + const options = { + listeners: { + stderr: (data) => { + if (error.length < 10) { + // Truncate to at most 1000 bytes + if (data.length > 1000) { + error.push(`${data.toString('utf8', 0, 1000)}`); + } + else { + error.push(data.toString('utf8')); + } + } + else if (error.length === 10) { + error.push('Additional writes to stderr truncated'); + } + } + } + }; + const scriptToExecute = new ScriptBuilder_1.default().getInlineScriptFile(this.inlineScript, this.errorActionPreference); + ScriptRunner.filePath = yield FileUtils_1.default.createScriptFile(scriptToExecute); + core.debug(`script file to run: ${ScriptRunner.filePath}`); + yield PowerShellToolRunner_1.default.init(); + const exitCode = yield PowerShellToolRunner_1.default.executePowerShellScriptBlock(ScriptRunner.filePath, options); + if (exitCode !== 0) { + core.setOutput(`Azure PowerShell exited with code:`, exitCode.toString()); + if (this.failOnStandardErr) { + error.forEach((err) => { + core.error(err); + }); + throw new Error(`Standard error stream contains one or more lines`); + } + } + }); + } +} +exports.default = ScriptRunner; diff --git a/lib/Utilities/FileUtils.js b/lib/Utilities/FileUtils.js new file mode 100644 index 00000000..d1411fd7 --- /dev/null +++ b/lib/Utilities/FileUtils.js @@ -0,0 +1,50 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = __importStar(require("fs")); +const os = __importStar(require("os")); +const path = __importStar(require("path")); +const core = __importStar(require("@actions/core")); +const uuid_1 = require("uuid"); +class FileUtils { + static createScriptFile(inlineScript) { + return __awaiter(this, void 0, void 0, function* () { + const fileName = FileUtils.getFileName(); + const filePath = path.join(FileUtils.tempDirectory, fileName); + fs.writeFileSync(filePath, inlineScript, 'utf-8'); + return filePath; + }); + } + static getFileName() { + return `${uuid_1.v4()}.ps1`; + } + static deleteFile(filePath) { + return __awaiter(this, void 0, void 0, function* () { + if (fs.existsSync(filePath)) { + try { + fs.unlinkSync(filePath); + } + catch (err) { + core.warning(err.toString()); + } + } + }); + } +} +exports.default = FileUtils; +FileUtils.tempDirectory = process.env.RUNNER_TEMP || os.tmpdir(); diff --git a/lib/Utilities/PowerShellToolRunner.js b/lib/Utilities/PowerShellToolRunner.js new file mode 100644 index 00000000..aefef784 --- /dev/null +++ b/lib/Utilities/PowerShellToolRunner.js @@ -0,0 +1,41 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const io = __importStar(require("@actions/io")); +const exec = __importStar(require("@actions/exec")); +class PowerShellToolRunner { + static init() { + return __awaiter(this, void 0, void 0, function* () { + if (!PowerShellToolRunner.psPath) { + PowerShellToolRunner.psPath = yield io.which("pwsh", true); + } + }); + } + static executePowerShellCommand(command, options = {}) { + return __awaiter(this, void 0, void 0, function* () { + yield exec.exec(`${PowerShellToolRunner.psPath} -NoLogo -NoProfile -NonInteractive -Command ${command}`, [], options); + }); + } + static executePowerShellScriptBlock(scriptBlock, options = {}) { + return __awaiter(this, void 0, void 0, function* () { + const exitCode = yield exec.exec(`${PowerShellToolRunner.psPath} -NoLogo -NoProfile -NonInteractive -Command`, [scriptBlock], options); + return exitCode; + }); + } +} +exports.default = PowerShellToolRunner; diff --git a/lib/Utilities/ScriptBuilder.js b/lib/Utilities/ScriptBuilder.js new file mode 100644 index 00000000..5d403d14 --- /dev/null +++ b/lib/Utilities/ScriptBuilder.js @@ -0,0 +1,60 @@ +"use strict"; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const os = __importStar(require("os")); +const core = __importStar(require("@actions/core")); +const Constants_1 = __importDefault(require("../Constants")); +class ScriptBuilder { + constructor() { + this.script = ""; + } + getLatestModuleScript(moduleName) { + const command = `Get-Module -Name ${moduleName} -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1`; + this.script += `try { + $ErrorActionPreference = "Stop" + $WarningPreference = "SilentlyContinue" + $output = @{} + $data = ${command} + $output['${Constants_1.default.AzVersion}'] = $data.Version.ToString() + $output['${Constants_1.default.Success}'] = "true" + } + catch { + $output['${Constants_1.default.Error}'] = $_.exception.Message + } + return ConvertTo-Json $output`; + core.debug(`GetLatestModuleScript: ${this.script}`); + return this.script; + } + checkModuleVersionScript(moduleName, version) { + const command = `Get-Module -Name ${moduleName} -ListAvailable | Where-Object Version -match ${version}`; + this.script += `try { + $ErrorActionPreference = "Stop" + $WarningPreference = "SilentlyContinue" + $output = @{} + $data = ${command} + $output['${Constants_1.default.versionExists}'] = [string]::IsNullOrEmpty($data) + $output['${Constants_1.default.Success}'] = "true" + } + catch { + $output['${Constants_1.default.Error}'] = $_.exception.Message + } + return ConvertTo-Json $output`; + core.debug(`CheckModuleVersionScript: ${this.script}`); + return this.script; + } + getInlineScriptFile(inlineScript, errorActionPreference) { + this.script = `$ErrorActionPreference = '${errorActionPreference}'${os.EOL}${inlineScript}`; + core.debug(`InlineScript file to be executed: ${this.script}`); + return this.script; + } +} +exports.default = ScriptBuilder; diff --git a/lib/Utilities/Utils.js b/lib/Utilities/Utils.js new file mode 100644 index 00000000..48b9ee2f --- /dev/null +++ b/lib/Utilities/Utils.js @@ -0,0 +1,106 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const os = __importStar(require("os")); +const Constants_1 = __importDefault(require("../Constants")); +const PowerShellToolRunner_1 = __importDefault(require("../Utilities/PowerShellToolRunner")); +const ScriptBuilder_1 = __importDefault(require("./ScriptBuilder")); +class Utils { + /** + * Add the folder path where Az modules are present to PSModulePath based on runner + * @param azPSVersion + * If azPSVersion is empty, folder path in which all Az modules are present are set + * If azPSVersion is not empty, folder path of exact Az module version is set + */ + static setPSModulePath(azPSVersion = "") { + let modulePath = ""; + const runner = process.env.RUNNER_OS || os.type(); + switch (runner.toLowerCase()) { + case "linux": + modulePath = `/usr/share/${azPSVersion}:`; + break; + case "windows": + case "windows_nt": + modulePath = `C:\\Modules\\${azPSVersion};`; + break; + case "macos": + case "darwin": + throw new Error(`OS not supported`); + default: + throw new Error(`Unknown os: ${runner.toLowerCase()}`); + } + process.env.PSModulePath = `${modulePath}${process.env.PSModulePath}`; + } + static getLatestModule(moduleName) { + return __awaiter(this, void 0, void 0, function* () { + let output = ""; + const options = { + listeners: { + stdout: (data) => { + output += data.toString(); + } + } + }; + yield PowerShellToolRunner_1.default.init(); + yield PowerShellToolRunner_1.default.executePowerShellScriptBlock(new ScriptBuilder_1.default() + .getLatestModuleScript(moduleName), options); + const outputJson = JSON.parse(output.trim()); + if (!(Constants_1.default.Success in outputJson)) { + throw new Error(outputJson[Constants_1.default.Error]); + } + const azLatestVersion = outputJson[Constants_1.default.AzVersion]; + if (!Utils.isValidVersion(azLatestVersion)) { + throw new Error(`Invalid AzPSVersion: ${azLatestVersion}`); + } + return azLatestVersion; + }); + } + static checkModuleVersion(moduleName, version) { + return __awaiter(this, void 0, void 0, function* () { + let output = ""; + const options = { + listeners: { + stdout: (data) => { + output += data.toString(); + } + } + }; + if (!Utils.isValidVersion(output.trim())) { + return ""; + } + yield PowerShellToolRunner_1.default.init(); + yield PowerShellToolRunner_1.default.executePowerShellCommand(new ScriptBuilder_1.default() + .checkModuleVersionScript(moduleName, version), options); + const outputJson = JSON.parse(output.trim()); + if (!(Constants_1.default.Success in outputJson)) { + throw new Error(outputJson[Constants_1.default.Error]); + } + const versionExists = outputJson[Constants_1.default.versionExists].toLowerCase() === "true"; + if (!(versionExists)) { + throw new Error("Invalid azPSVersion. Refer https://aka.ms/azure-powershell-release-notes for supported versions."); + } + }); + } + static isValidVersion(version) { + return !!version.match(Constants_1.default.versionPattern); + } +} +exports.default = Utils; diff --git a/lib/main.js b/lib/main.js new file mode 100644 index 00000000..aeaafe38 --- /dev/null +++ b/lib/main.js @@ -0,0 +1,71 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const Utils_1 = __importDefault(require("./Utilities/Utils")); +const FileUtils_1 = __importDefault(require("./Utilities/FileUtils")); +const ScriptRunner_1 = __importDefault(require("./ScriptRunner")); +const InitializeAzure_1 = __importDefault(require("./InitializeAzure")); +const errorActionPrefValues = new Set(['STOP', 'CONTINUE', 'SILENTLYCONTINUE']); +let azPSVersion; +function main() { + return __awaiter(this, void 0, void 0, function* () { + try { + const inlineScript = core.getInput('inlineScript', { required: true }); + azPSVersion = core.getInput('azPSVersion', { required: true }).trim().toLowerCase(); + const errorActionPreference = core.getInput('errorActionPreference'); + const failOnStandardError = core.getInput('failOnStandardError').trim().toLowerCase() === "true"; + console.log(`Validating inputs`); + validateInputs(inlineScript, errorActionPreference); + console.log(`Initializing Az Module`); + yield InitializeAzure_1.default.importAzModule(azPSVersion); + console.log(`Initializing Az Module Complete`); + console.log(`Running Az PowerShell Script`); + const scriptRunner = new ScriptRunner_1.default(inlineScript, errorActionPreference, failOnStandardError); + yield scriptRunner.executeFile(); + console.log(`Script execution Complete`); + } + catch (error) { + core.setFailed(error); + } + finally { + FileUtils_1.default.deleteFile(ScriptRunner_1.default.filePath); + } + }); +} +function validateInputs(inlineScript, errorActionPreference) { + if (!inlineScript.trim()) { + throw new Error(`inlineScript is empty. Please enter a valid script.`); + } + if (azPSVersion !== "latest") { + if (!Utils_1.default.isValidVersion(azPSVersion)) { + console.log(`Invalid azPSVersion : ${azPSVersion}. Using latest Az Module version.`); + azPSVersion = 'latest'; + } + } + validateErrorActionPref(errorActionPreference); +} +function validateErrorActionPref(errorActionPreference) { + if (!(errorActionPrefValues.has(errorActionPreference.toUpperCase()))) { + throw new Error(`Invalid errorActionPreference: ${errorActionPreference}`); + } +} +main(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..89cb4126 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,93 @@ +{ + "name": "powershell", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@actions/core": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.3.tgz", + "integrity": "sha512-Wp4xnyokakM45Uuj4WLUxdsa8fJjKVl1fDTsPbTEcTcuu0Nb26IPQbOtjmnfaCPGcaoPOOqId8H9NapZ8gii4w==" + }, + "@actions/exec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.3.tgz", + "integrity": "sha512-TogJGnueOmM7ntCi0ASTUj4LapRRtDfj57Ja4IhPmg2fls28uVOPbAn8N+JifaOumN2UG3oEO/Ixek2A4NcYSA==", + "requires": { + "@actions/io": "^1.0.1" + } + }, + "@actions/io": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz", + "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg==" + }, + "@types/node": { + "version": "13.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.3.tgz", + "integrity": "sha512-01s+ac4qerwd6RHD+mVbOEsraDHSgUaefQlEdBbUolnQFjKwCr7luvAlEwW1RFojh67u0z4OUTjPn9LEl4zIkA==", + "dev": true + }, + "emitter-component": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.1.tgz", + "integrity": "sha1-Bl4tvtaVm/RwZ57avq95gdEAOrY=" + }, + "fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "os": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/os/-/os-0.1.1.tgz", + "integrity": "sha1-IIhF6J4ZOtTZcUdLk5R3NqVtE/M=" + }, + "path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=", + "requires": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "stream": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", + "integrity": "sha1-f1Nj8Ff2WSxVlfALyAon9c7B8O8=", + "requires": { + "emitter-component": "^1.1.1" + } + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "dev": true + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "requires": { + "inherits": "2.0.3" + } + }, + "uuid": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.2.tgz", + "integrity": "sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..4715ae02 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "powershell", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "tsc", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/Azure/powershell.git" + }, + "keywords": [], + "author": "Akshaya", + "license": "MIT", + "bugs": { + "url": "https://github.com/Azure/powershell/issues" + }, + "homepage": "https://github.com/Azure/powershell#readme", + "dependencies": { + "@actions/core": "^1.2.2", + "@actions/exec": "^1.0.3", + "@actions/io": "^1.0.2", + "fs": "0.0.1-security", + "os": "^0.1.1", + "path": "^0.12.7", + "stream": "0.0.2", + "uuid": "^7.0.2" + }, + "devDependencies": { + "@types/node": "^13.7.7", + "typescript": "^3.8.3" + } +} diff --git a/src/Constants.ts b/src/Constants.ts new file mode 100644 index 00000000..1f0a131d --- /dev/null +++ b/src/Constants.ts @@ -0,0 +1,11 @@ +export default class Constants { + static readonly prefix: string = "az_"; + static readonly moduleName: string = "Az"; + + static readonly versionPattern = /[0-9]\.[0-9]\.[0-9]/; + + static readonly Success: string = "Success"; + static readonly Error: string = "Error"; + static readonly AzVersion: string = "AzVersion"; + static readonly versionExists: string = "versionExists"; +} \ No newline at end of file diff --git a/src/InitializeAzure.ts b/src/InitializeAzure.ts new file mode 100644 index 00000000..ec868478 --- /dev/null +++ b/src/InitializeAzure.ts @@ -0,0 +1,17 @@ +import * as core from '@actions/core'; + +import Utils from "./Utilities/Utils"; +import Constants from "./Constants"; + +export default class InitializeAzure { + static async importAzModule(azPSVersion: string) { + Utils.setPSModulePath(); + if (azPSVersion === "latest") { + azPSVersion = await Utils.getLatestModule(Constants.moduleName); + } else { + await Utils.checkModuleVersion(Constants.moduleName, azPSVersion); + } + core.debug(`Az Module version used: ${azPSVersion}`); + Utils.setPSModulePath(`${Constants.prefix}${azPSVersion}`); + } +} \ No newline at end of file diff --git a/src/ScriptRunner.ts b/src/ScriptRunner.ts new file mode 100644 index 00000000..189d71e1 --- /dev/null +++ b/src/ScriptRunner.ts @@ -0,0 +1,53 @@ +import * as core from '@actions/core'; + +import FileUtils from "./Utilities/FileUtils"; +import PowerShellToolRunner from "./Utilities/PowerShellToolRunner"; +import ScriptBuilder from './Utilities/ScriptBuilder'; + +export default class ScriptRunner { + static filePath: string; + inlineScript: string; + errorActionPreference: string; + failOnStandardErr: boolean; + + constructor(inlineScript: string, errorActionPreference: string, failOnStandardErr:boolean) { + this.inlineScript = inlineScript; + this.errorActionPreference = errorActionPreference; + this.failOnStandardErr = failOnStandardErr; + } + + async executeFile() { + const error: string[] = []; + const options: any = { + listeners: { + stderr: (data: Buffer) => { + if (error.length < 10) { + // Truncate to at most 1000 bytes + if (data.length > 1000) { + error.push(`${data.toString('utf8', 0, 1000)}`); + } else { + error.push(data.toString('utf8')); + } + } else if (error.length === 10) { + error.push('Additional writes to stderr truncated'); + } + } + } + }; + const scriptToExecute: string = new ScriptBuilder().getInlineScriptFile( + this.inlineScript, this.errorActionPreference); + ScriptRunner.filePath = await FileUtils.createScriptFile(scriptToExecute); + core.debug(`script file to run: ${ScriptRunner.filePath}`); + await PowerShellToolRunner.init(); + const exitCode: number = await PowerShellToolRunner.executePowerShellScriptBlock(ScriptRunner.filePath, options); + if (exitCode !== 0) { + core.setOutput(`Azure PowerShell exited with code:`, exitCode.toString()); + if (this.failOnStandardErr) { + error.forEach((err: string) => { + core.error(err); + }); + throw new Error(`Standard error stream contains one or more lines`); + } + } + } +} \ No newline at end of file diff --git a/src/Utilities/FileUtils.ts b/src/Utilities/FileUtils.ts new file mode 100644 index 00000000..80f25e05 --- /dev/null +++ b/src/Utilities/FileUtils.ts @@ -0,0 +1,31 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as core from '@actions/core'; +import { v4 as uuidv4 } from 'uuid'; + +export default class FileUtils { + static readonly tempDirectory: string = process.env.RUNNER_TEMP || os.tmpdir(); + + static async createScriptFile(inlineScript: string): Promise { + const fileName: string = FileUtils.getFileName(); + const filePath: string = path.join(FileUtils.tempDirectory, fileName); + fs.writeFileSync(filePath, inlineScript, 'utf-8'); + return filePath; + } + + private static getFileName(): string { + return `${uuidv4()}.ps1`; + } + + static async deleteFile(filePath: string) { + if (fs.existsSync(filePath)) { + try { + fs.unlinkSync(filePath); + } + catch (err) { + core.warning(err.toString()); + } + } + } +} \ No newline at end of file diff --git a/src/Utilities/PowerShellToolRunner.ts b/src/Utilities/PowerShellToolRunner.ts new file mode 100644 index 00000000..20e11cf1 --- /dev/null +++ b/src/Utilities/PowerShellToolRunner.ts @@ -0,0 +1,22 @@ +import * as io from '@actions/io'; +import * as exec from '@actions/exec'; + +export default class PowerShellToolRunner { + static psPath: string; + + static async init() { + if(!PowerShellToolRunner.psPath) { + PowerShellToolRunner.psPath = await io.which("pwsh", true); + } + } + + static async executePowerShellCommand(command: string, options: any = {}) { + await exec.exec(`${PowerShellToolRunner.psPath} -NoLogo -NoProfile -NonInteractive -Command ${command}`, [], options); + } + + static async executePowerShellScriptBlock(scriptBlock: string, options: any = {}): Promise { + const exitCode: number = await exec.exec(`${PowerShellToolRunner.psPath} -NoLogo -NoProfile -NonInteractive -Command`, + [scriptBlock], options); + return exitCode; + } +} \ No newline at end of file diff --git a/src/Utilities/ScriptBuilder.ts b/src/Utilities/ScriptBuilder.ts new file mode 100644 index 00000000..9ffbfe57 --- /dev/null +++ b/src/Utilities/ScriptBuilder.ts @@ -0,0 +1,50 @@ +import * as os from 'os'; +import * as core from '@actions/core'; + +import Constants from "../Constants"; + +export default class ScriptBuilder { + script: string = ""; + + getLatestModuleScript(moduleName: string): string { + const command: string = `Get-Module -Name ${moduleName} -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1`; + this.script += `try { + $ErrorActionPreference = "Stop" + $WarningPreference = "SilentlyContinue" + $output = @{} + $data = ${command} + $output['${Constants.AzVersion}'] = $data.Version.ToString() + $output['${Constants.Success}'] = "true" + } + catch { + $output['${Constants.Error}'] = $_.exception.Message + } + return ConvertTo-Json $output`; + core.debug(`GetLatestModuleScript: ${this.script}`); + return this.script; + } + + checkModuleVersionScript(moduleName: string, version: string) { + const command: string = `Get-Module -Name ${moduleName} -ListAvailable | Where-Object Version -match ${version}`; + this.script += `try { + $ErrorActionPreference = "Stop" + $WarningPreference = "SilentlyContinue" + $output = @{} + $data = ${command} + $output['${Constants.versionExists}'] = [string]::IsNullOrEmpty($data) + $output['${Constants.Success}'] = "true" + } + catch { + $output['${Constants.Error}'] = $_.exception.Message + } + return ConvertTo-Json $output`; + core.debug(`CheckModuleVersionScript: ${this.script}`); + return this.script; + } + + getInlineScriptFile(inlineScript: string, errorActionPreference: string) { + this.script = `$ErrorActionPreference = '${errorActionPreference}'${os.EOL}${inlineScript}` + core.debug(`InlineScript file to be executed: ${this.script}`); + return this.script; + } +} diff --git a/src/Utilities/Utils.ts b/src/Utilities/Utils.ts new file mode 100644 index 00000000..6aaafccf --- /dev/null +++ b/src/Utilities/Utils.ts @@ -0,0 +1,87 @@ +import * as os from 'os'; + +import Constants from '../Constants'; +import PowerShellToolRunner from '../Utilities/PowerShellToolRunner'; +import ScriptBuilder from './ScriptBuilder'; + +export default class Utils { + /** + * Add the folder path where Az modules are present to PSModulePath based on runner + * @param azPSVersion + * If azPSVersion is empty, folder path in which all Az modules are present are set + * If azPSVersion is not empty, folder path of exact Az module version is set + */ + static setPSModulePath(azPSVersion: string = "") { + let modulePath: string = ""; + const runner: string = process.env.RUNNER_OS || os.type(); + switch (runner.toLowerCase()) { + case "linux": + modulePath = `/usr/share/${azPSVersion}:`; + break; + case "windows": + case "windows_nt": + modulePath = `C:\\Modules\\${azPSVersion};`; + break; + case "macos": + case "darwin": + throw new Error(`OS not supported`); + default: + throw new Error(`Unknown os: ${runner.toLowerCase()}`); + } + process.env.PSModulePath = `${modulePath}${process.env.PSModulePath}`; + } + + static async getLatestModule(moduleName: string): Promise { + let output: string = ""; + const options: any = { + listeners: { + stdout: (data: Buffer) => { + output += data.toString(); + } + } + }; + await PowerShellToolRunner.init(); + await PowerShellToolRunner.executePowerShellScriptBlock(new ScriptBuilder() + .getLatestModuleScript(moduleName), options); + const outputJson = JSON.parse(output.trim()); + if (!(Constants.Success in outputJson)) { + throw new Error(outputJson[Constants.Error]); + } + const azLatestVersion: string = outputJson[Constants.AzVersion]; + if (!Utils.isValidVersion(azLatestVersion)) { + throw new Error(`Invalid AzPSVersion: ${azLatestVersion}`); + } + return azLatestVersion; + } + + static async checkModuleVersion(moduleName: string, version: string) { + let output: string = ""; + const options: any = { + listeners: { + stdout: (data: Buffer) => { + output += data.toString(); + } + } + }; + if (!Utils.isValidVersion(output.trim())) { + return ""; + } + await PowerShellToolRunner.init(); + await PowerShellToolRunner.executePowerShellCommand(new ScriptBuilder() + .checkModuleVersionScript(moduleName, version), options); + const outputJson = JSON.parse(output.trim()); + if (!(Constants.Success in outputJson)) { + throw new Error(outputJson[Constants.Error]); + } + const versionExists: boolean = outputJson[Constants.versionExists].toLowerCase() === "true"; + if(!(versionExists)) { + throw new Error("Invalid azPSVersion. Refer https://aka.ms/azure-powershell-release-notes for supported versions."); + } + } + + static isValidVersion(version: string): boolean { + return !!version.match(Constants.versionPattern); + } + +} + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 00000000..8f58f1e2 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,52 @@ +import * as core from '@actions/core'; +import Utils from './Utilities/Utils'; +import FileUtils from './Utilities/FileUtils'; +import ScriptRunner from './ScriptRunner'; +import InitializeAzure from './InitializeAzure'; + +const errorActionPrefValues = new Set(['STOP', 'CONTINUE', 'SILENTLYCONTINUE']); +let azPSVersion: string; +async function main() { + try { + const inlineScript: string = core.getInput('inlineScript', { required: true }); + azPSVersion = core.getInput('azPSVersion', { required: true }).trim().toLowerCase(); + const errorActionPreference: string = core.getInput('errorActionPreference'); + const failOnStandardError = core.getInput('failOnStandardError').trim().toLowerCase() === "true"; + console.log(`Validating inputs`); + validateInputs(inlineScript, errorActionPreference); + + console.log(`Initializing Az Module`); + await InitializeAzure.importAzModule(azPSVersion); + console.log(`Initializing Az Module Complete`); + + console.log(`Running Az PowerShell Script`); + const scriptRunner: ScriptRunner = new ScriptRunner(inlineScript, errorActionPreference, failOnStandardError); + await scriptRunner.executeFile(); + console.log(`Script execution Complete`); + } catch(error) { + core.setFailed(error); + } finally { + FileUtils.deleteFile(ScriptRunner.filePath); + } +} + +function validateInputs(inlineScript: string, errorActionPreference: string) { + if (!inlineScript.trim()) { + throw new Error(`inlineScript is empty. Please enter a valid script.`); + } + if (azPSVersion !== "latest") { + if (!Utils.isValidVersion(azPSVersion)) { + console.log(`Invalid azPSVersion : ${azPSVersion}. Using latest Az Module version.`); + azPSVersion = 'latest'; + } + } + validateErrorActionPref(errorActionPreference); +} + +function validateErrorActionPref(errorActionPreference: string) { + if(!(errorActionPrefValues.has(errorActionPreference.toUpperCase()))) { + throw new Error(`Invalid errorActionPreference: ${errorActionPreference}`); + } +} + +main() \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..34744726 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,68 @@ +{ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./lib", /* Redirect output structure to the directory. */ + "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": false, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + /* "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ + /* "paths": { */ + /* "src/*":["src/*"], + "src/Utilities/*": [ "src/Utilities/*" ] */ + /* },*/ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + } + } + \ No newline at end of file