diff --git a/.github/workflows/fail_on_detected_diff.yml b/.github/workflows/fail_on_detected_diff.yml new file mode 100644 index 0000000..e640575 --- /dev/null +++ b/.github/workflows/fail_on_detected_diff.yml @@ -0,0 +1,47 @@ +name: "build-test" + +on: # rebuild any PRs and main branch changes + pull_request: + push: + branches: + - main + - develop + - fail_on_detected_diff +jobs: + + exitcode: + name: 'Terraform Run Local Exitcode' + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./.github/workflows/data/local + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Terraform + uses: ./ + with: + fail_on_detected_diff: true + + - name: Terraform Init + shell: bash + run: terraform init + +# - name: Terraform Format +# shell: bash +# run: terraform fmt -check +# + - name: Terraform Plan + id: plan + shell: bash + run: terraform plan + + - name: Terraform Plan (fail) + id: plan_fail + shell: bash + run: terraform plan -detailed-exitcode +# +# - name: Print Terraform Plan +# shell: bash +# run: echo "${{ steps.plan.outputs.stdout }}" diff --git a/action.yml b/action.yml index 9009be1..801ab42 100644 --- a/action.yml +++ b/action.yml @@ -17,6 +17,10 @@ inputs: description: 'Whether or not to install a wrapper to wrap subsequent calls of the `terraform` binary and expose its STDOUT, STDERR, and exit code as outputs named `stdout`, `stderr`, and `exitcode` respectively. Defaults to `true`.' default: 'true' required: false + fail_on_detected_diff: + description: 'Using -detailed-exitcode option returns exit code 0 or 2 for a detected change. Normally these are accepted as success. If doing specific handling (failing on drift, for example), this option will fail when exit code 2 is returned. Defaults to `pass`. Accepts `fail` in any case.' + default: 'pass' + required: false runs: using: 'node20' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 04b7748..50e7b17 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3424,17 +3424,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }; var _a; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getCmdPath = exports.tryGetExecutablePath = exports.isRooted = exports.isDirectory = exports.exists = exports.READONLY = exports.UV_FS_O_EXLOCK = exports.IS_WINDOWS = exports.unlink = exports.symlink = exports.stat = exports.rmdir = exports.rm = exports.rename = exports.readlink = exports.readdir = exports.open = exports.mkdir = exports.lstat = exports.copyFile = exports.chmod = void 0; +exports.getCmdPath = exports.tryGetExecutablePath = exports.isRooted = exports.isDirectory = exports.exists = exports.IS_WINDOWS = exports.unlink = exports.symlink = exports.stat = exports.rmdir = exports.rename = exports.readlink = exports.readdir = exports.mkdir = exports.lstat = exports.copyFile = exports.chmod = void 0; const fs = __importStar(__nccwpck_require__(7147)); const path = __importStar(__nccwpck_require__(1017)); -_a = fs.promises -// export const {open} = 'fs' -, exports.chmod = _a.chmod, exports.copyFile = _a.copyFile, exports.lstat = _a.lstat, exports.mkdir = _a.mkdir, exports.open = _a.open, exports.readdir = _a.readdir, exports.readlink = _a.readlink, exports.rename = _a.rename, exports.rm = _a.rm, exports.rmdir = _a.rmdir, exports.stat = _a.stat, exports.symlink = _a.symlink, exports.unlink = _a.unlink; -// export const {open} = 'fs' +_a = fs.promises, exports.chmod = _a.chmod, exports.copyFile = _a.copyFile, exports.lstat = _a.lstat, exports.mkdir = _a.mkdir, exports.readdir = _a.readdir, exports.readlink = _a.readlink, exports.rename = _a.rename, exports.rmdir = _a.rmdir, exports.stat = _a.stat, exports.symlink = _a.symlink, exports.unlink = _a.unlink; exports.IS_WINDOWS = process.platform === 'win32'; -// See https://github.com/nodejs/node/blob/d0153aee367422d0858105abec186da4dff0a0c5/deps/uv/include/uv/win.h#L691 -exports.UV_FS_O_EXLOCK = 0x10000000; -exports.READONLY = fs.constants.O_RDONLY; function exists(fsPath) { return __awaiter(this, void 0, void 0, function* () { try { @@ -3615,8 +3609,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge Object.defineProperty(exports, "__esModule", ({ value: true })); exports.findInPath = exports.which = exports.mkdirP = exports.rmRF = exports.mv = exports.cp = void 0; const assert_1 = __nccwpck_require__(9491); +const childProcess = __importStar(__nccwpck_require__(2081)); const path = __importStar(__nccwpck_require__(1017)); +const util_1 = __nccwpck_require__(3837); const ioUtil = __importStar(__nccwpck_require__(1962)); +const exec = util_1.promisify(childProcess.exec); +const execFile = util_1.promisify(childProcess.execFile); /** * Copies a file or folder. * Based off of shelljs - https://github.com/shelljs/shelljs/blob/9237f66c52e5daa40458f94f9565e18e8132f5a6/src/cp.js @@ -3697,23 +3695,61 @@ exports.mv = mv; function rmRF(inputPath) { return __awaiter(this, void 0, void 0, function* () { if (ioUtil.IS_WINDOWS) { + // Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another + // program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del. // Check for invalid characters // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file if (/[*"<>|]/.test(inputPath)) { throw new Error('File path must not contain `*`, `"`, `<`, `>` or `|` on Windows'); } + try { + const cmdPath = ioUtil.getCmdPath(); + if (yield ioUtil.isDirectory(inputPath, true)) { + yield exec(`${cmdPath} /s /c "rd /s /q "%inputPath%""`, { + env: { inputPath } + }); + } + else { + yield exec(`${cmdPath} /s /c "del /f /a "%inputPath%""`, { + env: { inputPath } + }); + } + } + catch (err) { + // if you try to delete a file that doesn't exist, desired result is achieved + // other errors are valid + if (err.code !== 'ENOENT') + throw err; + } + // Shelling out fails to remove a symlink folder with missing source, this unlink catches that + try { + yield ioUtil.unlink(inputPath); + } + catch (err) { + // if you try to delete a file that doesn't exist, desired result is achieved + // other errors are valid + if (err.code !== 'ENOENT') + throw err; + } } - try { - // note if path does not exist, error is silent - yield ioUtil.rm(inputPath, { - force: true, - maxRetries: 3, - recursive: true, - retryDelay: 300 - }); - } - catch (err) { - throw new Error(`File was unable to be removed ${err}`); + else { + let isDir = false; + try { + isDir = yield ioUtil.isDirectory(inputPath); + } + catch (err) { + // if you try to delete a file that doesn't exist, desired result is achieved + // other errors are valid + if (err.code !== 'ENOENT') + throw err; + return; + } + if (isDir) { + yield execFile(`rm`, [`-rf`, `${inputPath}`]); + } + else { + yield ioUtil.unlink(inputPath); + } } }); } diff --git a/dist/index1.js b/dist/index1.js index 92c1a41..4eaba04 100755 --- a/dist/index1.js +++ b/dist/index1.js @@ -250,6 +250,10 @@ function getBooleanInput(name, options) { const trueValue = ['true', 'True', 'TRUE']; const falseValue = ['false', 'False', 'FALSE']; const val = getInput(name, options); + info(`getBooleanInput: ${name}:${val}:::`) + process.stdout.write(`getBooleanInput: ${name}:${val}:::`) + process.stdout.write(os.EOL); + if (trueValue.includes(val)) return true; if (falseValue.includes(val)) @@ -27746,24 +27750,44 @@ async function checkTerraform () { ignoreReturnCode: true, silent: true // avoid printing command in stdout: https://github.com/actions/toolkit/issues/649 }; + + // TODO: remove + core.setCommandEcho(true); + const exitCode = await exec(pathToCLI, args, options); // Pass-through stdout/err as `exec` won't due to `silent: true` option process.stdout.write(stdout.contents); process.stderr.write(stderr.contents); + const fail_on_detected_diff2 = core.getInput('fail_on_detected_diff') + core.debug(`fail_on_detected_diff2: ${fail_on_detected_diff2}`); + // Set outputs, result, exitcode, and stderr core.setOutput('stdout', stdout.contents); core.setOutput('stderr', stderr.contents); core.setOutput('exitcode', exitCode.toString(10)); - if (exitCode === 0 || exitCode === 2) { - // A exitCode of 0 is considered a success - // An exitCode of 2 may be returned when the '-detailed-exitcode' option - // is passed to plan. This denotes Success with non-empty - // diff (changes present). + // A exitCode of 0 is considered a success + if (exitCode === 0) { + core.info('Terraform completed successfully. (0)'); return; } + // An exitCode of 2 may be returned when the '-detailed-exitcode' option + // is passed to plan. This denotes Success with non-empty diff (changes present). + // The user may want to capture this and fail the job, so will set `fail_on_detected_diff: true` + if (exitCode === 2) { + const is_wrapper = core.getInput('terraform_wrapper') + const terraform_version = core.getInput('terraform_version'); + const failOnDetectedDiffString = core.getInput('fail_on_detected_diff'); + const failOnDetectedDiff = (failOnDetectedDiffString.toLowerCase() === 'true'); + core.debug(`Terraform detected a difference. (2) failOnDetectedDiffString=${failOnDetectedDiffString} failOnDetectedDiff=${failOnDetectedDiff}`); + core.info(`Terraform detected a difference. 4 (2) is_wrapper=${is_wrapper} terraform_version=${terraform_version} failOnDetectedDiffString=${failOnDetectedDiffString} failOnDetectedDiff=${failOnDetectedDiff}`); + if (!failOnDetectedDiff) { + core.info('Terraform difference ignored.'); + return; + } + } // A non-zero exitCode is considered an error core.setFailed(`Terraform exited with code ${exitCode}.`); @@ -27773,4 +27797,4 @@ async function checkTerraform () { module.exports = __webpack_exports__; /******/ })() -; \ No newline at end of file +; diff --git a/wrapper/terraform.js b/wrapper/terraform.js index 20fec38..c5e8070 100755 --- a/wrapper/terraform.js +++ b/wrapper/terraform.js @@ -10,6 +10,8 @@ const { exec } = require('@actions/exec'); const OutputListener = require('./lib/output-listener'); const pathToCLI = require('./lib/terraform-bin'); +const os = require("os"); +const releases = require("@hashicorp/js-releases"); async function checkTerraform () { // Setting check to `true` will cause `which` to throw if terraform isn't found @@ -36,24 +38,51 @@ async function checkTerraform () { ignoreReturnCode: true, silent: true // avoid printing command in stdout: https://github.com/actions/toolkit/issues/649 }; + + // TODO: remove + core.setCommandEcho(true); + const exitCode = await exec(pathToCLI, args, options); // Pass-through stdout/err as `exec` won't due to `silent: true` option process.stdout.write(stdout.contents); process.stderr.write(stderr.contents); + const fail_on_detected_diff2 = core.getInput('fail_on_detected_diff') + core.debug(`fail_on_detected_diff2: ${fail_on_detected_diff2}`); + + // Gather GitHub Actions inputs + const version = core.getInput('terraform_version'); + core.debug(`TEST d Terraform version ${version}`); + core.info(`TEST i Terraform version ${version}`); + // Set outputs, result, exitcode, and stderr core.setOutput('stdout', stdout.contents); core.setOutput('stderr', stderr.contents); core.setOutput('exitcode', exitCode.toString(10)); - if (exitCode === 0 || exitCode === 2) { - // A exitCode of 0 is considered a success - // An exitCode of 2 may be returned when the '-detailed-exitcode' option - // is passed to plan. This denotes Success with non-empty - // diff (changes present). + // A exitCode of 0 is considered a success + if (exitCode === 0) { + core.info('Terraform completed successfully. (0)'); return; } + // An exitCode of 2 may be returned when the '-detailed-exitcode' option + // is passed to plan. This denotes Success with non-empty diff (changes present). + // The user may want to capture this and fail the job, so will set `fail_on_detected_diff: true` + if (exitCode === 2) { + const is_wrapper = core.getInput('terraform_wrapper') + const terraform_version = core.getInput('terraform_version'); + const failOnDetectedDiffString = core.getInput('fail_on_detected_diff'); + // const failOnDetectedDiff = core.getBooleanInput('fail_on_detected_diff'); +//# getInput('my-input').toUpper() === 'true' + const failOnDetectedDiff = (failOnDetectedDiffString.toLowerCase() === 'fail'); + core.debug(`Terraform detected a difference. (2) failOnDetectedDiffString=${failOnDetectedDiffString} failOnDetectedDiff=${failOnDetectedDiff}`); + core.info(`Terraform detected a difference. 4 (2) is_wrapper=${is_wrapper} terraform_version=${terraform_version} failOnDetectedDiffString=${failOnDetectedDiffString} failOnDetectedDiff=${failOnDetectedDiff}`); + if (!failOnDetectedDiff) { + core.info('Terraform difference ignored.'); + return; + } + } // A non-zero exitCode is considered an error core.setFailed(`Terraform exited with code ${exitCode}.`);