From e64093d6911a8fe9e97e7dd411c7cacc903e48b5 Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Tue, 3 Aug 2021 18:00:57 +0200 Subject: [PATCH 01/46] Add the op-cli in the script By adding the `op-cli` in the script, we no longer need to look for environment variables that have a reference and fetch the values of the secrets through shell script. Instead, we use the commands `op list envars` and `op read` (alpha version) --- entrypoint.sh | 84 ++++----------------------------------------------- 1 file changed, 6 insertions(+), 78 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index b9a6d9a..4e8c691 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,6 +2,10 @@ # shellcheck disable=SC2046,SC2001,SC2086 set -e +# Install op-cli +$(curl -sSfLo op.zip "https://drive.google.com/uc?export=download&id=1HRAsihTN0Cx0pWZEWN06jAWxo0eW5eG-") +unzip -od /usr/local/bin/ op.zip && rm op.zip + if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then echo "\$OP_CONNECT_TOKEN and \$OP_CONNECT_HOST must be set" exit 1 @@ -27,90 +31,14 @@ if [ "$INPUT_UNSET_PREVIOUS" == "true" ]; then managed_variables=() fi -curl_headers=(-H "Content-Type: application/json" -H "Authorization: Bearer $OP_CONNECT_TOKEN") - # Iterate over environment varables to find 1Password references, load the secret values, # and make them available as environment variables in the next steps. IFS=$'\n' -for possible_ref in $(printenv | grep "=op://" | grep -v "^#"); do - env_var=$(echo "$possible_ref" | cut -d '=' -f1) +for env_var in $(op list envars); do ref=$(printenv $env_var) - if [[ ! $ref == "op://"* ]]; then - echo "Not really a reference: $ref" - continue - fi - - path=$(echo $ref | sed -e "s/^op:\/\///") - if [ $(echo "$path" | tr -cd '/' | wc -c) -lt 2 ]; then - echo "Expected path to be in format op:///[/
]/: $ref" - continue - fi - echo "Populating variable: $env_var" - - vault="" - item="" - section="" - field="" - i=0 - IFS="/" - for component in $path; do - ((i+=1)) - case "$i" in - 1) vault=$component ;; - 2) item=$component ;; - 3) section=$component ;; - 4) field=$component ;; - esac - done - unset IFS - - # If field is not set, it may have wrongfully been interpreted as the section. - if [ -z "$field" ]; then - field="$section" - section="" - fi - - if [[ $(echo -n $(echo $vault | grep "^[a-z0-9]*$") | wc -c) -ne 26 ]]; then - echo "Getting vault ID from vault name: $vault" - vault=$(curl -sSf "${curl_headers[@]}" "$OP_CONNECT_HOST/v1/vaults?filter=name%20eq%20%22$vault%22" | jq -r '.[0] | .id') - if [ -z "$vault" ]; then - echo "Could not find vault ID for vault: $vault" - exit 1 - fi - fi - - if [[ $(echo -n $(echo $item | grep "^[a-z0-9]*$") | wc -c) -ne 26 ]]; then - echo "Getting item ID from vault $vault..." - item=$(curl -sSf "${curl_headers[@]}" "$OP_CONNECT_HOST/v1/vaults/$vault/items?filter=title%20eq%20%22$item%22" | jq -r '.[0] | .id') - if [ -z "$item" ]; then - echo "Could not find item ID for item: $item" - exit 1 - fi - fi - - echo "Loading item $item from vault $vault..." - item_json=$(curl -sSf "${curl_headers[@]}" "$OP_CONNECT_HOST/v1/vaults/$vault/items/$item") - - jq_field_selector=".id == \"$field\" or .label == \"$field\"" - jq_section_selector=".section == null" - - # If the reference contains a section, edit the jq selector to take that into account. - if [ -n "$section" ]; then - echo "Looking for section: $section" - section_id=$(echo "$item_json" | jq -r ".sections[] | select(.id == \"$section\" or .label == \"$section\") | .id") - jq_section_selector=".section.id == \"$section_id\"" - fi - - jq_secret_selector="$jq_section_selector and ($jq_field_selector)" - - echo "Looking for field: $field" - secret_field_json=$(echo "$item_json" | jq -r "first(.fields[] | select($jq_secret_selector))") - - field_type=$(echo "$secret_field_json" | jq -r '.type') - field_purpose=$(echo "$secret_field_json" | jq -r '.purpose') - secret_value=$(echo "$secret_field_json" | jq -r '.value') + secret_value=$(op read $ref) if [ -z "$secret_value" ]; then echo "Could not find or access secret $ref" From 478705935c304cc5b9ae51bc946a5b43b2291925 Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Tue, 3 Aug 2021 19:17:32 +0200 Subject: [PATCH 02/46] Fix lint --- entrypoint.sh | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 4e8c691..39c5dac 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -45,21 +45,18 @@ for env_var in $(op list envars); do exit 1 fi - # If the field is marked as concealed or is a note, register a mask - # for the secret to prevent accidental log exposure. - if [ "$field_type" == "CONCEALED" ] || [ "$field_purpose" == "NOTES" ]; then - # To support multiline secrets, escape percent signs and add a mask per line. - escaped_mask_value=$(echo "$secret_value" | sed -e 's/%/%25/g') - IFS=$'\n' - for line in $escaped_mask_value; do - if [ "${#line}" -lt 3 ]; then - # To avoid false positives and unreadable logs, omit mask for lines that are too short. - continue - fi - echo "::add-mask::$line" - done - unset IFS - fi + # Register a mask for the secret to prevent accidental log exposure. + # To support multiline secrets, escape percent signs and add a mask per line. + escaped_mask_value=$(echo "$secret_value" | sed -e 's/%/%25/g') + IFS=$'\n' + for line in $escaped_mask_value; do + if [ "${#line}" -lt 3 ]; then + # To avoid false positives and unreadable logs, omit mask for lines that are too short. + continue + fi + echo "::add-mask::$line" + done + unset IFS # To support multiline secrets, we'll use the heredoc syntax to populate the environment variables. # As the heredoc identifier, we'll use a randomly generated 64-character string, From 08df44393f3ef41f1e58f7e08233e87bb9449eab Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Tue, 3 Aug 2021 19:24:26 +0200 Subject: [PATCH 03/46] Remove surrounding $() --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index 39c5dac..fe0de74 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -3,7 +3,7 @@ set -e # Install op-cli -$(curl -sSfLo op.zip "https://drive.google.com/uc?export=download&id=1HRAsihTN0Cx0pWZEWN06jAWxo0eW5eG-") +curl -sSfLo op.zip "https://drive.google.com/uc?export=download&id=1HRAsihTN0Cx0pWZEWN06jAWxo0eW5eG-" unzip -od /usr/local/bin/ op.zip && rm op.zip if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then From 5add51bcb8a12a351c3a3e67a1f8dbfa12022f10 Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Mon, 16 Aug 2021 17:17:20 +0200 Subject: [PATCH 04/46] Change to new command signature The command is changed from `op list envars` to `op env ls` --- entrypoint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index fe0de74..5987638 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -3,7 +3,7 @@ set -e # Install op-cli -curl -sSfLo op.zip "https://drive.google.com/uc?export=download&id=1HRAsihTN0Cx0pWZEWN06jAWxo0eW5eG-" +curl -sSfLo op.zip "https://drive.google.com/uc?export=download&id=1ih-kXa-5Jui4U-VETWbFqzfYUROB_Ukr" unzip -od /usr/local/bin/ op.zip && rm op.zip if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then @@ -34,7 +34,7 @@ fi # Iterate over environment varables to find 1Password references, load the secret values, # and make them available as environment variables in the next steps. IFS=$'\n' -for env_var in $(op list envars); do +for env_var in $(op env ls); do ref=$(printenv $env_var) echo "Populating variable: $env_var" From e8da10d005f0e92eaaab3d744dbef6a873d97877 Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Wed, 1 Sep 2021 16:20:47 +0200 Subject: [PATCH 05/46] Use op cli alpha v2 --- .github/workflows/test.yml | 20 +++----------------- entrypoint.sh | 6 +++++- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f48e122..e843d47 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,13 +20,9 @@ jobs: - name: Load secrets uses: ./ # 1password/load-secrets-action@ env: - SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password - SECRET_IN_SECTION: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/test-section/password - UNMASKED_VALUE: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/test-section/username - - name: Load multiline secret - uses: ./ # 1password/load-secrets-action@ - env: - MULTILINE_SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain + SECRET: op://acceptance-tests/test-secret/password + SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password + MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain - name: Print environment variables with masked secrets run: printenv - name: Assert test secret values @@ -39,13 +35,3 @@ jobs: run: printenv - name: Assert removed secrets run: ./tests/assert-env-unset.sh - - name: Load secrets by vault and item titles - uses: ./ # 1password/load-secrets-action@ - env: - SECRET: op://acceptance-tests/test-secret/password - SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password - MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain - - name: Print environment variables with masked secrets - run: printenv - - name: Assert test secret values again - run: ./tests/assert-env-set.sh diff --git a/entrypoint.sh b/entrypoint.sh index 5987638..4336bc3 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -3,7 +3,11 @@ set -e # Install op-cli -curl -sSfLo op.zip "https://drive.google.com/uc?export=download&id=1ih-kXa-5Jui4U-VETWbFqzfYUROB_Ukr" +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + curl -sSfLo op.zip "https://bucket.agilebits.com/cli-private-beta/v2/op_linux_amd64_v2-alpha2.zip" +elif [[ "$OSTYPE" == "darwin"* ]]; then + curl -sSfLo op.zip "https://bucket.agilebits.com/cli-private-beta/v2/op_darwin_amd64_v2-alpha2.zip" +fi unzip -od /usr/local/bin/ op.zip && rm op.zip if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then From ac12d2e3c462626d562c5fddffa99f11ad3a8cbd Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Wed, 1 Sep 2021 18:20:57 +0200 Subject: [PATCH 06/46] Switch to node action This is done to be able to pass loaded secrets as output. This is not available with a composite action, unless we hard-code the action's outputs, which is not a desired outcome. --- .github/workflows/test.yml | 34 +- .gitignore | 1 + action.yml | 8 +- dist/index.js | 1957 ++++++++++++++++++++++++++++++++++++ package-lock.json | 351 +++++++ package.json | 36 + src/index.ts | 24 + tsconfig.json | 13 + 8 files changed, 2415 insertions(+), 9 deletions(-) create mode 100644 .gitignore create mode 100644 dist/index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e843d47..d33be29 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,35 @@ on: push name: Run acceptance tests jobs: - test: + test-output-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Launch 1Password Connect instance + env: + OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }} + run: | + echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json + docker-compose -f tests/fixtures/docker-compose.yml up -d && sleep 10 + - name: Configure 1Password Connect + uses: ./configure # 1password/load-secrets-action/configure@ + with: + connect-host: http://localhost:8080 + connect-token: ${{ secrets.OP_CONNECT_TOKEN }} + - name: Load secrets + id: load_secrets + uses: ./ # 1password/load-secrets-action@ + env: + SECRET: op://acceptance-tests/test-secret/password + SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password + MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain + - name: Assert test secret values + env: + SECRET: ${{ steps.load_secrets.outputs.SECRET }} + SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} + MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} + run: ./tests/assert-env-set.sh + test-export-linux: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -19,6 +47,8 @@ jobs: connect-token: ${{ secrets.OP_CONNECT_TOKEN }} - name: Load secrets uses: ./ # 1password/load-secrets-action@ + with: + export-env: true env: SECRET: op://acceptance-tests/test-secret/password SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password @@ -31,7 +61,5 @@ jobs: uses: ./ # 1password/load-secrets-action@ with: unset-previous: true - - name: Print environment variables with secrets removed - run: printenv - name: Assert removed secrets run: ./tests/assert-env-unset.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/action.yml b/action.yml index 0177386..1631070 100644 --- a/action.yml +++ b/action.yml @@ -9,9 +9,5 @@ inputs: description: Whether to unset environment variables populated by 1Password in earlier job steps default: false runs: - using: composite - steps: - - run: | - export INPUT_UNSET_PREVIOUS=${{ inputs.unset-previous }} - ${{ github.action_path }}/entrypoint.sh - shell: bash + using: 'node12' + main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..e3d7f39 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,1957 @@ +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ({ + +/***/ 351: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.issue = exports.issueCommand = void 0; +const os = __importStar(__nccwpck_require__(87)); +const utils_1 = __nccwpck_require__(278); +/** + * Commands + * + * Command Format: + * ::name key=value,key=value::message + * + * Examples: + * ::warning::This is the message + * ::set-env name=MY_VAR::some value + */ +function issueCommand(command, properties, message) { + const cmd = new Command(command, properties, message); + process.stdout.write(cmd.toString() + os.EOL); +} +exports.issueCommand = issueCommand; +function issue(name, message = '') { + issueCommand(name, {}, message); +} +exports.issue = issue; +const CMD_STRING = '::'; +class Command { + constructor(command, properties, message) { + if (!command) { + command = 'missing.command'; + } + this.command = command; + this.properties = properties; + this.message = message; + } + toString() { + let cmdStr = CMD_STRING + this.command; + if (this.properties && Object.keys(this.properties).length > 0) { + cmdStr += ' '; + let first = true; + for (const key in this.properties) { + if (this.properties.hasOwnProperty(key)) { + const val = this.properties[key]; + if (val) { + if (first) { + first = false; + } + else { + cmdStr += ','; + } + cmdStr += `${key}=${escapeProperty(val)}`; + } + } + } + } + cmdStr += `${CMD_STRING}${escapeData(this.message)}`; + return cmdStr; + } +} +function escapeData(s) { + return utils_1.toCommandValue(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A'); +} +function escapeProperty(s) { + return utils_1.toCommandValue(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A') + .replace(/:/g, '%3A') + .replace(/,/g, '%2C'); +} +//# sourceMappingURL=command.js.map + +/***/ }), + +/***/ 186: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +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()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0; +const command_1 = __nccwpck_require__(351); +const file_command_1 = __nccwpck_require__(717); +const utils_1 = __nccwpck_require__(278); +const os = __importStar(__nccwpck_require__(87)); +const path = __importStar(__nccwpck_require__(622)); +/** + * The code to exit an action + */ +var ExitCode; +(function (ExitCode) { + /** + * A code indicating that the action was successful + */ + ExitCode[ExitCode["Success"] = 0] = "Success"; + /** + * A code indicating that the action was a failure + */ + ExitCode[ExitCode["Failure"] = 1] = "Failure"; +})(ExitCode = exports.ExitCode || (exports.ExitCode = {})); +//----------------------------------------------------------------------- +// Variables +//----------------------------------------------------------------------- +/** + * Sets env variable for this action and future actions in the job + * @param name the name of the variable to set + * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function exportVariable(name, val) { + const convertedVal = utils_1.toCommandValue(val); + process.env[name] = convertedVal; + const filePath = process.env['GITHUB_ENV'] || ''; + if (filePath) { + const delimiter = '_GitHubActionsFileCommandDelimeter_'; + const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`; + file_command_1.issueCommand('ENV', commandValue); + } + else { + command_1.issueCommand('set-env', { name }, convertedVal); + } +} +exports.exportVariable = exportVariable; +/** + * Registers a secret which will get masked from logs + * @param secret value of the secret + */ +function setSecret(secret) { + command_1.issueCommand('add-mask', {}, secret); +} +exports.setSecret = setSecret; +/** + * Prepends inputPath to the PATH (for this action and future actions) + * @param inputPath + */ +function addPath(inputPath) { + const filePath = process.env['GITHUB_PATH'] || ''; + if (filePath) { + file_command_1.issueCommand('PATH', inputPath); + } + else { + command_1.issueCommand('add-path', {}, inputPath); + } + process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; +} +exports.addPath = addPath; +/** + * Gets the value of an input. + * Unless trimWhitespace is set to false in InputOptions, the value is also trimmed. + * Returns an empty string if the value is not defined. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string + */ +function getInput(name, options) { + const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; + if (options && options.required && !val) { + throw new Error(`Input required and not supplied: ${name}`); + } + if (options && options.trimWhitespace === false) { + return val; + } + return val.trim(); +} +exports.getInput = getInput; +/** + * Gets the values of an multiline input. Each value is also trimmed. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string[] + * + */ +function getMultilineInput(name, options) { + const inputs = getInput(name, options) + .split('\n') + .filter(x => x !== ''); + return inputs; +} +exports.getMultilineInput = getMultilineInput; +/** + * Gets the input value of the boolean type in the YAML 1.2 "core schema" specification. + * Support boolean input list: `true | True | TRUE | false | False | FALSE` . + * The return value is also in boolean type. + * ref: https://yaml.org/spec/1.2/spec.html#id2804923 + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns boolean + */ +function getBooleanInput(name, options) { + const trueValue = ['true', 'True', 'TRUE']; + const falseValue = ['false', 'False', 'FALSE']; + const val = getInput(name, options); + if (trueValue.includes(val)) + return true; + if (falseValue.includes(val)) + return false; + throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` + + `Support boolean input list: \`true | True | TRUE | false | False | FALSE\``); +} +exports.getBooleanInput = getBooleanInput; +/** + * Sets the value of an output. + * + * @param name name of the output to set + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function setOutput(name, value) { + process.stdout.write(os.EOL); + command_1.issueCommand('set-output', { name }, value); +} +exports.setOutput = setOutput; +/** + * Enables or disables the echoing of commands into stdout for the rest of the step. + * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. + * + */ +function setCommandEcho(enabled) { + command_1.issue('echo', enabled ? 'on' : 'off'); +} +exports.setCommandEcho = setCommandEcho; +//----------------------------------------------------------------------- +// Results +//----------------------------------------------------------------------- +/** + * Sets the action status to failed. + * When the action exits it will be with an exit code of 1 + * @param message add error issue message + */ +function setFailed(message) { + process.exitCode = ExitCode.Failure; + error(message); +} +exports.setFailed = setFailed; +//----------------------------------------------------------------------- +// Logging Commands +//----------------------------------------------------------------------- +/** + * Gets whether Actions Step Debug is on or not + */ +function isDebug() { + return process.env['RUNNER_DEBUG'] === '1'; +} +exports.isDebug = isDebug; +/** + * Writes debug message to user log + * @param message debug message + */ +function debug(message) { + command_1.issueCommand('debug', {}, message); +} +exports.debug = debug; +/** + * Adds an error issue + * @param message error issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function error(message, properties = {}) { + command_1.issueCommand('error', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message); +} +exports.error = error; +/** + * Adds a warning issue + * @param message warning issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function warning(message, properties = {}) { + command_1.issueCommand('warning', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message); +} +exports.warning = warning; +/** + * Adds a notice issue + * @param message notice issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function notice(message, properties = {}) { + command_1.issueCommand('notice', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message); +} +exports.notice = notice; +/** + * Writes info to log with console.log. + * @param message info message + */ +function info(message) { + process.stdout.write(message + os.EOL); +} +exports.info = info; +/** + * Begin an output group. + * + * Output until the next `groupEnd` will be foldable in this group + * + * @param name The name of the output group + */ +function startGroup(name) { + command_1.issue('group', name); +} +exports.startGroup = startGroup; +/** + * End an output group. + */ +function endGroup() { + command_1.issue('endgroup'); +} +exports.endGroup = endGroup; +/** + * Wrap an asynchronous function call in a group. + * + * Returns the same type as the function itself. + * + * @param name The name of the group + * @param fn The function to wrap in the group + */ +function group(name, fn) { + return __awaiter(this, void 0, void 0, function* () { + startGroup(name); + let result; + try { + result = yield fn(); + } + finally { + endGroup(); + } + return result; + }); +} +exports.group = group; +//----------------------------------------------------------------------- +// Wrapper action state +//----------------------------------------------------------------------- +/** + * Saves state for current action, the state can only be retrieved by this action's post job execution. + * + * @param name name of the state to store + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function saveState(name, value) { + command_1.issueCommand('save-state', { name }, value); +} +exports.saveState = saveState; +/** + * Gets the value of an state set by this action's main execution. + * + * @param name name of the state to get + * @returns string + */ +function getState(name) { + return process.env[`STATE_${name}`] || ''; +} +exports.getState = getState; +//# sourceMappingURL=core.js.map + +/***/ }), + +/***/ 717: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + + +// For internal use, subject to change. +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.issueCommand = void 0; +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +const fs = __importStar(__nccwpck_require__(747)); +const os = __importStar(__nccwpck_require__(87)); +const utils_1 = __nccwpck_require__(278); +function issueCommand(command, message) { + const filePath = process.env[`GITHUB_${command}`]; + if (!filePath) { + throw new Error(`Unable to find environment variable for file command ${command}`); + } + if (!fs.existsSync(filePath)) { + throw new Error(`Missing file at path: ${filePath}`); + } + fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, { + encoding: 'utf8' + }); +} +exports.issueCommand = issueCommand; +//# sourceMappingURL=file-command.js.map + +/***/ }), + +/***/ 278: +/***/ ((__unused_webpack_module, exports) => { + + +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toCommandProperties = exports.toCommandValue = void 0; +/** + * Sanitizes an input into a string so it can be passed into issueCommand safely + * @param input input to sanitize into a string + */ +function toCommandValue(input) { + if (input === null || input === undefined) { + return ''; + } + else if (typeof input === 'string' || input instanceof String) { + return input; + } + return JSON.stringify(input); +} +exports.toCommandValue = toCommandValue; +/** + * + * @param annotationProperties + * @returns The command properties to send with the actual annotation command + * See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646 + */ +function toCommandProperties(annotationProperties) { + if (!Object.keys(annotationProperties).length) { + return {}; + } + return { + title: annotationProperties.title, + line: annotationProperties.startLine, + endLine: annotationProperties.endLine, + col: annotationProperties.startColumn, + endColumn: annotationProperties.endColumn + }; +} +exports.toCommandProperties = toCommandProperties; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 514: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +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()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getExecOutput = exports.exec = void 0; +const string_decoder_1 = __nccwpck_require__(304); +const tr = __importStar(__nccwpck_require__(159)); +/** + * Exec a command. + * Output will be streamed to the live console. + * Returns promise with return code + * + * @param commandLine command to execute (can include additional args). Must be correctly escaped. + * @param args optional arguments for tool. Escaping is handled by the lib. + * @param options optional exec options. See ExecOptions + * @returns Promise exit code + */ +function exec(commandLine, args, options) { + return __awaiter(this, void 0, void 0, function* () { + const commandArgs = tr.argStringToArray(commandLine); + if (commandArgs.length === 0) { + throw new Error(`Parameter 'commandLine' cannot be null or empty.`); + } + // Path to tool to execute should be first arg + const toolPath = commandArgs[0]; + args = commandArgs.slice(1).concat(args || []); + const runner = new tr.ToolRunner(toolPath, args, options); + return runner.exec(); + }); +} +exports.exec = exec; +/** + * Exec a command and get the output. + * Output will be streamed to the live console. + * Returns promise with the exit code and collected stdout and stderr + * + * @param commandLine command to execute (can include additional args). Must be correctly escaped. + * @param args optional arguments for tool. Escaping is handled by the lib. + * @param options optional exec options. See ExecOptions + * @returns Promise exit code, stdout, and stderr + */ +function getExecOutput(commandLine, args, options) { + var _a, _b; + return __awaiter(this, void 0, void 0, function* () { + let stdout = ''; + let stderr = ''; + //Using string decoder covers the case where a mult-byte character is split + const stdoutDecoder = new string_decoder_1.StringDecoder('utf8'); + const stderrDecoder = new string_decoder_1.StringDecoder('utf8'); + const originalStdoutListener = (_a = options === null || options === void 0 ? void 0 : options.listeners) === null || _a === void 0 ? void 0 : _a.stdout; + const originalStdErrListener = (_b = options === null || options === void 0 ? void 0 : options.listeners) === null || _b === void 0 ? void 0 : _b.stderr; + const stdErrListener = (data) => { + stderr += stderrDecoder.write(data); + if (originalStdErrListener) { + originalStdErrListener(data); + } + }; + const stdOutListener = (data) => { + stdout += stdoutDecoder.write(data); + if (originalStdoutListener) { + originalStdoutListener(data); + } + }; + const listeners = Object.assign(Object.assign({}, options === null || options === void 0 ? void 0 : options.listeners), { stdout: stdOutListener, stderr: stdErrListener }); + const exitCode = yield exec(commandLine, args, Object.assign(Object.assign({}, options), { listeners })); + //flush any remaining characters + stdout += stdoutDecoder.end(); + stderr += stderrDecoder.end(); + return { + exitCode, + stdout, + stderr + }; + }); +} +exports.getExecOutput = getExecOutput; +//# sourceMappingURL=exec.js.map + +/***/ }), + +/***/ 159: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +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()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.argStringToArray = exports.ToolRunner = void 0; +const os = __importStar(__nccwpck_require__(87)); +const events = __importStar(__nccwpck_require__(614)); +const child = __importStar(__nccwpck_require__(129)); +const path = __importStar(__nccwpck_require__(622)); +const io = __importStar(__nccwpck_require__(436)); +const ioUtil = __importStar(__nccwpck_require__(962)); +const timers_1 = __nccwpck_require__(213); +/* eslint-disable @typescript-eslint/unbound-method */ +const IS_WINDOWS = process.platform === 'win32'; +/* + * Class for running command line tools. Handles quoting and arg parsing in a platform agnostic way. + */ +class ToolRunner extends events.EventEmitter { + constructor(toolPath, args, options) { + super(); + if (!toolPath) { + throw new Error("Parameter 'toolPath' cannot be null or empty."); + } + this.toolPath = toolPath; + this.args = args || []; + this.options = options || {}; + } + _debug(message) { + if (this.options.listeners && this.options.listeners.debug) { + this.options.listeners.debug(message); + } + } + _getCommandString(options, noPrefix) { + const toolPath = this._getSpawnFileName(); + const args = this._getSpawnArgs(options); + let cmd = noPrefix ? '' : '[command]'; // omit prefix when piped to a second tool + if (IS_WINDOWS) { + // Windows + cmd file + if (this._isCmdFile()) { + cmd += toolPath; + for (const a of args) { + cmd += ` ${a}`; + } + } + // Windows + verbatim + else if (options.windowsVerbatimArguments) { + cmd += `"${toolPath}"`; + for (const a of args) { + cmd += ` ${a}`; + } + } + // Windows (regular) + else { + cmd += this._windowsQuoteCmdArg(toolPath); + for (const a of args) { + cmd += ` ${this._windowsQuoteCmdArg(a)}`; + } + } + } + else { + // OSX/Linux - this can likely be improved with some form of quoting. + // creating processes on Unix is fundamentally different than Windows. + // on Unix, execvp() takes an arg array. + cmd += toolPath; + for (const a of args) { + cmd += ` ${a}`; + } + } + return cmd; + } + _processLineBuffer(data, strBuffer, onLine) { + try { + let s = strBuffer + data.toString(); + let n = s.indexOf(os.EOL); + while (n > -1) { + const line = s.substring(0, n); + onLine(line); + // the rest of the string ... + s = s.substring(n + os.EOL.length); + n = s.indexOf(os.EOL); + } + return s; + } + catch (err) { + // streaming lines to console is best effort. Don't fail a build. + this._debug(`error processing line. Failed with error ${err}`); + return ''; + } + } + _getSpawnFileName() { + if (IS_WINDOWS) { + if (this._isCmdFile()) { + return process.env['COMSPEC'] || 'cmd.exe'; + } + } + return this.toolPath; + } + _getSpawnArgs(options) { + if (IS_WINDOWS) { + if (this._isCmdFile()) { + let argline = `/D /S /C "${this._windowsQuoteCmdArg(this.toolPath)}`; + for (const a of this.args) { + argline += ' '; + argline += options.windowsVerbatimArguments + ? a + : this._windowsQuoteCmdArg(a); + } + argline += '"'; + return [argline]; + } + } + return this.args; + } + _endsWith(str, end) { + return str.endsWith(end); + } + _isCmdFile() { + const upperToolPath = this.toolPath.toUpperCase(); + return (this._endsWith(upperToolPath, '.CMD') || + this._endsWith(upperToolPath, '.BAT')); + } + _windowsQuoteCmdArg(arg) { + // for .exe, apply the normal quoting rules that libuv applies + if (!this._isCmdFile()) { + return this._uvQuoteCmdArg(arg); + } + // otherwise apply quoting rules specific to the cmd.exe command line parser. + // the libuv rules are generic and are not designed specifically for cmd.exe + // command line parser. + // + // for a detailed description of the cmd.exe command line parser, refer to + // http://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/7970912#7970912 + // need quotes for empty arg + if (!arg) { + return '""'; + } + // determine whether the arg needs to be quoted + const cmdSpecialChars = [ + ' ', + '\t', + '&', + '(', + ')', + '[', + ']', + '{', + '}', + '^', + '=', + ';', + '!', + "'", + '+', + ',', + '`', + '~', + '|', + '<', + '>', + '"' + ]; + let needsQuotes = false; + for (const char of arg) { + if (cmdSpecialChars.some(x => x === char)) { + needsQuotes = true; + break; + } + } + // short-circuit if quotes not needed + if (!needsQuotes) { + return arg; + } + // the following quoting rules are very similar to the rules that by libuv applies. + // + // 1) wrap the string in quotes + // + // 2) double-up quotes - i.e. " => "" + // + // this is different from the libuv quoting rules. libuv replaces " with \", which unfortunately + // doesn't work well with a cmd.exe command line. + // + // note, replacing " with "" also works well if the arg is passed to a downstream .NET console app. + // for example, the command line: + // foo.exe "myarg:""my val""" + // is parsed by a .NET console app into an arg array: + // [ "myarg:\"my val\"" ] + // which is the same end result when applying libuv quoting rules. although the actual + // command line from libuv quoting rules would look like: + // foo.exe "myarg:\"my val\"" + // + // 3) double-up slashes that precede a quote, + // e.g. hello \world => "hello \world" + // hello\"world => "hello\\""world" + // hello\\"world => "hello\\\\""world" + // hello world\ => "hello world\\" + // + // technically this is not required for a cmd.exe command line, or the batch argument parser. + // the reasons for including this as a .cmd quoting rule are: + // + // a) this is optimized for the scenario where the argument is passed from the .cmd file to an + // external program. many programs (e.g. .NET console apps) rely on the slash-doubling rule. + // + // b) it's what we've been doing previously (by deferring to node default behavior) and we + // haven't heard any complaints about that aspect. + // + // note, a weakness of the quoting rules chosen here, is that % is not escaped. in fact, % cannot be + // escaped when used on the command line directly - even though within a .cmd file % can be escaped + // by using %%. + // + // the saving grace is, on the command line, %var% is left as-is if var is not defined. this contrasts + // the line parsing rules within a .cmd file, where if var is not defined it is replaced with nothing. + // + // one option that was explored was replacing % with ^% - i.e. %var% => ^%var^%. this hack would + // often work, since it is unlikely that var^ would exist, and the ^ character is removed when the + // variable is used. the problem, however, is that ^ is not removed when %* is used to pass the args + // to an external program. + // + // an unexplored potential solution for the % escaping problem, is to create a wrapper .cmd file. + // % can be escaped within a .cmd file. + let reverse = '"'; + let quoteHit = true; + for (let i = arg.length; i > 0; i--) { + // walk the string in reverse + reverse += arg[i - 1]; + if (quoteHit && arg[i - 1] === '\\') { + reverse += '\\'; // double the slash + } + else if (arg[i - 1] === '"') { + quoteHit = true; + reverse += '"'; // double the quote + } + else { + quoteHit = false; + } + } + reverse += '"'; + return reverse + .split('') + .reverse() + .join(''); + } + _uvQuoteCmdArg(arg) { + // Tool runner wraps child_process.spawn() and needs to apply the same quoting as + // Node in certain cases where the undocumented spawn option windowsVerbatimArguments + // is used. + // + // Since this function is a port of quote_cmd_arg from Node 4.x (technically, lib UV, + // see https://github.com/nodejs/node/blob/v4.x/deps/uv/src/win/process.c for details), + // pasting copyright notice from Node within this function: + // + // Copyright Joyent, Inc. and other Node contributors. All rights reserved. + // + // Permission is hereby granted, free of charge, to any person obtaining a copy + // of this software and associated documentation files (the "Software"), to + // deal in the Software without restriction, including without limitation the + // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + // sell copies of the Software, and to permit persons to whom the Software is + // furnished to do so, subject to the following conditions: + // + // The above copyright notice and this permission notice shall be included in + // all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + // IN THE SOFTWARE. + if (!arg) { + // Need double quotation for empty argument + return '""'; + } + if (!arg.includes(' ') && !arg.includes('\t') && !arg.includes('"')) { + // No quotation needed + return arg; + } + if (!arg.includes('"') && !arg.includes('\\')) { + // No embedded double quotes or backslashes, so I can just wrap + // quote marks around the whole thing. + return `"${arg}"`; + } + // Expected input/output: + // input : hello"world + // output: "hello\"world" + // input : hello""world + // output: "hello\"\"world" + // input : hello\world + // output: hello\world + // input : hello\\world + // output: hello\\world + // input : hello\"world + // output: "hello\\\"world" + // input : hello\\"world + // output: "hello\\\\\"world" + // input : hello world\ + // output: "hello world\\" - note the comment in libuv actually reads "hello world\" + // but it appears the comment is wrong, it should be "hello world\\" + let reverse = '"'; + let quoteHit = true; + for (let i = arg.length; i > 0; i--) { + // walk the string in reverse + reverse += arg[i - 1]; + if (quoteHit && arg[i - 1] === '\\') { + reverse += '\\'; + } + else if (arg[i - 1] === '"') { + quoteHit = true; + reverse += '\\'; + } + else { + quoteHit = false; + } + } + reverse += '"'; + return reverse + .split('') + .reverse() + .join(''); + } + _cloneExecOptions(options) { + options = options || {}; + const result = { + cwd: options.cwd || process.cwd(), + env: options.env || process.env, + silent: options.silent || false, + windowsVerbatimArguments: options.windowsVerbatimArguments || false, + failOnStdErr: options.failOnStdErr || false, + ignoreReturnCode: options.ignoreReturnCode || false, + delay: options.delay || 10000 + }; + result.outStream = options.outStream || process.stdout; + result.errStream = options.errStream || process.stderr; + return result; + } + _getSpawnOptions(options, toolPath) { + options = options || {}; + const result = {}; + result.cwd = options.cwd; + result.env = options.env; + result['windowsVerbatimArguments'] = + options.windowsVerbatimArguments || this._isCmdFile(); + if (options.windowsVerbatimArguments) { + result.argv0 = `"${toolPath}"`; + } + return result; + } + /** + * Exec a tool. + * Output will be streamed to the live console. + * Returns promise with return code + * + * @param tool path to tool to exec + * @param options optional exec options. See ExecOptions + * @returns number + */ + exec() { + return __awaiter(this, void 0, void 0, function* () { + // root the tool path if it is unrooted and contains relative pathing + if (!ioUtil.isRooted(this.toolPath) && + (this.toolPath.includes('/') || + (IS_WINDOWS && this.toolPath.includes('\\')))) { + // prefer options.cwd if it is specified, however options.cwd may also need to be rooted + this.toolPath = path.resolve(process.cwd(), this.options.cwd || process.cwd(), this.toolPath); + } + // if the tool is only a file name, then resolve it from the PATH + // otherwise verify it exists (add extension on Windows if necessary) + this.toolPath = yield io.which(this.toolPath, true); + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + this._debug(`exec tool: ${this.toolPath}`); + this._debug('arguments:'); + for (const arg of this.args) { + this._debug(` ${arg}`); + } + const optionsNonNull = this._cloneExecOptions(this.options); + if (!optionsNonNull.silent && optionsNonNull.outStream) { + optionsNonNull.outStream.write(this._getCommandString(optionsNonNull) + os.EOL); + } + const state = new ExecState(optionsNonNull, this.toolPath); + state.on('debug', (message) => { + this._debug(message); + }); + if (this.options.cwd && !(yield ioUtil.exists(this.options.cwd))) { + return reject(new Error(`The cwd: ${this.options.cwd} does not exist!`)); + } + const fileName = this._getSpawnFileName(); + const cp = child.spawn(fileName, this._getSpawnArgs(optionsNonNull), this._getSpawnOptions(this.options, fileName)); + let stdbuffer = ''; + if (cp.stdout) { + cp.stdout.on('data', (data) => { + if (this.options.listeners && this.options.listeners.stdout) { + this.options.listeners.stdout(data); + } + if (!optionsNonNull.silent && optionsNonNull.outStream) { + optionsNonNull.outStream.write(data); + } + stdbuffer = this._processLineBuffer(data, stdbuffer, (line) => { + if (this.options.listeners && this.options.listeners.stdline) { + this.options.listeners.stdline(line); + } + }); + }); + } + let errbuffer = ''; + if (cp.stderr) { + cp.stderr.on('data', (data) => { + state.processStderr = true; + if (this.options.listeners && this.options.listeners.stderr) { + this.options.listeners.stderr(data); + } + if (!optionsNonNull.silent && + optionsNonNull.errStream && + optionsNonNull.outStream) { + const s = optionsNonNull.failOnStdErr + ? optionsNonNull.errStream + : optionsNonNull.outStream; + s.write(data); + } + errbuffer = this._processLineBuffer(data, errbuffer, (line) => { + if (this.options.listeners && this.options.listeners.errline) { + this.options.listeners.errline(line); + } + }); + }); + } + cp.on('error', (err) => { + state.processError = err.message; + state.processExited = true; + state.processClosed = true; + state.CheckComplete(); + }); + cp.on('exit', (code) => { + state.processExitCode = code; + state.processExited = true; + this._debug(`Exit code ${code} received from tool '${this.toolPath}'`); + state.CheckComplete(); + }); + cp.on('close', (code) => { + state.processExitCode = code; + state.processExited = true; + state.processClosed = true; + this._debug(`STDIO streams have closed for tool '${this.toolPath}'`); + state.CheckComplete(); + }); + state.on('done', (error, exitCode) => { + if (stdbuffer.length > 0) { + this.emit('stdline', stdbuffer); + } + if (errbuffer.length > 0) { + this.emit('errline', errbuffer); + } + cp.removeAllListeners(); + if (error) { + reject(error); + } + else { + resolve(exitCode); + } + }); + if (this.options.input) { + if (!cp.stdin) { + throw new Error('child process missing stdin'); + } + cp.stdin.end(this.options.input); + } + })); + }); + } +} +exports.ToolRunner = ToolRunner; +/** + * Convert an arg string to an array of args. Handles escaping + * + * @param argString string of arguments + * @returns string[] array of arguments + */ +function argStringToArray(argString) { + const args = []; + let inQuotes = false; + let escaped = false; + let arg = ''; + function append(c) { + // we only escape double quotes. + if (escaped && c !== '"') { + arg += '\\'; + } + arg += c; + escaped = false; + } + for (let i = 0; i < argString.length; i++) { + const c = argString.charAt(i); + if (c === '"') { + if (!escaped) { + inQuotes = !inQuotes; + } + else { + append(c); + } + continue; + } + if (c === '\\' && escaped) { + append(c); + continue; + } + if (c === '\\' && inQuotes) { + escaped = true; + continue; + } + if (c === ' ' && !inQuotes) { + if (arg.length > 0) { + args.push(arg); + arg = ''; + } + continue; + } + append(c); + } + if (arg.length > 0) { + args.push(arg.trim()); + } + return args; +} +exports.argStringToArray = argStringToArray; +class ExecState extends events.EventEmitter { + constructor(options, toolPath) { + super(); + this.processClosed = false; // tracks whether the process has exited and stdio is closed + this.processError = ''; + this.processExitCode = 0; + this.processExited = false; // tracks whether the process has exited + this.processStderr = false; // tracks whether stderr was written to + this.delay = 10000; // 10 seconds + this.done = false; + this.timeout = null; + if (!toolPath) { + throw new Error('toolPath must not be empty'); + } + this.options = options; + this.toolPath = toolPath; + if (options.delay) { + this.delay = options.delay; + } + } + CheckComplete() { + if (this.done) { + return; + } + if (this.processClosed) { + this._setResult(); + } + else if (this.processExited) { + this.timeout = timers_1.setTimeout(ExecState.HandleTimeout, this.delay, this); + } + } + _debug(message) { + this.emit('debug', message); + } + _setResult() { + // determine whether there is an error + let error; + if (this.processExited) { + if (this.processError) { + error = new Error(`There was an error when attempting to execute the process '${this.toolPath}'. This may indicate the process failed to start. Error: ${this.processError}`); + } + else if (this.processExitCode !== 0 && !this.options.ignoreReturnCode) { + error = new Error(`The process '${this.toolPath}' failed with exit code ${this.processExitCode}`); + } + else if (this.processStderr && this.options.failOnStdErr) { + error = new Error(`The process '${this.toolPath}' failed because one or more lines were written to the STDERR stream`); + } + } + // clear the timeout + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + this.done = true; + this.emit('done', error, this.processExitCode); + } + static HandleTimeout(state) { + if (state.done) { + return; + } + if (!state.processClosed && state.processExited) { + const message = `The STDIO streams did not close within ${state.delay / + 1000} seconds of the exit event from process '${state.toolPath}'. This may indicate a child process inherited the STDIO streams and has not yet exited.`; + state._debug(message); + } + state._setResult(); + } +} +//# sourceMappingURL=toolrunner.js.map + +/***/ }), + +/***/ 962: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +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 _a; +Object.defineProperty(exports, "__esModule", ({ value: true })); +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__(747)); +const path = __importStar(__nccwpck_require__(622)); +_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'; +function exists(fsPath) { + return __awaiter(this, void 0, void 0, function* () { + try { + yield exports.stat(fsPath); + } + catch (err) { + if (err.code === 'ENOENT') { + return false; + } + throw err; + } + return true; + }); +} +exports.exists = exists; +function isDirectory(fsPath, useStat = false) { + return __awaiter(this, void 0, void 0, function* () { + const stats = useStat ? yield exports.stat(fsPath) : yield exports.lstat(fsPath); + return stats.isDirectory(); + }); +} +exports.isDirectory = isDirectory; +/** + * On OSX/Linux, true if path starts with '/'. On Windows, true for paths like: + * \, \hello, \\hello\share, C:, and C:\hello (and corresponding alternate separator cases). + */ +function isRooted(p) { + p = normalizeSeparators(p); + if (!p) { + throw new Error('isRooted() parameter "p" cannot be empty'); + } + if (exports.IS_WINDOWS) { + return (p.startsWith('\\') || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello + ); // e.g. C: or C:\hello + } + return p.startsWith('/'); +} +exports.isRooted = isRooted; +/** + * Best effort attempt to determine whether a file exists and is executable. + * @param filePath file path to check + * @param extensions additional file extensions to try + * @return if file exists and is executable, returns the file path. otherwise empty string. + */ +function tryGetExecutablePath(filePath, extensions) { + return __awaiter(this, void 0, void 0, function* () { + let stats = undefined; + try { + // test file exists + stats = yield exports.stat(filePath); + } + catch (err) { + if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); + } + } + if (stats && stats.isFile()) { + if (exports.IS_WINDOWS) { + // on Windows, test for valid extension + const upperExt = path.extname(filePath).toUpperCase(); + if (extensions.some(validExt => validExt.toUpperCase() === upperExt)) { + return filePath; + } + } + else { + if (isUnixExecutable(stats)) { + return filePath; + } + } + } + // try each extension + const originalFilePath = filePath; + for (const extension of extensions) { + filePath = originalFilePath + extension; + stats = undefined; + try { + stats = yield exports.stat(filePath); + } + catch (err) { + if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); + } + } + if (stats && stats.isFile()) { + if (exports.IS_WINDOWS) { + // preserve the case of the actual file (since an extension was appended) + try { + const directory = path.dirname(filePath); + const upperName = path.basename(filePath).toUpperCase(); + for (const actualName of yield exports.readdir(directory)) { + if (upperName === actualName.toUpperCase()) { + filePath = path.join(directory, actualName); + break; + } + } + } + catch (err) { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}`); + } + return filePath; + } + else { + if (isUnixExecutable(stats)) { + return filePath; + } + } + } + } + return ''; + }); +} +exports.tryGetExecutablePath = tryGetExecutablePath; +function normalizeSeparators(p) { + p = p || ''; + if (exports.IS_WINDOWS) { + // convert slashes on Windows + p = p.replace(/\//g, '\\'); + // remove redundant slashes + return p.replace(/\\\\+/g, '\\'); + } + // remove redundant slashes + return p.replace(/\/\/+/g, '/'); +} +// on Mac/Linux, test the execute bit +// R W X R W X R W X +// 256 128 64 32 16 8 4 2 1 +function isUnixExecutable(stats) { + return ((stats.mode & 1) > 0 || + ((stats.mode & 8) > 0 && stats.gid === process.getgid()) || + ((stats.mode & 64) > 0 && stats.uid === process.getuid())); +} +// Get the path of cmd.exe in windows +function getCmdPath() { + var _a; + return (_a = process.env['COMSPEC']) !== null && _a !== void 0 ? _a : `cmd.exe`; +} +exports.getCmdPath = getCmdPath; +//# sourceMappingURL=io-util.js.map + +/***/ }), + +/***/ 436: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +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()); + }); +}; +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__(357); +const childProcess = __importStar(__nccwpck_require__(129)); +const path = __importStar(__nccwpck_require__(622)); +const util_1 = __nccwpck_require__(669); +const ioUtil = __importStar(__nccwpck_require__(962)); +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 + * + * @param source source path + * @param dest destination path + * @param options optional. See CopyOptions. + */ +function cp(source, dest, options = {}) { + return __awaiter(this, void 0, void 0, function* () { + const { force, recursive, copySourceDirectory } = readCopyOptions(options); + const destStat = (yield ioUtil.exists(dest)) ? yield ioUtil.stat(dest) : null; + // Dest is an existing file, but not forcing + if (destStat && destStat.isFile() && !force) { + return; + } + // If dest is an existing directory, should copy inside. + const newDest = destStat && destStat.isDirectory() && copySourceDirectory + ? path.join(dest, path.basename(source)) + : dest; + if (!(yield ioUtil.exists(source))) { + throw new Error(`no such file or directory: ${source}`); + } + const sourceStat = yield ioUtil.stat(source); + if (sourceStat.isDirectory()) { + if (!recursive) { + throw new Error(`Failed to copy. ${source} is a directory, but tried to copy without recursive flag.`); + } + else { + yield cpDirRecursive(source, newDest, 0, force); + } + } + else { + if (path.relative(source, newDest) === '') { + // a file cannot be copied to itself + throw new Error(`'${newDest}' and '${source}' are the same file`); + } + yield copyFile(source, newDest, force); + } + }); +} +exports.cp = cp; +/** + * Moves a path. + * + * @param source source path + * @param dest destination path + * @param options optional. See MoveOptions. + */ +function mv(source, dest, options = {}) { + return __awaiter(this, void 0, void 0, function* () { + if (yield ioUtil.exists(dest)) { + let destExists = true; + if (yield ioUtil.isDirectory(dest)) { + // If dest is directory copy src into dest + dest = path.join(dest, path.basename(source)); + destExists = yield ioUtil.exists(dest); + } + if (destExists) { + if (options.force == null || options.force) { + yield rmRF(dest); + } + else { + throw new Error('Destination already exists'); + } + } + } + yield mkdirP(path.dirname(dest)); + yield ioUtil.rename(source, dest); + }); +} +exports.mv = mv; +/** + * Remove a path recursively with force + * + * @param inputPath path to remove + */ +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; + } + } + 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); + } + } + }); +} +exports.rmRF = rmRF; +/** + * Make a directory. Creates the full path with folders in between + * Will throw if it fails + * + * @param fsPath path to create + * @returns Promise + */ +function mkdirP(fsPath) { + return __awaiter(this, void 0, void 0, function* () { + assert_1.ok(fsPath, 'a path argument must be provided'); + yield ioUtil.mkdir(fsPath, { recursive: true }); + }); +} +exports.mkdirP = mkdirP; +/** + * Returns path of a tool had the tool actually been invoked. Resolves via paths. + * If you check and the tool does not exist, it will throw. + * + * @param tool name of the tool + * @param check whether to check if tool exists + * @returns Promise path to tool + */ +function which(tool, check) { + return __awaiter(this, void 0, void 0, function* () { + if (!tool) { + throw new Error("parameter 'tool' is required"); + } + // recursive when check=true + if (check) { + const result = yield which(tool, false); + if (!result) { + if (ioUtil.IS_WINDOWS) { + throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.`); + } + else { + throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.`); + } + } + return result; + } + const matches = yield findInPath(tool); + if (matches && matches.length > 0) { + return matches[0]; + } + return ''; + }); +} +exports.which = which; +/** + * Returns a list of all occurrences of the given tool on the system path. + * + * @returns Promise the paths of the tool + */ +function findInPath(tool) { + return __awaiter(this, void 0, void 0, function* () { + if (!tool) { + throw new Error("parameter 'tool' is required"); + } + // build the list of extensions to try + const extensions = []; + if (ioUtil.IS_WINDOWS && process.env['PATHEXT']) { + for (const extension of process.env['PATHEXT'].split(path.delimiter)) { + if (extension) { + extensions.push(extension); + } + } + } + // if it's rooted, return it if exists. otherwise return empty. + if (ioUtil.isRooted(tool)) { + const filePath = yield ioUtil.tryGetExecutablePath(tool, extensions); + if (filePath) { + return [filePath]; + } + return []; + } + // if any path separators, return empty + if (tool.includes(path.sep)) { + return []; + } + // build the list of directories + // + // Note, technically "where" checks the current directory on Windows. From a toolkit perspective, + // it feels like we should not do this. Checking the current directory seems like more of a use + // case of a shell, and the which() function exposed by the toolkit should strive for consistency + // across platforms. + const directories = []; + if (process.env.PATH) { + for (const p of process.env.PATH.split(path.delimiter)) { + if (p) { + directories.push(p); + } + } + } + // find all matches + const matches = []; + for (const directory of directories) { + const filePath = yield ioUtil.tryGetExecutablePath(path.join(directory, tool), extensions); + if (filePath) { + matches.push(filePath); + } + } + return matches; + }); +} +exports.findInPath = findInPath; +function readCopyOptions(options) { + const force = options.force == null ? true : options.force; + const recursive = Boolean(options.recursive); + const copySourceDirectory = options.copySourceDirectory == null + ? true + : Boolean(options.copySourceDirectory); + return { force, recursive, copySourceDirectory }; +} +function cpDirRecursive(sourceDir, destDir, currentDepth, force) { + return __awaiter(this, void 0, void 0, function* () { + // Ensure there is not a run away recursive copy + if (currentDepth >= 255) + return; + currentDepth++; + yield mkdirP(destDir); + const files = yield ioUtil.readdir(sourceDir); + for (const fileName of files) { + const srcFile = `${sourceDir}/${fileName}`; + const destFile = `${destDir}/${fileName}`; + const srcFileStat = yield ioUtil.lstat(srcFile); + if (srcFileStat.isDirectory()) { + // Recurse + yield cpDirRecursive(srcFile, destFile, currentDepth, force); + } + else { + yield copyFile(srcFile, destFile, force); + } + } + // Change the mode for the newly created directory + yield ioUtil.chmod(destDir, (yield ioUtil.stat(sourceDir)).mode); + }); +} +// Buffered file copy +function copyFile(srcFile, destFile, force) { + return __awaiter(this, void 0, void 0, function* () { + if ((yield ioUtil.lstat(srcFile)).isSymbolicLink()) { + // unlink/re-link it + try { + yield ioUtil.lstat(destFile); + yield ioUtil.unlink(destFile); + } + catch (e) { + // Try to override file permission + if (e.code === 'EPERM') { + yield ioUtil.chmod(destFile, '0666'); + yield ioUtil.unlink(destFile); + } + // other errors = it doesn't exist, no work to do + } + // Copy over symlink + const symlinkFull = yield ioUtil.readlink(srcFile); + yield ioUtil.symlink(symlinkFull, destFile, ioUtil.IS_WINDOWS ? 'junction' : null); + } + else if (!(yield ioUtil.exists(destFile)) || force) { + yield ioUtil.copyFile(srcFile, destFile); + } + }); +} +//# sourceMappingURL=io.js.map + +/***/ }), + +/***/ 357: +/***/ ((module) => { + +module.exports = require("assert"); + +/***/ }), + +/***/ 129: +/***/ ((module) => { + +module.exports = require("child_process"); + +/***/ }), + +/***/ 614: +/***/ ((module) => { + +module.exports = require("events"); + +/***/ }), + +/***/ 747: +/***/ ((module) => { + +module.exports = require("fs"); + +/***/ }), + +/***/ 87: +/***/ ((module) => { + +module.exports = require("os"); + +/***/ }), + +/***/ 622: +/***/ ((module) => { + +module.exports = require("path"); + +/***/ }), + +/***/ 304: +/***/ ((module) => { + +module.exports = require("string_decoder"); + +/***/ }), + +/***/ 213: +/***/ ((module) => { + +module.exports = require("timers"); + +/***/ }), + +/***/ 669: +/***/ ((module) => { + +module.exports = require("util"); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __nccwpck_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ var threw = true; +/******/ try { +/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __nccwpck_require__); +/******/ threw = false; +/******/ } finally { +/******/ if(threw) delete __webpack_module_cache__[moduleId]; +/******/ } +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat get default export */ +/******/ (() => { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __nccwpck_require__.n = (module) => { +/******/ var getter = module && module.__esModule ? +/******/ () => (module['default']) : +/******/ () => (module); +/******/ __nccwpck_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __nccwpck_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__nccwpck_require__.o(definition, key) && !__nccwpck_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __nccwpck_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __nccwpck_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/compat */ +/******/ +/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. +(() => { +__nccwpck_require__.r(__webpack_exports__); +/* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(186); +/* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__nccwpck_require__.n(_actions_core__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _actions_exec__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(514); +/* harmony import */ var _actions_exec__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__nccwpck_require__.n(_actions_exec__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __nccwpck_require__(622); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__nccwpck_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); +var __awaiter = (undefined && undefined.__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()); + }); +}; + + + +function run() { + return __awaiter(this, void 0, void 0, function* () { + try { + const parentDir = path__WEBPACK_IMPORTED_MODULE_2___default().resolve(__dirname, '..'); + // Get action inputs + const unsetPrevious = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('unset-previous'); + const exportEnv = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('export-env'); + // Execute bash script + yield _actions_exec__WEBPACK_IMPORTED_MODULE_1__.exec(`sh -c "` + + `INPUT_UNSET_PREVIOUS=` + unsetPrevious + ` ` + + `INPUT_EXPORT_ENV=` + exportEnv + ` ` + + parentDir + `/entrypoint.sh"`); + } + catch (error) { + _actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed(error.message); + } + }); +} +run(); + +})(); + +module.exports = __webpack_exports__; +/******/ })() +; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..338bea8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,351 @@ +{ + "name": "load-secrets-action", + "version": "1.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "load-secrets-action", + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.5.0", + "@actions/exec": "^1.1.0" + }, + "devDependencies": { + "@vercel/ncc": "^0.30.0", + "typescript": "^4.4.2" + } + }, + "node_modules/@actions/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.5.0.tgz", + "integrity": "sha512-eDOLH1Nq9zh+PJlYLqEMkS/jLQxhksPNmUGNBHfa4G+tQmnIhzpctxmchETtVGyBOvXgOVVpYuE40+eS4cUnwQ==" + }, + "node_modules/@actions/exec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.0.tgz", + "integrity": "sha512-LImpN9AY0J1R1mEYJjVJfSZWU4zYOlEcwSTgPve1rFQqK5AwrEs6uWW5Rv70gbDIQIAUwI86z6B+9mPK4w9Sbg==", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/io": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.1.tgz", + "integrity": "sha512-Qi4JoKXjmE0O67wAOH6y0n26QXhMKMFo7GD/4IXNVcrtLjUlGjGuVys6pQgwF3ArfGTQu0XpqaNr0YhED2RaRA==" + }, + "node_modules/@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", + "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.7.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.10.tgz", + "integrity": "sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==", + "dev": true + }, + "node_modules/@vercel/ncc": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.30.0.tgz", + "integrity": "sha512-16ePj2GkwjomvE0HLL5ny+d+sudOwvZNYW8vjpMh3cyWdFxoMI8KSQiolVxeHBULbU1C5mVxLK5nL9NtnnpIew==", + "dev": true, + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/acorn": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.1.1.tgz", + "integrity": "sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/ts-node": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", + "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "0.6.1", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", + "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + }, + "dependencies": { + "@actions/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.5.0.tgz", + "integrity": "sha512-eDOLH1Nq9zh+PJlYLqEMkS/jLQxhksPNmUGNBHfa4G+tQmnIhzpctxmchETtVGyBOvXgOVVpYuE40+eS4cUnwQ==" + }, + "@actions/exec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.0.tgz", + "integrity": "sha512-LImpN9AY0J1R1mEYJjVJfSZWU4zYOlEcwSTgPve1rFQqK5AwrEs6uWW5Rv70gbDIQIAUwI86z6B+9mPK4w9Sbg==", + "requires": { + "@actions/io": "^1.0.1" + } + }, + "@actions/io": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.1.tgz", + "integrity": "sha512-Qi4JoKXjmE0O67wAOH6y0n26QXhMKMFo7GD/4IXNVcrtLjUlGjGuVys6pQgwF3ArfGTQu0XpqaNr0YhED2RaRA==" + }, + "@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "dev": true + }, + "@cspotcode/source-map-support": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", + "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "dev": true, + "requires": { + "@cspotcode/source-map-consumer": "0.8.0" + } + }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "@types/node": { + "version": "16.7.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.10.tgz", + "integrity": "sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==", + "dev": true + }, + "@vercel/ncc": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.30.0.tgz", + "integrity": "sha512-16ePj2GkwjomvE0HLL5ny+d+sudOwvZNYW8vjpMh3cyWdFxoMI8KSQiolVxeHBULbU1C5mVxLK5nL9NtnnpIew==", + "dev": true + }, + "acorn": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", + "dev": true + }, + "acorn-walk": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.1.1.tgz", + "integrity": "sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==", + "dev": true + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "ts-node": { + "version": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", + "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "0.6.1", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + } + }, + "typescript": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", + "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..56cc50e --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "load-secrets-action", + "version": "1.1.0", + "description": "Load Secrets from 1Password", + "main": "dist/index.js", + "directories": { + "test": "tests" + }, + "scripts": { + "build": "ncc build src/index.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/1Password/load-secrets-action.git" + }, + "keywords": [ + "actions", + "1password", + "load secrets", + "connect" + ], + "author": "1Password", + "license": "MIT", + "bugs": { + "url": "https://github.com/1Password/load-secrets-action/issues" + }, + "homepage": "https://github.com/1Password/load-secrets-action#readme", + "dependencies": { + "@actions/core": "^1.5.0", + "@actions/exec": "^1.1.0" + }, + "devDependencies": { + "@vercel/ncc": "^0.30.0", + "typescript": "^4.4.2" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a4d5f4c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,24 @@ +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; +import path from 'path'; + +async function run(): Promise { + try { + const parentDir = path.resolve(__dirname, '..'); + + // Get action inputs + const unsetPrevious = core.getInput('unset-previous'); + const exportEnv = core.getInput('export-env'); + + // Execute bash script + await exec.exec(`sh -c "` + + `INPUT_UNSET_PREVIOUS=` + unsetPrevious + ` ` + + `INPUT_EXPORT_ENV=` + exportEnv + ` ` + + parentDir + `/entrypoint.sh"`); + + } catch (error: any) { + core.setFailed(error.message); + } +} + +run(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5bf4ee5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true + }, + "exclude": ["node_modules"] +} \ No newline at end of file From 4baca64066d1f0a4aa4ea61509ed636214357851 Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Wed, 1 Sep 2021 18:23:35 +0200 Subject: [PATCH 07/46] Enable using loaded secrets from step's output --- action.yml | 3 +++ entrypoint.sh | 35 ++++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/action.yml b/action.yml index 1631070..bd337e4 100644 --- a/action.yml +++ b/action.yml @@ -8,6 +8,9 @@ inputs: unset-previous: description: Whether to unset environment variables populated by 1Password in earlier job steps default: false + export-env: + description: Export the secrets as environment variables + default: false runs: using: 'node12' main: 'dist/index.js' diff --git a/entrypoint.sh b/entrypoint.sh index 4336bc3..144ad7a 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -62,19 +62,28 @@ for env_var in $(op env ls); do done unset IFS - # To support multiline secrets, we'll use the heredoc syntax to populate the environment variables. - # As the heredoc identifier, we'll use a randomly generated 64-character string, - # so that collisions are practically impossible. - random_heredoc_identifier=$(openssl rand -hex 16) - - { - # Populate env var, using heredoc syntax with generated identifier - echo "$env_var<<${random_heredoc_identifier}" - echo "$secret_value" - echo "${random_heredoc_identifier}" - } >> $GITHUB_ENV - - managed_variables+=("$env_var") + if [ "$INPUT_EXPORT_ENV" == "true" ]; then + # To support multiline secrets, we'll use the heredoc syntax to populate the environment variables. + # As the heredoc identifier, we'll use a randomly generated 64-character string, + # so that collisions are practically impossible. + random_heredoc_identifier=$(openssl rand -hex 16) + + { + # Populate env var, using heredoc syntax with generated identifier + echo "$env_var<<${random_heredoc_identifier}" + echo "$secret_value" + echo "${random_heredoc_identifier}" + } >> $GITHUB_ENV + + managed_variables+=("$env_var") + + else + # Prepare the secret_value to be outputed properly (especially multiline secrets) + secret_value=$(echo "$secret_value" | awk -v ORS='%0A' '1') + + echo "::set-output name=$env_var::$secret_value" + fi + done unset IFS From 000522e32f92ff08d990b76418cf437b6011f65d Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Wed, 1 Sep 2021 20:09:34 +0200 Subject: [PATCH 08/46] Adjust package dependencies and add newlines --- .gitignore | 2 +- package.json | 1 + src/index.ts | 2 +- tsconfig.json | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 40b878d..c2658d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -node_modules/ \ No newline at end of file +node_modules/ diff --git a/package.json b/package.json index 56cc50e..e611053 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@actions/exec": "^1.1.0" }, "devDependencies": { + "@types/node": "^16.7.10", "@vercel/ncc": "^0.30.0", "typescript": "^4.4.2" } diff --git a/src/index.ts b/src/index.ts index a4d5f4c..855332d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,4 +21,4 @@ async function run(): Promise { } } -run(); \ No newline at end of file +run(); diff --git a/tsconfig.json b/tsconfig.json index 5bf4ee5..232eb8d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,4 +10,4 @@ "esModuleInterop": true }, "exclude": ["node_modules"] -} \ No newline at end of file +} From 4af3346b6afc5f54cafec0219605590f922e0040 Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Mon, 6 Sep 2021 12:39:46 +0300 Subject: [PATCH 09/46] Update README with new functionality Give examples for both ways of loading secrets. Update masking. Add security and help sections. --- README.md | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c3f18a4..d0b718c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,90 @@ # Load Secrets from 1Password - GitHub Action -The action to load secrets from [1Password Connect](https://1password.com/secrets/) into GitHub Actions. +This action loads secrets from [1Password Connect](https://1password.com/secrets/) into GitHub Actions. Specify right from your workflow YAML which secrets from 1Password should be loaded into your job, and the action will make them available as environment variables for the next steps. +## Prerequisites + - [1Password Connect](https://support.1password.com/secrets-automation/#step-2-deploy-a-1password-connect-server) deployed in your infrastructure + ## Usage +There are two ways that secrets can be loaded: + - [use the secrets from the action's ouput](#use-secrets-from-the-actions-output) + - [export secrets as environment variables](#export-secrets-as-environment-variables) + +### Use secrets from the action's output + +This approach enables the user to use the loaded secrets as an output from the step: `steps.step-id.outputs.secret-name`. You need to set an id for the step that uses this action to be able to access its outputs. More details about the metadata syntax [here](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputsoutput_id). + +```yml +on: push +jobs: + hello-world: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Load secret + id: op-load-secret + uses: 1password/load-secrets-action@v1 + env: + OP_CONNECT_HOST: + OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }} + SECRET: op://app-cicd/hello-world/secret + + - name: Print masked secret + run: echo "Secret: ${{ steps.op-load-secret.outputs.SECRET }}" + # Prints: Secret: *** +``` + +
+Longer usage example + +```yml +on: push +name: Deploy app + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Configure 1Password Connect + uses: 1password/load-secrets-action/configure@v1 + with: + # Persist the 1Password Connect URL for next steps. You can also persist + # the Connect token using input `connect-token`, but keep in mind that + # every single step in the job would then be able to access the token. + connect-host: https://1password.acme.com + + - name: Load Docker credentials + id: load-docker-credentials + uses: 1password/load-secrets-action@v1 + env: + OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }} + DOCKERHUB_USERNAME: op://app-cicd/docker/username + DOCKERHUB_TOKEN: op://app-cicd/docker/token + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ steps.load-docker-credentials.outputs.DOCKERHUB_USERNAME }} + password: ${{ steps.load-docker-credentials.outputs.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + push: true + tags: acme/app:latest +``` +
+ +### Export secrets as environment variables + +In this approach, the user can access the loaded secrets as environment variables. These environment variables are accessible at a job level. + ```yml on: push jobs: @@ -16,6 +95,9 @@ jobs: - name: Load secret uses: 1password/load-secrets-action@v1 + with: + # Export loaded secrets as environment variables + export-env: true env: OP_CONNECT_HOST: OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }} @@ -48,6 +130,9 @@ jobs: - name: Load Docker credentials uses: 1password/load-secrets-action@v1 + with: + # Export loaded secrets as environment variables + export-env: true env: OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }} DOCKERHUB_USERNAME: op://app-cicd/docker/username @@ -71,6 +156,8 @@ jobs: - name: Load AWS credentials uses: 1password/load-secrets-action@v1 with: + # Export loaded secrets as environment variables + export-env: true # Remove local copies of the Docker credentials, which are not needed anymore unset-previous: true env: @@ -89,6 +176,7 @@ jobs: | Name | Default | Description | |---|---|---| +| `export-env` | `false` | Export the loaded secrets as environment variables | | `unset-previous` | `false` | Whether to unset environment variables populated by 1Password in earlier job steps | ## Secrets Reference Syntax @@ -107,12 +195,9 @@ So for example, the reference URI `op://app-cicd/aws/secret-access-key` would be ## Masking -Similar to regular GitHub repository secrets, secret fields from 1Password will automatically be masked from the GitHub Actions logs too. -A 1Password field is considered 'secret' when it's marked as concealed (which shows as `•••••••` in the 1Password GUI) or when it's a secure note. +Similar to regular GitHub repository secrets, fields from 1Password will automatically be masked from the GitHub Actions logs too. So if one of these values accidentally gets printed, it'll get replaced with `***`. -This means that a username or port field for example will not get masked. - ## 1Password Connect Configuration To use the action, you need to have a [1Password Connect](https://support.1password.com/secrets-automation/#step-1-set-up-a-secrets-automation-workflow) instance deployed somewhere. @@ -150,3 +235,15 @@ jobs: ## Supported Runners You can run the action on Linux and macOS runners. Windows is currently not supported. + +## Security + +1Password requests you practice responsible disclosure if you discover a vulnerability. + +Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits). + +For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits). + +## Getting help + +If you find yourself stuck, visit our [**Support Page**](https://support.1password.com/) for help. From da5dd0865daab0ef6bcf4d64ad87edbebd5e4e2d Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Tue, 5 Oct 2021 16:34:31 +0200 Subject: [PATCH 10/46] Add newline to trigger pipeline --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d0b718c..5351238 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ This action loads secrets from [1Password Connect](https://1password.com/secrets/) into GitHub Actions. + Specify right from your workflow YAML which secrets from 1Password should be loaded into your job, and the action will make them available as environment variables for the next steps. ## Prerequisites From 2faffa0507a85f470ca22d5a5123577bfe3ba5bc Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Tue, 9 Aug 2022 11:21:49 +0300 Subject: [PATCH 11/46] split logic for connect and service_account flows --- entrypoint.sh | 155 +++++++++++++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 64 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 144ad7a..c268722 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,90 +2,117 @@ # shellcheck disable=SC2046,SC2001,SC2086 set -e -# Install op-cli -if [[ "$OSTYPE" == "linux-gnu"* ]]; then - curl -sSfLo op.zip "https://bucket.agilebits.com/cli-private-beta/v2/op_linux_amd64_v2-alpha2.zip" -elif [[ "$OSTYPE" == "darwin"* ]]; then - curl -sSfLo op.zip "https://bucket.agilebits.com/cli-private-beta/v2/op_darwin_amd64_v2-alpha2.zip" -fi -unzip -od /usr/local/bin/ op.zip && rm op.zip - -if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then - echo "\$OP_CONNECT_TOKEN and \$OP_CONNECT_HOST must be set" - exit 1 -fi +readonly CONNECT="CONNECT" +readonly SERVICE_ACCOUNT="SERVICE_ACCOUNT" +auth_type=$CONNECT managed_variables_var="OP_MANAGED_VARIABLES" -IFS=',' read -r -a managed_variables <<< "$(printenv $managed_variables_var)" +IFS=',' # Unset all secrets managed by 1Password if `unset-previous` is set. -if [ "$INPUT_UNSET_PREVIOUS" == "true" ]; then - echo "Unsetting previous values..." +unset_prev_secrets() { + if [ "$INPUT_UNSET_PREVIOUS" == "true" ]; then + echo "Unsetting previous values..." - # Find environment variables that are managed by 1Password. - for env_var in "${managed_variables[@]}"; do - echo "Unsetting $env_var" - unset $env_var + # Find environment variables that are managed by 1Password. + for env_var in "${managed_variables[@]}"; do + echo "Unsetting $env_var" + unset $env_var - echo "$env_var=" >> $GITHUB_ENV + echo "$env_var=" >> $GITHUB_ENV - # Keep the masks, just in case. - done + # Keep the masks, just in case. + done - managed_variables=() -fi + managed_variables=() + fi +} + +# Install op-cli +install_op_cli() { + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + curl -sSfLo op.zip "https://bucket.agilebits.com/cli-private-beta/v2/op_linux_amd64_v2-alpha2.zip" + elif [[ "$OSTYPE" == "darwin"* ]]; then + curl -sSfLo op.zip "https://bucket.agilebits.com/cli-private-beta/v2/op_darwin_amd64_v2-alpha2.zip" + fi + unzip -od /usr/local/bin/ op.zip && rm op.zip +} # Iterate over environment varables to find 1Password references, load the secret values, # and make them available as environment variables in the next steps. -IFS=$'\n' -for env_var in $(op env ls); do - ref=$(printenv $env_var) +extract_from_op_env() { + IFS=$'\n' + for env_var in $(op env ls); do + ref=$(printenv $env_var) - echo "Populating variable: $env_var" - secret_value=$(op read $ref) + echo "Populating variable: $env_var" + secret_value=$(op read $ref) - if [ -z "$secret_value" ]; then - echo "Could not find or access secret $ref" - exit 1 - fi + if [ -z "$secret_value" ]; then + echo "Could not find or access secret $ref" + exit 1 + fi - # Register a mask for the secret to prevent accidental log exposure. - # To support multiline secrets, escape percent signs and add a mask per line. - escaped_mask_value=$(echo "$secret_value" | sed -e 's/%/%25/g') - IFS=$'\n' - for line in $escaped_mask_value; do - if [ "${#line}" -lt 3 ]; then - # To avoid false positives and unreadable logs, omit mask for lines that are too short. - continue + # Register a mask for the secret to prevent accidental log exposure. + # To support multiline secrets, escape percent signs and add a mask per line. + escaped_mask_value=$(echo "$secret_value" | sed -e 's/%/%25/g') + IFS=$'\n' + for line in $escaped_mask_value; do + if [ "${#line}" -lt 3 ]; then + # To avoid false positives and unreadable logs, omit mask for lines that are too short. + continue + fi + echo "::add-mask::$line" + done + unset IFS + + if [ "$INPUT_EXPORT_ENV" == "true" ]; then + # To support multiline secrets, we'll use the heredoc syntax to populate the environment variables. + # As the heredoc identifier, we'll use a randomly generated 64-character string, + # so that collisions are practically impossible. + random_heredoc_identifier=$(openssl rand -hex 16) + + { + # Populate env var, using heredoc syntax with generated identifier + echo "$env_var<<${random_heredoc_identifier}" + echo "$secret_value" + echo "${random_heredoc_identifier}" + } >> $GITHUB_ENV + + else + # Prepare the secret_value to be outputed properly (especially multiline secrets) + secret_value=$(echo "$secret_value" | awk -v ORS='%0A' '1') + + echo "::set-output name=$env_var::$secret_value" fi - echo "::add-mask::$line" + + managed_variables+=("$env_var") + done unset IFS +} + +read -r -a managed_variables <<< "$(printenv $managed_variables_var)" - if [ "$INPUT_EXPORT_ENV" == "true" ]; then - # To support multiline secrets, we'll use the heredoc syntax to populate the environment variables. - # As the heredoc identifier, we'll use a randomly generated 64-character string, - # so that collisions are practically impossible. - random_heredoc_identifier=$(openssl rand -hex 16) - - { - # Populate env var, using heredoc syntax with generated identifier - echo "$env_var<<${random_heredoc_identifier}" - echo "$secret_value" - echo "${random_heredoc_identifier}" - } >> $GITHUB_ENV - - managed_variables+=("$env_var") - - else - # Prepare the secret_value to be outputed properly (especially multiline secrets) - secret_value=$(echo "$secret_value" | awk -v ORS='%0A' '1') - - echo "::set-output name=$env_var::$secret_value" +if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then + if [ -z "$OP_SERVICE_ACCOUNT_TOKEN" ]; then + echo "(\$OP_CONNECT_TOKEN and \$OP_CONNECT_HOST) or \$OP_SERVICE_ACCOUNT_TOKEN must be set" + exit 1 fi -done -unset IFS + auth_type=$SERVICE_ACCOUNT +fi + +printf "Authenticated with %s \n" $auth_type + +unset_prev_secrets + +if [ "$auth_type" == "$SERVICE_ACCOUNT" ]; then + install_op_cli + extract_from_op_env +elif [ "$auth_type" == "$CONNECT" ]; then + echo "Fetch via connect" +fi # Add extra env var that lists which secrets are managed by 1Password so that in a later step # these can be unset again. From 300c776a97db52fb591f2fd8a6f58d8965d6485b Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Tue, 9 Aug 2022 11:39:34 +0300 Subject: [PATCH 12/46] revert logic to fetch items using connect --- entrypoint.sh | 128 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index c268722..bd511ec 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -38,7 +38,7 @@ install_op_cli() { unzip -od /usr/local/bin/ op.zip && rm op.zip } -# Iterate over environment varables to find 1Password references, load the secret values, +# Load environment variables using op cli. Iterate over them to find 1Password references, load the secret values, # and make them available as environment variables in the next steps. extract_from_op_env() { IFS=$'\n' @@ -92,6 +92,130 @@ extract_from_op_env() { unset IFS } +# Load environment variables using connect service. Iterate over hem to find 1Password references, load the secret values, +# and make them available as environment variables in the next steps. +extract_from_connect() { + curl_headers=(-H "Content-Type: application/json" -H "Authorization: Bearer $OP_CONNECT_TOKEN") + IFS=$'\n' + + for possible_ref in $(printenv | grep "=op://" | grep -v "^#"); do + env_var=$(echo "$possible_ref" | cut -d '=' -f1) + ref=$(printenv $env_var) + + if [[ ! $ref == "op://"* ]]; then + echo "Not really a reference: $ref" + continue + fi + + path=$(echo $ref | sed -e "s/^op:\/\///") + if [ $(echo "$path" | tr -cd '/' | wc -c) -lt 2 ]; then + echo "Expected path to be in format op:///[/
]/: $ref" + continue + fi + + echo "Populating variable: $env_var" + + vault="" + item="" + section="" + field="" + i=0 + IFS="/" + for component in $path; do + ((i+=1)) + case "$i" in + 1) vault=$component ;; + 2) item=$component ;; + 3) section=$component ;; + 4) field=$component ;; + esac + done + unset IFS + + # If field is not set, it may have wrongfully been interpreted as the section. + if [ -z "$field" ]; then + field="$section" + section="" + fi + + if [[ $(echo -n $(echo $vault | grep "^[a-z0-9]*$") | wc -c) -ne 26 ]]; then + echo "Getting vault ID from vault name: $vault" + vault=$(curl -sSf "${curl_headers[@]}" "$OP_CONNECT_HOST/v1/vaults?filter=name%20eq%20%22$vault%22" | jq -r '.[0] | .id') + if [ -z "$vault" ]; then + echo "Could not find vault ID for vault: $vault" + exit 1 + fi + fi + + if [[ $(echo -n $(echo $item | grep "^[a-z0-9]*$") | wc -c) -ne 26 ]]; then + echo "Getting item ID from vault $vault..." + item=$(curl -sSf "${curl_headers[@]}" "$OP_CONNECT_HOST/v1/vaults/$vault/items?filter=title%20eq%20%22$item%22" | jq -r '.[0] | .id') + if [ -z "$item" ]; then + echo "Could not find item ID for item: $item" + exit 1 + fi + fi + + echo "Loading item $item from vault $vault..." + item_json=$(curl -sSf "${curl_headers[@]}" "$OP_CONNECT_HOST/v1/vaults/$vault/items/$item") + + jq_field_selector=".id == \"$field\" or .label == \"$field\"" + jq_section_selector=".section == null" + + # If the reference contains a section, edit the jq selector to take that into account. + if [ -n "$section" ]; then + echo "Looking for section: $section" + section_id=$(echo "$item_json" | jq -r ".sections[] | select(.id == \"$section\" or .label == \"$section\") | .id") + jq_section_selector=".section.id == \"$section_id\"" + fi + + jq_secret_selector="$jq_section_selector and ($jq_field_selector)" + + echo "Looking for field: $field" + secret_field_json=$(echo "$item_json" | jq -r "first(.fields[] | select($jq_secret_selector))") + + field_type=$(echo "$secret_field_json" | jq -r '.type') + field_purpose=$(echo "$secret_field_json" | jq -r '.purpose') + secret_value=$(echo "$secret_field_json" | jq -r '.value') + + if [ -z "$secret_value" ]; then + echo "Could not find or access secret $ref" + exit 1 + fi + + # If the field is marked as concealed or is a note, register a mask + # for the secret to prevent accidental log exposure. + if [ "$field_type" == "CONCEALED" ] || [ "$field_purpose" == "NOTES" ]; then + # To support multiline secrets, escape percent signs and add a mask per line. + escaped_mask_value=$(echo "$secret_value" | sed -e 's/%/%25/g') + IFS=$'\n' + for line in $escaped_mask_value; do + if [ "${#line}" -lt 3 ]; then + # To avoid false positives and unreadable logs, omit mask for lines that are too short. + continue + fi + echo "::add-mask::$line" + done + unset IFS + fi + + # To support multiline secrets, we'll use the heredoc syntax to populate the environment variables. + # As the heredoc identifier, we'll use a randomly generated 64-character string, + # so that collisions are practically impossible. + random_heredoc_identifier=$(openssl rand -hex 16) + + { + # Populate env var, using heredoc syntax with generated identifier + echo "$env_var<<${random_heredoc_identifier}" + echo "$secret_value" + echo "${random_heredoc_identifier}" + } >> $GITHUB_ENV + + managed_variables+=("$env_var") + done + unset IFS +} + read -r -a managed_variables <<< "$(printenv $managed_variables_var)" if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then @@ -111,7 +235,7 @@ if [ "$auth_type" == "$SERVICE_ACCOUNT" ]; then install_op_cli extract_from_op_env elif [ "$auth_type" == "$CONNECT" ]; then - echo "Fetch via connect" + extract_from_connect fi # Add extra env var that lists which secrets are managed by 1Password so that in a later step From 6c02b8909f89fb32f27a982c97240a7842c8d654 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Wed, 10 Aug 2022 10:23:14 +0300 Subject: [PATCH 13/46] updated configure script --- .github/workflows/test.yml | 2 ++ configure/action.yml | 3 +++ configure/entrypoint.sh | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d33be29..b54b540 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,8 @@ jobs: - name: Load secrets id: load_secrets uses: ./ # 1password/load-secrets-action@ + with: + export-env: true env: SECRET: op://acceptance-tests/test-secret/password SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password diff --git a/configure/action.yml b/configure/action.yml index 3ffe2b6..9b6bf96 100644 --- a/configure/action.yml +++ b/configure/action.yml @@ -6,6 +6,8 @@ inputs: description: Your 1Password Connect instance URL connect-token: description: Token to authenticate to your 1Password Connect instance + service-account-token: + description: Your 1Password service account token runs: using: composite steps: @@ -13,5 +15,6 @@ runs: env: INPUT_CONNECT_HOST: ${{ inputs.connect-host }} INPUT_CONNECT_TOKEN: ${{ inputs.connect-token }} + INPUT_SERVICE_ACCOUNT_TOKEN: ${{ inputs.service-account-token }} run: | ${{ github.action_path }}/entrypoint.sh diff --git a/configure/entrypoint.sh b/configure/entrypoint.sh index a0a4494..571791c 100755 --- a/configure/entrypoint.sh +++ b/configure/entrypoint.sh @@ -14,3 +14,8 @@ OP_CONNECT_TOKEN="${INPUT_CONNECT_TOKEN:-$OP_CONNECT_TOKEN}" if [ -n "$OP_CONNECT_TOKEN" ]; then echo "OP_CONNECT_TOKEN=$OP_CONNECT_TOKEN" >> $GITHUB_ENV fi + +OP_SERVICE_ACCOUNT_TOKEN="${INPUT_SERVICE_ACCOUNT_TOKEN:-$OP_SERVICE_ACCOUNT_TOKEN}" +if [ -n "$OP_SERVICE_ACCOUNT_TOKEN" ]; then + echo "OP_SERVICE_ACCOUNT_TOKEN=$OP_SERVICE_ACCOUNT_TOKEN" >> $GITHUB_ENV +fi From d78889ad20e735a1c7d3ee700c9a30a6d389ced0 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Wed, 10 Aug 2022 10:26:19 +0300 Subject: [PATCH 14/46] updated test.yml --- .github/workflows/test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b54b540..756f7b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,10 +27,6 @@ jobs: SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain - name: Assert test secret values - env: - SECRET: ${{ steps.load_secrets.outputs.SECRET }} - SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} - MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} run: ./tests/assert-env-set.sh test-export-linux: runs-on: ubuntu-latest From b38d01591f006e873fbd4ede324ee1e06df485c3 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Wed, 10 Aug 2022 10:33:04 +0300 Subject: [PATCH 15/46] test should fail cause not existing field provided --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 756f7b1..9738253 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: with: export-env: true env: - SECRET: op://acceptance-tests/test-secret/password + SECRET: op://acceptance-tests/test-secret/not_existing_field SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain - name: Assert test secret values From 9fdfd04a6f953958efba2e8e8df21f1b84b75e96 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Wed, 10 Aug 2022 10:34:38 +0300 Subject: [PATCH 16/46] revert back test change --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9738253..756f7b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: with: export-env: true env: - SECRET: op://acceptance-tests/test-secret/not_existing_field + SECRET: op://acceptance-tests/test-secret/password SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain - name: Assert test secret values From 302881bfef5c8be60401277c5d214f44f31aa080 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Wed, 10 Aug 2022 11:42:31 +0300 Subject: [PATCH 17/46] use correct op cli version fo linux --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index bd511ec..0757466 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -31,7 +31,7 @@ unset_prev_secrets() { # Install op-cli install_op_cli() { if [[ "$OSTYPE" == "linux-gnu"* ]]; then - curl -sSfLo op.zip "https://bucket.agilebits.com/cli-private-beta/v2/op_linux_amd64_v2-alpha2.zip" + curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/op_linux_amd64_v2.6.0-beta.06.zip" elif [[ "$OSTYPE" == "darwin"* ]]; then curl -sSfLo op.zip "https://bucket.agilebits.com/cli-private-beta/v2/op_darwin_amd64_v2-alpha2.zip" fi From bb28cdf0c663c52930873b4b67186033527049ec Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Wed, 10 Aug 2022 11:44:12 +0300 Subject: [PATCH 18/46] added test case for load secrets via op cli --- .github/workflows/test.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 756f7b1..fe6ce9c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,3 +61,21 @@ jobs: unset-previous: true - name: Assert removed secrets run: ./tests/assert-env-unset.sh + test-op-cli-load-secrets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Load secrets + id: load_secrets + uses: ./ # 1password/load-secrets-action@ + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + SECRET: op://acceptance-tests/test-secret/password + SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password + MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain + - name: Assert test secret values + env: + SECRET: ${{ steps.load_secrets.outputs.SECRET }} + SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} + MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} + run: ./tests/assert-env-set.sh From b38b493d73ede6cf52aca068e627093c41aaa2cb Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Wed, 10 Aug 2022 11:45:24 +0300 Subject: [PATCH 19/46] rename tests --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe6ce9c..06d10ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ on: push name: Run acceptance tests jobs: - test-output-linux: + test-connect-output: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -28,7 +28,7 @@ jobs: MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain - name: Assert test secret values run: ./tests/assert-env-set.sh - test-export-linux: + test-connect-export: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 From 858b6a838e6b90e414d324331dff418275b1f93a Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Wed, 10 Aug 2022 12:59:29 +0300 Subject: [PATCH 20/46] added env variables to test secret section --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 06d10ad..6ab8dfd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,10 @@ jobs: SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain - name: Assert test secret values + env: + SECRET: ${{ steps.load_secrets.outputs.SECRET }} + SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} + MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} run: ./tests/assert-env-set.sh test-connect-export: runs-on: ubuntu-latest From 1263fc888c1d7f9791ecce98aaa053fa52c9df49 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Wed, 10 Aug 2022 16:57:06 +0300 Subject: [PATCH 21/46] updated darwin op archive link --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index 0757466..8caf9d9 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -33,7 +33,7 @@ install_op_cli() { if [[ "$OSTYPE" == "linux-gnu"* ]]; then curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/op_linux_amd64_v2.6.0-beta.06.zip" elif [[ "$OSTYPE" == "darwin"* ]]; then - curl -sSfLo op.zip "https://bucket.agilebits.com/cli-private-beta/v2/op_darwin_amd64_v2-alpha2.zip" + curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/1password-cli_v2.6.0-beta.06_darwin_arm64.tar.gz" fi unzip -od /usr/local/bin/ op.zip && rm op.zip } From c7774c7068b5919d25f7ba60a9c4a5303aa9e44b Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Wed, 10 Aug 2022 17:02:15 +0300 Subject: [PATCH 22/46] updated darwin link --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index 8caf9d9..7efa705 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -33,7 +33,7 @@ install_op_cli() { if [[ "$OSTYPE" == "linux-gnu"* ]]; then curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/op_linux_amd64_v2.6.0-beta.06.zip" elif [[ "$OSTYPE" == "darwin"* ]]; then - curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/1password-cli_v2.6.0-beta.06_darwin_arm64.tar.gz" + curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/1password-cli_v2.6.0-beta.06_darwin_amd64.tar.gz" fi unzip -od /usr/local/bin/ op.zip && rm op.zip } From ada03a032597161fc25a3aced8b8003a16c0ea08 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Thu, 11 Aug 2022 16:39:39 +0300 Subject: [PATCH 23/46] use cli to retrieve secrets if signed in via connect --- entrypoint.sh | 133 +------------------------------------------------- 1 file changed, 2 insertions(+), 131 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 7efa705..fcc1963 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -92,130 +92,6 @@ extract_from_op_env() { unset IFS } -# Load environment variables using connect service. Iterate over hem to find 1Password references, load the secret values, -# and make them available as environment variables in the next steps. -extract_from_connect() { - curl_headers=(-H "Content-Type: application/json" -H "Authorization: Bearer $OP_CONNECT_TOKEN") - IFS=$'\n' - - for possible_ref in $(printenv | grep "=op://" | grep -v "^#"); do - env_var=$(echo "$possible_ref" | cut -d '=' -f1) - ref=$(printenv $env_var) - - if [[ ! $ref == "op://"* ]]; then - echo "Not really a reference: $ref" - continue - fi - - path=$(echo $ref | sed -e "s/^op:\/\///") - if [ $(echo "$path" | tr -cd '/' | wc -c) -lt 2 ]; then - echo "Expected path to be in format op:///[/
]/: $ref" - continue - fi - - echo "Populating variable: $env_var" - - vault="" - item="" - section="" - field="" - i=0 - IFS="/" - for component in $path; do - ((i+=1)) - case "$i" in - 1) vault=$component ;; - 2) item=$component ;; - 3) section=$component ;; - 4) field=$component ;; - esac - done - unset IFS - - # If field is not set, it may have wrongfully been interpreted as the section. - if [ -z "$field" ]; then - field="$section" - section="" - fi - - if [[ $(echo -n $(echo $vault | grep "^[a-z0-9]*$") | wc -c) -ne 26 ]]; then - echo "Getting vault ID from vault name: $vault" - vault=$(curl -sSf "${curl_headers[@]}" "$OP_CONNECT_HOST/v1/vaults?filter=name%20eq%20%22$vault%22" | jq -r '.[0] | .id') - if [ -z "$vault" ]; then - echo "Could not find vault ID for vault: $vault" - exit 1 - fi - fi - - if [[ $(echo -n $(echo $item | grep "^[a-z0-9]*$") | wc -c) -ne 26 ]]; then - echo "Getting item ID from vault $vault..." - item=$(curl -sSf "${curl_headers[@]}" "$OP_CONNECT_HOST/v1/vaults/$vault/items?filter=title%20eq%20%22$item%22" | jq -r '.[0] | .id') - if [ -z "$item" ]; then - echo "Could not find item ID for item: $item" - exit 1 - fi - fi - - echo "Loading item $item from vault $vault..." - item_json=$(curl -sSf "${curl_headers[@]}" "$OP_CONNECT_HOST/v1/vaults/$vault/items/$item") - - jq_field_selector=".id == \"$field\" or .label == \"$field\"" - jq_section_selector=".section == null" - - # If the reference contains a section, edit the jq selector to take that into account. - if [ -n "$section" ]; then - echo "Looking for section: $section" - section_id=$(echo "$item_json" | jq -r ".sections[] | select(.id == \"$section\" or .label == \"$section\") | .id") - jq_section_selector=".section.id == \"$section_id\"" - fi - - jq_secret_selector="$jq_section_selector and ($jq_field_selector)" - - echo "Looking for field: $field" - secret_field_json=$(echo "$item_json" | jq -r "first(.fields[] | select($jq_secret_selector))") - - field_type=$(echo "$secret_field_json" | jq -r '.type') - field_purpose=$(echo "$secret_field_json" | jq -r '.purpose') - secret_value=$(echo "$secret_field_json" | jq -r '.value') - - if [ -z "$secret_value" ]; then - echo "Could not find or access secret $ref" - exit 1 - fi - - # If the field is marked as concealed or is a note, register a mask - # for the secret to prevent accidental log exposure. - if [ "$field_type" == "CONCEALED" ] || [ "$field_purpose" == "NOTES" ]; then - # To support multiline secrets, escape percent signs and add a mask per line. - escaped_mask_value=$(echo "$secret_value" | sed -e 's/%/%25/g') - IFS=$'\n' - for line in $escaped_mask_value; do - if [ "${#line}" -lt 3 ]; then - # To avoid false positives and unreadable logs, omit mask for lines that are too short. - continue - fi - echo "::add-mask::$line" - done - unset IFS - fi - - # To support multiline secrets, we'll use the heredoc syntax to populate the environment variables. - # As the heredoc identifier, we'll use a randomly generated 64-character string, - # so that collisions are practically impossible. - random_heredoc_identifier=$(openssl rand -hex 16) - - { - # Populate env var, using heredoc syntax with generated identifier - echo "$env_var<<${random_heredoc_identifier}" - echo "$secret_value" - echo "${random_heredoc_identifier}" - } >> $GITHUB_ENV - - managed_variables+=("$env_var") - done - unset IFS -} - read -r -a managed_variables <<< "$(printenv $managed_variables_var)" if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then @@ -230,13 +106,8 @@ fi printf "Authenticated with %s \n" $auth_type unset_prev_secrets - -if [ "$auth_type" == "$SERVICE_ACCOUNT" ]; then - install_op_cli - extract_from_op_env -elif [ "$auth_type" == "$CONNECT" ]; then - extract_from_connect -fi +install_op_cli +extract_from_op_env # Add extra env var that lists which secrets are managed by 1Password so that in a later step # these can be unset again. From 34bed60a89e6db25eb61e96fcf0205ae28c670b5 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Thu, 11 Aug 2022 18:08:26 +0300 Subject: [PATCH 24/46] use op read to retrive item values --- entrypoint.sh | 214 +++++++++++++------------------------------------- 1 file changed, 56 insertions(+), 158 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 8caf9d9..592fbfe 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -38,171 +38,38 @@ install_op_cli() { unzip -od /usr/local/bin/ op.zip && rm op.zip } -# Load environment variables using op cli. Iterate over them to find 1Password references, load the secret values, -# and make them available as environment variables in the next steps. -extract_from_op_env() { - IFS=$'\n' - for env_var in $(op env ls); do - ref=$(printenv $env_var) - - echo "Populating variable: $env_var" - secret_value=$(op read $ref) +populating_secret() { + ref=$(printenv $1) - if [ -z "$secret_value" ]; then - echo "Could not find or access secret $ref" - exit 1 - fi - - # Register a mask for the secret to prevent accidental log exposure. - # To support multiline secrets, escape percent signs and add a mask per line. - escaped_mask_value=$(echo "$secret_value" | sed -e 's/%/%25/g') - IFS=$'\n' - for line in $escaped_mask_value; do - if [ "${#line}" -lt 3 ]; then - # To avoid false positives and unreadable logs, omit mask for lines that are too short. - continue - fi - echo "::add-mask::$line" - done - unset IFS - - if [ "$INPUT_EXPORT_ENV" == "true" ]; then - # To support multiline secrets, we'll use the heredoc syntax to populate the environment variables. - # As the heredoc identifier, we'll use a randomly generated 64-character string, - # so that collisions are practically impossible. - random_heredoc_identifier=$(openssl rand -hex 16) - - { - # Populate env var, using heredoc syntax with generated identifier - echo "$env_var<<${random_heredoc_identifier}" - echo "$secret_value" - echo "${random_heredoc_identifier}" - } >> $GITHUB_ENV - - else - # Prepare the secret_value to be outputed properly (especially multiline secrets) - secret_value=$(echo "$secret_value" | awk -v ORS='%0A' '1') - - echo "::set-output name=$env_var::$secret_value" - fi + echo "Populating variable: $1" + secret_value=$(op read $ref) + echo "Secret value: $secret_value" - managed_variables+=("$env_var") - - done - unset IFS -} + if [ -z "$secret_value" ]; then + echo "Could not find or access secret $ref" + exit 1 + fi -# Load environment variables using connect service. Iterate over hem to find 1Password references, load the secret values, -# and make them available as environment variables in the next steps. -extract_from_connect() { - curl_headers=(-H "Content-Type: application/json" -H "Authorization: Bearer $OP_CONNECT_TOKEN") + # Register a mask for the secret to prevent accidental log exposure. + # To support multiline secrets, escape percent signs and add a mask per line. + escaped_mask_value=$(echo "$secret_value" | sed -e 's/%/%25/g') IFS=$'\n' - - for possible_ref in $(printenv | grep "=op://" | grep -v "^#"); do - env_var=$(echo "$possible_ref" | cut -d '=' -f1) - ref=$(printenv $env_var) - - if [[ ! $ref == "op://"* ]]; then - echo "Not really a reference: $ref" - continue - fi - - path=$(echo $ref | sed -e "s/^op:\/\///") - if [ $(echo "$path" | tr -cd '/' | wc -c) -lt 2 ]; then - echo "Expected path to be in format op:///[/
]/: $ref" + for line in $escaped_mask_value; do + if [ "${#line}" -lt 3 ]; then + # To avoid false positives and unreadable logs, omit mask for lines that are too short. continue fi + echo "::add-mask::$line" + done + unset IFS - echo "Populating variable: $env_var" - - vault="" - item="" - section="" - field="" - i=0 - IFS="/" - for component in $path; do - ((i+=1)) - case "$i" in - 1) vault=$component ;; - 2) item=$component ;; - 3) section=$component ;; - 4) field=$component ;; - esac - done - unset IFS - - # If field is not set, it may have wrongfully been interpreted as the section. - if [ -z "$field" ]; then - field="$section" - section="" - fi - - if [[ $(echo -n $(echo $vault | grep "^[a-z0-9]*$") | wc -c) -ne 26 ]]; then - echo "Getting vault ID from vault name: $vault" - vault=$(curl -sSf "${curl_headers[@]}" "$OP_CONNECT_HOST/v1/vaults?filter=name%20eq%20%22$vault%22" | jq -r '.[0] | .id') - if [ -z "$vault" ]; then - echo "Could not find vault ID for vault: $vault" - exit 1 - fi - fi - - if [[ $(echo -n $(echo $item | grep "^[a-z0-9]*$") | wc -c) -ne 26 ]]; then - echo "Getting item ID from vault $vault..." - item=$(curl -sSf "${curl_headers[@]}" "$OP_CONNECT_HOST/v1/vaults/$vault/items?filter=title%20eq%20%22$item%22" | jq -r '.[0] | .id') - if [ -z "$item" ]; then - echo "Could not find item ID for item: $item" - exit 1 - fi - fi - - echo "Loading item $item from vault $vault..." - item_json=$(curl -sSf "${curl_headers[@]}" "$OP_CONNECT_HOST/v1/vaults/$vault/items/$item") - - jq_field_selector=".id == \"$field\" or .label == \"$field\"" - jq_section_selector=".section == null" - - # If the reference contains a section, edit the jq selector to take that into account. - if [ -n "$section" ]; then - echo "Looking for section: $section" - section_id=$(echo "$item_json" | jq -r ".sections[] | select(.id == \"$section\" or .label == \"$section\") | .id") - jq_section_selector=".section.id == \"$section_id\"" - fi - - jq_secret_selector="$jq_section_selector and ($jq_field_selector)" - - echo "Looking for field: $field" - secret_field_json=$(echo "$item_json" | jq -r "first(.fields[] | select($jq_secret_selector))") - - field_type=$(echo "$secret_field_json" | jq -r '.type') - field_purpose=$(echo "$secret_field_json" | jq -r '.purpose') - secret_value=$(echo "$secret_field_json" | jq -r '.value') - - if [ -z "$secret_value" ]; then - echo "Could not find or access secret $ref" - exit 1 - fi - - # If the field is marked as concealed or is a note, register a mask - # for the secret to prevent accidental log exposure. - if [ "$field_type" == "CONCEALED" ] || [ "$field_purpose" == "NOTES" ]; then - # To support multiline secrets, escape percent signs and add a mask per line. - escaped_mask_value=$(echo "$secret_value" | sed -e 's/%/%25/g') - IFS=$'\n' - for line in $escaped_mask_value; do - if [ "${#line}" -lt 3 ]; then - # To avoid false positives and unreadable logs, omit mask for lines that are too short. - continue - fi - echo "::add-mask::$line" - done - unset IFS - fi - + if [ "$INPUT_EXPORT_ENV" == "true" ]; then + echo "inside if" # To support multiline secrets, we'll use the heredoc syntax to populate the environment variables. # As the heredoc identifier, we'll use a randomly generated 64-character string, # so that collisions are practically impossible. random_heredoc_identifier=$(openssl rand -hex 16) + echo "random_heredoc_identifier: $random_heredoc_identifier" { # Populate env var, using heredoc syntax with generated identifier @@ -210,12 +77,43 @@ extract_from_connect() { echo "$secret_value" echo "${random_heredoc_identifier}" } >> $GITHUB_ENV + echo "GITHUB_ENV: $(cat $GITHUB_ENV)" - managed_variables+=("$env_var") + else + echo "inside else" + # Prepare the secret_value to be outputed properly (especially multiline secrets) + secret_value=$(echo "$secret_value" | awk -v ORS='%0A' '1') + + echo "::set-output name=$env_var::$secret_value" + fi + + echo "env_var: $env_var" + echo "secret_value: $secret_value" + managed_variables+=("$env_var") +} + +# Load environment variables using op cli. Iterate over them to find 1Password references, load the secret values, +# and make them available as environment variables in the next steps. +extract_using_service_account() { + IFS=$'\n' + for env_var in $(op env ls); do + populating_secret $env_var done unset IFS } +# Load environment variables using connect service. Iterate over hem to find 1Password references, load the secret values, +# and make them available as environment variables in the next steps. +extract_using_connec() { + curl_headers=(-H "Content-Type: application/json" -H "Authorization: Bearer $OP_CONNECT_TOKEN") + IFS=$'\n' + + for possible_ref in $(printenv | grep "=op://" | grep -v "^#"); do + env_var=$(echo "$possible_ref" | cut -d '=' -f1) + populating_secret $env_var + done +} + read -r -a managed_variables <<< "$(printenv $managed_variables_var)" if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then @@ -230,12 +128,12 @@ fi printf "Authenticated with %s \n" $auth_type unset_prev_secrets +install_op_cli if [ "$auth_type" == "$SERVICE_ACCOUNT" ]; then - install_op_cli - extract_from_op_env + extract_using_service_account elif [ "$auth_type" == "$CONNECT" ]; then - extract_from_connect + extract_using_connect fi # Add extra env var that lists which secrets are managed by 1Password so that in a later step From 8052f86fc66b3f9ce6744d1e536fa387dc029156 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Thu, 11 Aug 2022 18:32:47 +0300 Subject: [PATCH 25/46] added additional test cases --- .github/workflows/test.yml | 48 ++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ab8dfd..c740f63 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ on: push name: Run acceptance tests jobs: - test-connect-output: + use-connect-without-export-env: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -20,8 +20,6 @@ jobs: - name: Load secrets id: load_secrets uses: ./ # 1password/load-secrets-action@ - with: - export-env: true env: SECRET: op://acceptance-tests/test-secret/password SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password @@ -32,6 +30,32 @@ jobs: SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} run: ./tests/assert-env-set.sh + use-connect-with-export-env: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Launch 1Password Connect instance + env: + OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }} + run: | + echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json + docker-compose -f tests/fixtures/docker-compose.yml up -d && sleep 10 + - name: Configure 1Password Connect + uses: ./configure # 1password/load-secrets-action/configure@ + with: + connect-host: http://localhost:8080 + connect-token: ${{ secrets.OP_CONNECT_TOKEN }} + - name: Load secrets + id: load_secrets + uses: ./ # 1password/load-secrets-action@ + with: + export-env: true + env: + SECRET: op://acceptance-tests/test-secret/password + SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password + MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain + - name: Assert test secret values + run: ./tests/assert-env-set.sh test-connect-export: runs-on: ubuntu-latest steps: @@ -65,7 +89,7 @@ jobs: unset-previous: true - name: Assert removed secrets run: ./tests/assert-env-unset.sh - test-op-cli-load-secrets: + use-service-account-without-export-env: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -83,3 +107,19 @@ jobs: SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} run: ./tests/assert-env-set.sh + use-service-account-with-export-env: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Load secrets + id: load_secrets + uses: ./ # 1password/load-secrets-action@ + with: + export-env: true + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + SECRET: op://acceptance-tests/test-secret/password + SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password + MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain + - name: Assert test secret values + run: ./tests/assert-env-set.sh From df0b228eb9bb3cb5287f0470303c8c565228874b Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Thu, 11 Aug 2022 18:33:04 +0300 Subject: [PATCH 26/46] removed logs to console --- entrypoint.sh | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 592fbfe..8dd07ca 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -43,7 +43,6 @@ populating_secret() { echo "Populating variable: $1" secret_value=$(op read $ref) - echo "Secret value: $secret_value" if [ -z "$secret_value" ]; then echo "Could not find or access secret $ref" @@ -64,12 +63,10 @@ populating_secret() { unset IFS if [ "$INPUT_EXPORT_ENV" == "true" ]; then - echo "inside if" # To support multiline secrets, we'll use the heredoc syntax to populate the environment variables. # As the heredoc identifier, we'll use a randomly generated 64-character string, # so that collisions are practically impossible. random_heredoc_identifier=$(openssl rand -hex 16) - echo "random_heredoc_identifier: $random_heredoc_identifier" { # Populate env var, using heredoc syntax with generated identifier @@ -80,15 +77,12 @@ populating_secret() { echo "GITHUB_ENV: $(cat $GITHUB_ENV)" else - echo "inside else" # Prepare the secret_value to be outputed properly (especially multiline secrets) secret_value=$(echo "$secret_value" | awk -v ORS='%0A' '1') echo "::set-output name=$env_var::$secret_value" fi - echo "env_var: $env_var" - echo "secret_value: $secret_value" managed_variables+=("$env_var") } @@ -104,7 +98,7 @@ extract_using_service_account() { # Load environment variables using connect service. Iterate over hem to find 1Password references, load the secret values, # and make them available as environment variables in the next steps. -extract_using_connec() { +extract_using_connect() { curl_headers=(-H "Content-Type: application/json" -H "Authorization: Bearer $OP_CONNECT_TOKEN") IFS=$'\n' From 2a214a29d302dece6a171dfc0b6c88ebe6099544 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Thu, 11 Aug 2022 18:38:45 +0300 Subject: [PATCH 27/46] unset IFS at the end of the flow --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index 6cad026..1de72ce 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -93,7 +93,6 @@ extract_using_service_account() { for env_var in $(op env ls); do populating_secret $env_var done - unset IFS } # Load environment variables using connect service. Iterate over hem to find 1Password references, load the secret values, @@ -129,6 +128,7 @@ elif [ "$auth_type" == "$CONNECT" ]; then extract_using_connect fi +unset IFS # Add extra env var that lists which secrets are managed by 1Password so that in a later step # these can be unset again. managed_variables_str=$(IFS=','; echo "${managed_variables[*]}") From e1b37a5b1e835dddb2d2cbe5120d8ae0be0f6d28 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Thu, 11 Aug 2022 18:58:35 +0300 Subject: [PATCH 28/46] updated README and configuration.yml --- README.md | 18 ++++++++++++++---- configure/action.yml | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5351238..f28ec62 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,14 @@ Specify right from your workflow YAML which secrets from 1Password should be loa ## Usage +You can configure the action to use either 1Password connect instance or service account. + +If provide `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` variables - connect instance will be used to load secrets. + +If provide `OP_SERVICE_ACCOUNT_TOKEN` variable - service account will be used to load secrets. + +***Note***: if all variables are provided the `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` environment variables will take precedence over `OP_SERVICE_ACCOUNT_TOKEN`. Clear the Connect environment variables to configure a service account instead. + There are two ways that secrets can be loaded: - [use the secrets from the action's ouput](#use-secrets-from-the-actions-output) - [export secrets as environment variables](#export-secrets-as-environment-variables) @@ -199,10 +207,11 @@ So for example, the reference URI `op://app-cicd/aws/secret-access-key` would be Similar to regular GitHub repository secrets, fields from 1Password will automatically be masked from the GitHub Actions logs too. So if one of these values accidentally gets printed, it'll get replaced with `***`. -## 1Password Connect Configuration +## 1Password Configuration -To use the action, you need to have a [1Password Connect](https://support.1password.com/secrets-automation/#step-1-set-up-a-secrets-automation-workflow) instance deployed somewhere. -To configure the action with your Connect URL and a Connect token, you can set the `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` variables. +To use the action, you may need to have a [1Password Connect](https://support.1password.com/secrets-automation/#step-1-set-up-a-secrets-automation-workflow) instance deployed somewhere. +To configure the action with your Connect URL and a Connect token, you can set the `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` variables. +If you provide `OP_SERVICE_ACCOUNT_TOKEN` you can skip connect setup as action will use your service account instead. If you're using the `load-secrets` action more than once in a single job, you can use the `configure` action to avoid duplicate configuration: @@ -219,7 +228,7 @@ jobs: with: connect-host: connect-token: ${{ secrets.OP_CONNECT_TOKEN }} - + service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - name: Load secret uses: 1password/load-secrets-action@v1 env: @@ -232,6 +241,7 @@ jobs: |---|---|---|---| | `connect-host` | | `OP_CONNECT_HOST` | Your 1Password Connect instance URL | | `connect-token` | | `OP_CONNECT_TOKEN` | Token to authenticate to your 1Password Connect instance | +| `service-account-token` | | `OP_SERVICE_ACCOUNT_TOKEN` | Your 1Password service account token | ## Supported Runners diff --git a/configure/action.yml b/configure/action.yml index 9b6bf96..40ab9eb 100644 --- a/configure/action.yml +++ b/configure/action.yml @@ -1,5 +1,5 @@ -name: Configure 1Password Connect -description: Persist 1Password Connect host and token for use in next steps. +name: Configure 1Password Connect and service account +description: Persist 1Password Connect host, token and service account for use in next steps. author: 1Password inputs: connect-host: From 5c5bbcbaf033a38eccf646cb868380999d069c6b Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Mon, 15 Aug 2022 13:32:42 +0300 Subject: [PATCH 29/46] prevent command injection vulnerability --- dist/index.js | 9 +++------ src/index.ts | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/dist/index.js b/dist/index.js index e3d7f39..cbda727 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1935,13 +1935,10 @@ function run() { try { const parentDir = path__WEBPACK_IMPORTED_MODULE_2___default().resolve(__dirname, '..'); // Get action inputs - const unsetPrevious = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('unset-previous'); - const exportEnv = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('export-env'); + process.env.INPUT_UNSET_PREVIOUS = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('unset-previous'); + process.env.INPUT_EXPORT_ENV = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('export-env'); // Execute bash script - yield _actions_exec__WEBPACK_IMPORTED_MODULE_1__.exec(`sh -c "` + - `INPUT_UNSET_PREVIOUS=` + unsetPrevious + ` ` + - `INPUT_EXPORT_ENV=` + exportEnv + ` ` + - parentDir + `/entrypoint.sh"`); + yield _actions_exec__WEBPACK_IMPORTED_MODULE_1__.exec(`sh -c "` + parentDir + `/entrypoint.sh"`); } catch (error) { _actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed(error.message); diff --git a/src/index.ts b/src/index.ts index 855332d..0a60ed8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,14 +7,11 @@ async function run(): Promise { const parentDir = path.resolve(__dirname, '..'); // Get action inputs - const unsetPrevious = core.getInput('unset-previous'); - const exportEnv = core.getInput('export-env'); + process.env.INPUT_UNSET_PREVIOUS = core.getInput('unset-previous'); + process.env.INPUT_EXPORT_ENV = core.getInput('export-env'); // Execute bash script - await exec.exec(`sh -c "` + - `INPUT_UNSET_PREVIOUS=` + unsetPrevious + ` ` + - `INPUT_EXPORT_ENV=` + exportEnv + ` ` + - parentDir + `/entrypoint.sh"`); + await exec.exec(`sh -c "` + parentDir + `/entrypoint.sh"`); } catch (error: any) { core.setFailed(error.message); From 8d99fc2a1e60f257c71b8c7451dffeedf5f975d1 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Mon, 15 Aug 2022 14:17:44 +0300 Subject: [PATCH 30/46] properly unpack tar.gz --- entrypoint.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 1de72ce..6210061 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -32,10 +32,11 @@ unset_prev_secrets() { install_op_cli() { if [[ "$OSTYPE" == "linux-gnu"* ]]; then curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/op_linux_amd64_v2.6.0-beta.06.zip" + unzip -od ./op op.zip && rm op.zip elif [[ "$OSTYPE" == "darwin"* ]]; then - curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/1password-cli_v2.6.0-beta.06_darwin_amd64.tar.gz" + curl -sSfLo op.tar.gz "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/1password-cli_v2.6.0-beta.06_darwin_amd64.tar.gz" + mkdir ./op && tar -xf op.tar.gz -C ./op fi - unzip -od /usr/local/bin/ op.zip && rm op.zip } populating_secret() { From d5280efa32d2cd2ba524b8b05276caa9336f5fc1 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Mon, 15 Aug 2022 14:17:50 +0300 Subject: [PATCH 31/46] updated README.md --- README.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f28ec62..ea23e93 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,19 @@ # Load Secrets from 1Password - GitHub Action -This action loads secrets from [1Password Connect](https://1password.com/secrets/) into GitHub Actions. +This action loads secrets from 1Password into GitHub Actions using [1Password Connect](https://1password.com/secrets/) or a Service Account. Specify right from your workflow YAML which secrets from 1Password should be loaded into your job, and the action will make them available as environment variables for the next steps. -## Prerequisites - - [1Password Connect](https://support.1password.com/secrets-automation/#step-2-deploy-a-1password-connect-server) deployed in your infrastructure - ## Usage -You can configure the action to use either 1Password connect instance or service account. +You can configure the action to use either 1Password Connect instance or service account. -If provide `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` variables - connect instance will be used to load secrets. +If you provide `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` variables, the Connect instance will be used to load secrets. Make sure [1Password Connect](https://support.1password.com/secrets-automation/#step-2-deploy-a-1password-connect-server) deployed in your infrastructure. -If provide `OP_SERVICE_ACCOUNT_TOKEN` variable - service account will be used to load secrets. +If you provide `OP_SERVICE_ACCOUNT_TOKEN` variable, the service account will be used to load secrets. -***Note***: if all variables are provided the `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` environment variables will take precedence over `OP_SERVICE_ACCOUNT_TOKEN`. Clear the Connect environment variables to configure a service account instead. +***Note***: if all environment variables are provided, the GitHub action will use Connect over the service account. Clear the Connect environment variables to make the action use a service account instead. There are two ways that secrets can be loaded: - [use the secrets from the action's ouput](#use-secrets-from-the-actions-output) @@ -209,9 +206,9 @@ So if one of these values accidentally gets printed, it'll get replaced with `** ## 1Password Configuration -To use the action, you may need to have a [1Password Connect](https://support.1password.com/secrets-automation/#step-1-set-up-a-secrets-automation-workflow) instance deployed somewhere. -To configure the action with your Connect URL and a Connect token, you can set the `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` variables. -If you provide `OP_SERVICE_ACCOUNT_TOKEN` you can skip connect setup as action will use your service account instead. +To use the action with Connect, you need to have a [1Password Connect](https://support.1password.com/secrets-automation/#step-1-set-up-a-secrets-automation-workflow) instance deployed somewhere. +To configure the action with your Connect host and token, set the `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` environment variables. +To configure the action with your service account token, set the `OP_SERVICE_ACCOUNT_TOKEN` environment variable. If you're using the `load-secrets` action more than once in a single job, you can use the `configure` action to avoid duplicate configuration: From e9bd76c87a80a5e8e6a83b48a3a379e80deddf65 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Mon, 15 Aug 2022 14:33:00 +0300 Subject: [PATCH 32/46] removed test-connect-export case --- .github/workflows/test.yml | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c740f63..f473378 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,33 +56,6 @@ jobs: MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain - name: Assert test secret values run: ./tests/assert-env-set.sh - test-connect-export: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Launch 1Password Connect instance - env: - OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }} - run: | - echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json - docker-compose -f tests/fixtures/docker-compose.yml up -d && sleep 10 - - name: Configure 1Password Connect - uses: ./configure # 1password/load-secrets-action/configure@ - with: - connect-host: http://localhost:8080 - connect-token: ${{ secrets.OP_CONNECT_TOKEN }} - - name: Load secrets - uses: ./ # 1password/load-secrets-action@ - with: - export-env: true - env: - SECRET: op://acceptance-tests/test-secret/password - SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password - MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain - - name: Print environment variables with masked secrets - run: printenv - - name: Assert test secret values - run: ./tests/assert-env-set.sh - name: Remove secrets uses: ./ # 1password/load-secrets-action@ with: From 2f338e16afa49b9e01201af244c09a48e2e7534d Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Mon, 15 Aug 2022 14:33:11 +0300 Subject: [PATCH 33/46] use node16 --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index bd337e4..0736f03 100644 --- a/action.yml +++ b/action.yml @@ -12,5 +12,5 @@ inputs: description: Export the secrets as environment variables default: false runs: - using: 'node12' + using: 'node16' main: 'dist/index.js' From 953b51736f14122b9d5441a256ca99da3157d01b Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Mon, 15 Aug 2022 17:20:19 +0300 Subject: [PATCH 34/46] extract op to proper path --- entrypoint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 6210061..2658d61 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -32,10 +32,10 @@ unset_prev_secrets() { install_op_cli() { if [[ "$OSTYPE" == "linux-gnu"* ]]; then curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/op_linux_amd64_v2.6.0-beta.06.zip" - unzip -od ./op op.zip && rm op.zip + unzip -od /usr/local/bin/ op.zip && rm op.zip elif [[ "$OSTYPE" == "darwin"* ]]; then curl -sSfLo op.tar.gz "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/1password-cli_v2.6.0-beta.06_darwin_amd64.tar.gz" - mkdir ./op && tar -xf op.tar.gz -C ./op + tar -xf op.tar.gz -C /usr/local/bin/ && rm op.tar.gz fi } From c27a045581764c5dea7808d51b3bc4af74ed7475 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Mon, 15 Aug 2022 17:22:28 +0300 Subject: [PATCH 35/46] use references that has ids instead of names --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f473378..0533ae2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,9 +21,9 @@ jobs: id: load_secrets uses: ./ # 1password/load-secrets-action@ env: - SECRET: op://acceptance-tests/test-secret/password - SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password - MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain + SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password + SECRET_IN_SECTION: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/test-section/password + UNMASKED_VALUE: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/test-section/username - name: Assert test secret values env: SECRET: ${{ steps.load_secrets.outputs.SECRET }} From 641b0d93ecc74987234142493d2f62a9b30cb242 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Mon, 15 Aug 2022 18:14:55 +0300 Subject: [PATCH 36/46] use ids for multiline secret --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0533ae2..11c7c57 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,9 +21,9 @@ jobs: id: load_secrets uses: ./ # 1password/load-secrets-action@ env: - SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password - SECRET_IN_SECTION: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/test-section/password - UNMASKED_VALUE: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/test-section/username + SECRET: op://acceptance-tests/test-secret/password + SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password + MULTILINE_SECRET: op://psbfcqkcx5eup45paydr4f7zsu/c7nxmxceicjdevotw3gpxcd354/dphnoeos3nrynixqcp7kbnukzm/username - name: Assert test secret values env: SECRET: ${{ steps.load_secrets.outputs.SECRET }} From 2ac8886444005090564ee3372a6fde181def0dba Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Mon, 15 Aug 2022 18:18:33 +0300 Subject: [PATCH 37/46] fixed multiline secret ref --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11c7c57..f1828d8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: env: SECRET: op://acceptance-tests/test-secret/password SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password - MULTILINE_SECRET: op://psbfcqkcx5eup45paydr4f7zsu/c7nxmxceicjdevotw3gpxcd354/dphnoeos3nrynixqcp7kbnukzm/username + MULTILINE_SECRET: op://psbfcqkcx5eup45paydr4f7zsu/dphnoeos3nrynixqcp7kbnukzm/notesPlain - name: Assert test secret values env: SECRET: ${{ steps.load_secrets.outputs.SECRET }} From ce8b31d0b999a11b34497f881c0cdf1deba9ef30 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Mon, 15 Aug 2022 18:21:18 +0300 Subject: [PATCH 38/46] added more secrets to test --- .github/workflows/test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f1828d8..3b06476 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,13 +22,17 @@ jobs: uses: ./ # 1password/load-secrets-action@ env: SECRET: op://acceptance-tests/test-secret/password + SECRET2: op://psbfcqkcx5eup45paydr4f7zsu/c7nxmxceicjdevotw3gpxcd354/password SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password - MULTILINE_SECRET: op://psbfcqkcx5eup45paydr4f7zsu/dphnoeos3nrynixqcp7kbnukzm/notesPlain + MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain + MULTILINE_SECRET2: op://psbfcqkcx5eup45paydr4f7zsu/dphnoeos3nrynixqcp7kbnukzm/notesPlain - name: Assert test secret values env: SECRET: ${{ steps.load_secrets.outputs.SECRET }} + SECRET2: ${{ steps.load_secrets.outputs.SECRET2 }} SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} + MULTILINE_SECRET2: ${{ steps.load_secrets.outputs.MULTILINE_SECRET2 }} run: ./tests/assert-env-set.sh use-connect-with-export-env: runs-on: ubuntu-latest From 7d858c7ad5871b555aef7cbba7221701ce47ff4b Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Tue, 16 Aug 2022 13:38:07 +0100 Subject: [PATCH 39/46] Make dedicated tests for secret references with IDs The item and vault IDs are changed as well. --- .github/workflows/test.yml | 55 ++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b06476..f36e7df 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,24 +15,20 @@ jobs: - name: Configure 1Password Connect uses: ./configure # 1password/load-secrets-action/configure@ with: - connect-host: http://localhost:8080 + connect-host: http://localhost:8080 connect-token: ${{ secrets.OP_CONNECT_TOKEN }} - name: Load secrets id: load_secrets uses: ./ # 1password/load-secrets-action@ env: SECRET: op://acceptance-tests/test-secret/password - SECRET2: op://psbfcqkcx5eup45paydr4f7zsu/c7nxmxceicjdevotw3gpxcd354/password SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain - MULTILINE_SECRET2: op://psbfcqkcx5eup45paydr4f7zsu/dphnoeos3nrynixqcp7kbnukzm/notesPlain - name: Assert test secret values env: SECRET: ${{ steps.load_secrets.outputs.SECRET }} - SECRET2: ${{ steps.load_secrets.outputs.SECRET2 }} SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} - MULTILINE_SECRET2: ${{ steps.load_secrets.outputs.MULTILINE_SECRET2 }} run: ./tests/assert-env-set.sh use-connect-with-export-env: runs-on: ubuntu-latest @@ -47,7 +43,7 @@ jobs: - name: Configure 1Password Connect uses: ./configure # 1password/load-secrets-action/configure@ with: - connect-host: http://localhost:8080 + connect-host: http://localhost:8080 connect-token: ${{ secrets.OP_CONNECT_TOKEN }} - name: Load secrets id: load_secrets @@ -66,6 +62,34 @@ jobs: unset-previous: true - name: Assert removed secrets run: ./tests/assert-env-unset.sh + use-connect-with-references-with-id: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Launch 1Password Connect instance + env: + OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }} + run: | + echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json + docker-compose -f tests/fixtures/docker-compose.yml up -d && sleep 10 + - name: Configure 1Password Connect + uses: ./configure # 1password/load-secrets-action/configure@ + with: + connect-host: http://localhost:8080 + connect-token: ${{ secrets.OP_CONNECT_TOKEN }} + - name: Load secrets + id: load_secrets + uses: ./ # 1password/load-secrets-action@ + env: + SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password + SECRET_IN_SECTION: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/Section_tco6nsqycj6jcbyx63h5isxcny/doxu3mhkozcznnk5vjrkpdqayy + MULTILINE_SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain + - name: Assert test secret values + env: + SECRET: ${{ steps.load_secrets.outputs.SECRET }} + SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} + MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} + run: ./tests/assert-env-set.sh use-service-account-without-export-env: runs-on: ubuntu-latest steps: @@ -74,7 +98,6 @@ jobs: id: load_secrets uses: ./ # 1password/load-secrets-action@ env: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} SECRET: op://acceptance-tests/test-secret/password SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain @@ -94,9 +117,25 @@ jobs: with: export-env: true env: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} SECRET: op://acceptance-tests/test-secret/password SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain - name: Assert test secret values run: ./tests/assert-env-set.sh + use-service-account-with-references-with-id: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Load secrets + id: load_secrets + uses: ./ # 1password/load-secrets-action@ + env: + SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password + SECRET_IN_SECTION: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/Section_tco6nsqycj6jcbyx63h5isxcny/doxu3mhkozcznnk5vjrkpdqayy + MULTILINE_SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain + - name: Assert test secret values + env: + SECRET: ${{ steps.load_secrets.outputs.SECRET }} + SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} + MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} + run: ./tests/assert-env-set.sh From c53c263a7ef5053a9288414ca8accd2c436f468f Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Tue, 16 Aug 2022 14:05:33 +0100 Subject: [PATCH 40/46] Add service account token in env It was accidentally removed --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f36e7df..f30528b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -98,6 +98,7 @@ jobs: id: load_secrets uses: ./ # 1password/load-secrets-action@ env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} SECRET: op://acceptance-tests/test-secret/password SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain @@ -117,6 +118,7 @@ jobs: with: export-env: true env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} SECRET: op://acceptance-tests/test-secret/password SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain @@ -130,6 +132,7 @@ jobs: id: load_secrets uses: ./ # 1password/load-secrets-action@ env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password SECRET_IN_SECTION: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/Section_tco6nsqycj6jcbyx63h5isxcny/doxu3mhkozcznnk5vjrkpdqayy MULTILINE_SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain From aaee1916c63e69783de0cb3eea6d45afc7e22503 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Wed, 17 Aug 2022 18:07:07 +0300 Subject: [PATCH 41/46] updated README --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ea23e93..5f4ba66 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ Specify right from your workflow YAML which secrets from 1Password should be loa ## Usage -You can configure the action to use either 1Password Connect instance or service account. +You can configure the action to use either 1Password Connect instance or a 1Password Service Account. Service Accounts are currently in Beta and are only available to select users. If you provide `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` variables, the Connect instance will be used to load secrets. Make sure [1Password Connect](https://support.1password.com/secrets-automation/#step-2-deploy-a-1password-connect-server) deployed in your infrastructure. If you provide `OP_SERVICE_ACCOUNT_TOKEN` variable, the service account will be used to load secrets. -***Note***: if all environment variables are provided, the GitHub action will use Connect over the service account. Clear the Connect environment variables to make the action use a service account instead. +***Note***: If all environment variables have been set, the Connect credentials will take precedence over the provided service account token. You must unset the Connect environment variables to ensure the action uses the service account token. There are two ways that secrets can be loaded: - [use the secrets from the action's ouput](#use-secrets-from-the-actions-output) @@ -21,7 +21,7 @@ There are two ways that secrets can be loaded: ### Use secrets from the action's output -This approach enables the user to use the loaded secrets as an output from the step: `steps.step-id.outputs.secret-name`. You need to set an id for the step that uses this action to be able to access its outputs. More details about the metadata syntax [here](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputsoutput_id). +This method allows for you to use the loaded secrets as an output from the step: `steps.step-id.outputs.secret-name`. You will need to set an id for the step that uses this action to be able to access its outputs. More details about the metadata syntax [here](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputsoutput_id). ```yml on: push @@ -89,7 +89,7 @@ jobs: ### Export secrets as environment variables -In this approach, the user can access the loaded secrets as environment variables. These environment variables are accessible at a job level. +This method, allows the action to access the loaded secrets as environment variables. These environment variables are accessible at a job level. ```yml on: push @@ -208,7 +208,8 @@ So if one of these values accidentally gets printed, it'll get replaced with `** To use the action with Connect, you need to have a [1Password Connect](https://support.1password.com/secrets-automation/#step-1-set-up-a-secrets-automation-workflow) instance deployed somewhere. To configure the action with your Connect host and token, set the `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` environment variables. -To configure the action with your service account token, set the `OP_SERVICE_ACCOUNT_TOKEN` environment variable. +To configure the action with your service account token, set the `OP_SERVICE_ACCOUNT_TOKEN` environment variable. +*** Note: *** Service Accounts are currently in Beta and are only available to select users. If you're using the `load-secrets` action more than once in a single job, you can use the `configure` action to avoid duplicate configuration: From b1b82d73849150c7b90c17928413592ee17faac5 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Fri, 19 Aug 2022 11:48:08 +0300 Subject: [PATCH 42/46] updated gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c2658d7..bf74325 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ node_modules/ +.vscode +.idea +.secrets From ed1f9a48af5f3c301f81005536073febab6bed93 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Fri, 19 Aug 2022 12:42:34 +0300 Subject: [PATCH 43/46] use op cli to fetch secrets for connect --- entrypoint.sh | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 2658d61..8c6f91f 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -31,7 +31,7 @@ unset_prev_secrets() { # Install op-cli install_op_cli() { if [[ "$OSTYPE" == "linux-gnu"* ]]; then - curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/op_linux_amd64_v2.6.0-beta.06.zip" + curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.7.1-beta.01/op_linux_amd64_v2.7.1-beta.01.zip" unzip -od /usr/local/bin/ op.zip && rm op.zip elif [[ "$OSTYPE" == "darwin"* ]]; then curl -sSfLo op.tar.gz "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/1password-cli_v2.6.0-beta.06_darwin_amd64.tar.gz" @@ -89,24 +89,13 @@ populating_secret() { # Load environment variables using op cli. Iterate over them to find 1Password references, load the secret values, # and make them available as environment variables in the next steps. -extract_using_service_account() { +extract_secrets() { IFS=$'\n' for env_var in $(op env ls); do populating_secret $env_var done } -# Load environment variables using connect service. Iterate over hem to find 1Password references, load the secret values, -# and make them available as environment variables in the next steps. -extract_using_connect() { - IFS=$'\n' - - for possible_ref in $(printenv | grep "=op://" | grep -v "^#"); do - env_var=$(echo "$possible_ref" | cut -d '=' -f1) - populating_secret $env_var - done -} - read -r -a managed_variables <<< "$(printenv $managed_variables_var)" if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then @@ -122,12 +111,7 @@ printf "Authenticated with %s \n" $auth_type unset_prev_secrets install_op_cli - -if [ "$auth_type" == "$SERVICE_ACCOUNT" ]; then - extract_using_service_account -elif [ "$auth_type" == "$CONNECT" ]; then - extract_using_connect -fi +extract_secrets unset IFS # Add extra env var that lists which secrets are managed by 1Password so that in a later step From 30def81a24fd6466e6288dbd4a82fb2a5d05bba3 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Fri, 19 Aug 2022 12:52:57 +0300 Subject: [PATCH 44/46] added macos installer reference --- entrypoint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 8c6f91f..38a9a2a 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -34,8 +34,8 @@ install_op_cli() { curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.7.1-beta.01/op_linux_amd64_v2.7.1-beta.01.zip" unzip -od /usr/local/bin/ op.zip && rm op.zip elif [[ "$OSTYPE" == "darwin"* ]]; then - curl -sSfLo op.tar.gz "https://cache.agilebits.com/dist/1P/op2/pkg/v2.6.0-beta.06/1password-cli_v2.6.0-beta.06_darwin_amd64.tar.gz" - tar -xf op.tar.gz -C /usr/local/bin/ && rm op.tar.gz + curl -sSfLo op.pkg "https://cache.agilebits.com/dist/1P/op2/pkg/v2.7.1-beta.01/op_apple_universal_v2.7.1-beta.01.pkg" + sudo installer -pkg op.pkg -target /usr/local/bin/ && rm op.pkg fi } From e3b137e0075566080dd300b72c18170ceff32e16 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Fri, 19 Aug 2022 12:54:02 +0300 Subject: [PATCH 45/46] added macos test case --- .github/workflows/test.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f30528b..c5b5f5c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -142,3 +142,21 @@ jobs: SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} run: ./tests/assert-env-set.sh + run-on-macos-12: + runs-on: macos-12 + steps: + - uses: actions/checkout@v2 + - name: Load secrets + id: load_secrets + uses: ./ # 1password/load-secrets-action@ + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + SECRET: op://acceptance-tests/test-secret/password + SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password + MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain + - name: Assert test secret values + env: + SECRET: ${{ steps.load_secrets.outputs.SECRET }} + SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} + MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} + run: ./tests/assert-env-set.sh From cb83ae04bfbbf301095bfddbf5b77c742e57b0a2 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Tue, 30 Aug 2022 10:51:22 +0300 Subject: [PATCH 46/46] improved readme --- .gitignore | 3 --- README.md | 55 ++++++++++++++++++++++++++++-------------------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index bf74325..c2658d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1 @@ node_modules/ -.vscode -.idea -.secrets diff --git a/README.md b/README.md index 5f4ba66..e8005a7 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,26 @@ This action loads secrets from 1Password into GitHub Actions using [1Password Connect](https://1password.com/secrets/) or a Service Account. - Specify right from your workflow YAML which secrets from 1Password should be loaded into your job, and the action will make them available as environment variables for the next steps. ## Usage You can configure the action to use either 1Password Connect instance or a 1Password Service Account. Service Accounts are currently in Beta and are only available to select users. -If you provide `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` variables, the Connect instance will be used to load secrets. Make sure [1Password Connect](https://support.1password.com/secrets-automation/#step-2-deploy-a-1password-connect-server) deployed in your infrastructure. +If you provide `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` variables, the Connect instance will be used to load secrets. Make sure [1Password Connect](https://support.1password.com/secrets-automation/#step-2-deploy-a-1password-connect-server) is deployed in your infrastructure. If you provide `OP_SERVICE_ACCOUNT_TOKEN` variable, the service account will be used to load secrets. -***Note***: If all environment variables have been set, the Connect credentials will take precedence over the provided service account token. You must unset the Connect environment variables to ensure the action uses the service account token. +**_Note_**: If all environment variables have been set, the Connect credentials will take precedence over the provided service account token. You must unset the Connect environment variables to ensure the action uses the service account token. There are two ways that secrets can be loaded: - - [use the secrets from the action's ouput](#use-secrets-from-the-actions-output) - - [export secrets as environment variables](#export-secrets-as-environment-variables) + +- [use the secrets from the action's ouput](#use-secrets-from-the-actions-output) +- [export secrets as environment variables](#export-secrets-as-environment-variables) ### Use secrets from the action's output -This method allows for you to use the loaded secrets as an output from the step: `steps.step-id.outputs.secret-name`. You will need to set an id for the step that uses this action to be able to access its outputs. More details about the metadata syntax [here](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputsoutput_id). +This method allows for you to use the loaded secrets as an output from the step: `steps.step-id.outputs.secret-name`. You will need to set an id for the step that uses this action to be able to access its outputs. For more details, , see [`outputs.`](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputsoutput_id). ```yml on: push @@ -60,8 +60,8 @@ jobs: - name: Configure 1Password Connect uses: 1password/load-secrets-action/configure@v1 with: - # Persist the 1Password Connect URL for next steps. You can also persist - # the Connect token using input `connect-token`, but keep in mind that + # Persist the 1Password Connect URL for next steps. You can also persist + # the Connect token using input `connect-token`, but keep in mind that # every single step in the job would then be able to access the token. connect-host: https://1password.acme.com @@ -85,6 +85,7 @@ jobs: push: true tags: acme/app:latest ``` + ### Export secrets as environment variables @@ -113,6 +114,7 @@ jobs: run: echo "Secret: $SECRET" # Prints: Secret: *** ``` +
Longer usage example @@ -129,8 +131,8 @@ jobs: - name: Configure 1Password Connect uses: 1password/load-secrets-action/configure@v1 with: - # Persist the 1Password Connect URL for next steps. You can also persist - # the Connect token using input `connect-token`, but keep in mind that + # Persist the 1Password Connect URL for next steps. You can also persist + # the Connect token using input `connect-token`, but keep in mind that # every single step in the job would then be able to access the token. connect-host: https://1password.acme.com @@ -172,17 +174,18 @@ jobs: AWS_SECRET_ACCESS_KEY: op://app-cicd/aws/secret-access-key - name: Deploy app - # This script expects AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to be set, which was + # This script expects AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to be set, which was # done automatically by the step above run: ./deploy.sh ``` +
## Action Inputs -| Name | Default | Description | -|---|---|---| -| `export-env` | `false` | Export the loaded secrets as environment variables | +| Name | Default | Description | +| ---------------- | ------- | ---------------------------------------------------------------------------------- | +| `export-env` | `false` | Export the loaded secrets as environment variables | | `unset-previous` | `false` | Whether to unset environment variables populated by 1Password in earlier job steps | ## Secrets Reference Syntax @@ -194,10 +197,11 @@ These reference URIs have the following syntax: > `op:///[/
]/` So for example, the reference URI `op://app-cicd/aws/secret-access-key` would be interpreted as: - * **Vault:** `app-cicd` - * **Item:** `aws` - * **Section:** default section - * **Field:** `secret-access-key` + +- **Vault:** `app-cicd` +- **Item:** `aws` +- **Section:** default section +- **Field:** `secret-access-key` ## Masking @@ -208,8 +212,8 @@ So if one of these values accidentally gets printed, it'll get replaced with `** To use the action with Connect, you need to have a [1Password Connect](https://support.1password.com/secrets-automation/#step-1-set-up-a-secrets-automation-workflow) instance deployed somewhere. To configure the action with your Connect host and token, set the `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` environment variables. -To configure the action with your service account token, set the `OP_SERVICE_ACCOUNT_TOKEN` environment variable. -*** Note: *** Service Accounts are currently in Beta and are only available to select users. +To configure the action with your service account token, set the `OP_SERVICE_ACCOUNT_TOKEN` environment variable. +**_ Note: _** Service Accounts are currently in Beta and are only available to select users. If you're using the `load-secrets` action more than once in a single job, you can use the `configure` action to avoid duplicate configuration: @@ -226,7 +230,6 @@ jobs: with: connect-host: connect-token: ${{ secrets.OP_CONNECT_TOKEN }} - service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - name: Load secret uses: 1password/load-secrets-action@v1 env: @@ -235,11 +238,11 @@ jobs: ### `configure` Action Inputs -| Name | Default | Environment variable | Description | -|---|---|---|---| -| `connect-host` | | `OP_CONNECT_HOST` | Your 1Password Connect instance URL | -| `connect-token` | | `OP_CONNECT_TOKEN` | Token to authenticate to your 1Password Connect instance | -| `service-account-token` | | `OP_SERVICE_ACCOUNT_TOKEN` | Your 1Password service account token | +| Name | Default | Environment variable | Description | +| ----------------------- | ------- | -------------------------- | -------------------------------------------------------- | +| `connect-host` | | `OP_CONNECT_HOST` | Your 1Password Connect instance URL | +| `connect-token` | | `OP_CONNECT_TOKEN` | Token to authenticate to your 1Password Connect instance | +| `service-account-token` | | `OP_SERVICE_ACCOUNT_TOKEN` | Your 1Password service account token | ## Supported Runners