diff --git a/README.md b/README.md index f38bfe5..8e90fe4 100644 --- a/README.md +++ b/README.md @@ -3,39 +3,84 @@ A [Buildkite plugin](https://buildkite.com/docs/agent/v3/plugins) to allow pipeline steps to easily install private packages from an [npm](https://www.npmjs.com) repository. -Note this plugin should work equally well despite your personal preferences for either `yarn` or `npm`. +Note this plugin should work equally well despite any personal preferences for either `yarn` or `npm`. ## Example -The following pipeline will run `yarn install` (and presumably some private packages). +To read the value from an environment variable named `MY_TOKEN` when the plugin executes, use the `env` fiels. ```yml steps: - command: yarn install plugins: - seek-oss/private-npm#v1.0.1: - token: ${NPM_TOKEN} + seek-oss/private-npm#v1.1.1: + env: "MY_TOKEN" ``` +To read the value from a file named `my_token_file`, use the `file` field. + +```yml +steps: + - command: yarn install + plugins: + seek-oss/private-npm#v1.1.1: + file: "my_token_file" +``` + +Alternatively you can read the token directly from any value exposed toxs your `pipeline.yml` file. However, this +approach is discoraged in favour of using with the `env` or `file` fields. This functionality remains in the interest + of backwards compatibility. + +```yml +steps: + - command: yarn install + plugins: + seek-oss/private-npm#v1.1.1: + token: ${MY_TOKEN} +``` + + You can also specify a custom npm registry if you are using your own mirror. ```yml steps: - command: yarn install plugins: - seek-oss/private-npm#v1.0.1: - token: ${NPM_TOKEN} + seek-oss/private-npm#v1.1.1: + env: "MY_TOKEN" registry: //myprivatenpm.com/ ``` ## Configuration -### `token` (required) -The value of the NPM token. +> **NOTE** Even thought `env`, `file` and `token` are described as optional, _at least one must be set_ or the plugin +> will fail. + +### `env` (optional) + +The value of the NPM token will be read from the agent environment when the plugin executes. This is useful in working +around cases where eager binding of variables in `pipeline.yml` means some variables are not present in the +environment when the configuration file is parsed. + +### `file` (optional) + +The value of the NPM token will be read from a file on the agent when the plugin executes. This is useful when working +with secret that are created as files on the filesystem when a build is initiated. + +### `token` (optional) + +The value of the NPM token will be read from a variable which is available to the Buildkite YAML parsing context. +This value is interpolated when the YAML configuration is parsed by the Buildgent agent and provided to the plugin "as +is". -Example: `${NPM_TOKEN}` +Example: `${MY_TOKEN}` +> **NOTE:** Don't put your tokens into source control. Don't use web interfaces you don't control to inject them into +> your environment either. Rather use a Secrets Manager. If you are an AWS user, perhaps consider the +> [aws-sm-buildkite-plugin](https://github.com/seek-oss/aws-sm-buildkite-plugin) which works well with this plugin. -> *NOTE* It's bad security practise to put your secrets into source control. A better idea is to use environment variables. +> **NOTE:** There is anecdotal evidence to suggest that using `NPM_TOKEN` as the variable name containing the +> token can intermittently cause the token to become empty. It is advised to use a different name as has been done in +> these docs. ### `registry` (optional) The path to a private npm repository. Please ensure you supply the trailing `/`! diff --git a/hooks/pre-command b/hooks/pre-command index b6d0e47..171eff9 100755 --- a/hooks/pre-command +++ b/hooks/pre-command @@ -2,12 +2,36 @@ set -euo pipefail IFS=$'\n\t' -echo '--- Setting up access for :no_entry_sign: :npm: :package:' - REGISTRY=${BUILDKITE_PLUGIN_PRIVATE_NPM_REGISTRY:-'//registry.npmjs.org/'} -NPM_TOKEN=${BUILDKITE_PLUGIN_PRIVATE_NPM_TOKEN} +TOKEN=${BUILDKITE_PLUGIN_PRIVATE_NPM_TOKEN:-''} +FILE=${BUILDKITE_PLUGIN_PRIVATE_NPM_FILE:-''} +ENV=${BUILDKITE_PLUGIN_PRIVATE_NPM_ENV:-''} + +if { [[ -n "${FILE}" ]] && [[ -n "${ENV}" ]]; } \ + || { [[ -n "${FILE}" ]] && [[ -n "${TOKEN}" ]]; } \ + || { [[ -n "${TOKEN}" ]] && [[ -n "${ENV}" ]]; } +then + echo ':no_entry_sign: :npm: :package: Failed! Only one of file, env or token parameters may be set' + exit 1 +fi + +if [[ -n "${FILE}" ]] +then + TOKEN=$(cat "${FILE}") +elif [[ -n "${ENV}" ]] +then + TOKEN="${!ENV}" +fi + +if [[ -z $TOKEN ]] +then + echo ':no_entry_sign: :npm: :package: Failed! A valid NPM_TOKEN could not be determined' + exit 1 +fi + +echo '--- Setting up access for :no_entry_sign: :npm: :package:' cat > .npmrc << EOF -${REGISTRY}:_authToken=${NPM_TOKEN} +${REGISTRY}:_authToken=${TOKEN} save-exact=true EOF diff --git a/plugin.yml b/plugin.yml index 3aa65a9..a24dc7f 100644 --- a/plugin.yml +++ b/plugin.yml @@ -10,5 +10,8 @@ configuration: type: string token: type: string - required: - - token + env: + type: string + file: + type: string + diff --git a/tests/pre-command.bats b/tests/pre-command.bats index f24cd93..6b02078 100644 --- a/tests/pre-command.bats +++ b/tests/pre-command.bats @@ -3,6 +3,13 @@ load "$BATS_PATH/load.bash" teardown() { rm -f .npmrc + unset BUILDKITE_PLUGIN_PRIVATE_NPM_ENV + unset BUILDKITE_PLUGIN_PRIVATE_NPM_TOKEN + unset BUILDKITE_PLUGIN_PRIVATE_NPM_FILE + unset BUILDKITE_PLUGIN_PRIVATE_NPM_REGISTRY + unset MY_ENV_VAR + rm -fr my_token_file + rm -fr my_empty_file } @@ -16,7 +23,65 @@ teardown() { assert_equal "$(head -n1 .npmrc)" '//registry.npmjs.org/:_authToken=abc123' } -@test "crates a npmrc file with supplied registry path and token" { +@test "reads the token from a file if the file parameter is used" { + export BUILDKITE_PLUGIN_PRIVATE_NPM_FILE='my_token_file' + echo 'abc123' > my_token_file + + run $PWD/hooks/pre-command + + assert_success + assert [ -e '.npmrc' ] + assert_equal "$(head -n1 .npmrc)" '//registry.npmjs.org/:_authToken=abc123' +} + +@test "fails if the file parameter is used but no file exists" { + export BUILDKITE_PLUGIN_PRIVATE_NPM_FILE='my_missing_file' + + run $PWD/hooks/pre-command + + assert_failure +} + +@test "fails if the file parameter is used and the file exists but is empty" { + export BUILDKITE_PLUGIN_PRIVATE_NPM_FILE='my_empty_file' + + touch my_empty_file + + run $PWD/hooks/pre-command + + assert_failure +} + +@test "reads the token from the environment if the env parameter is used" { + export BUILDKITE_PLUGIN_PRIVATE_NPM_ENV='MY_ENV_VAR' + export MY_ENV_VAR='abc123' + + run $PWD/hooks/pre-command + + assert_success + assert [ -e '.npmrc' ] + assert_equal "$(head -n1 .npmrc)" '//registry.npmjs.org/:_authToken=abc123' +} + +@test "fails if the env parameter is used but no such variable is defined" { + export BUILDKITE_PLUGIN_PRIVATE_NPM_ENV='MY_MISSING_VAR' + + run $PWD/hooks/pre-command + + assert_failure +} + +@test "fails if the env parameter is used but the value of the variable is empty" { + export BUILDKITE_PLUGIN_PRIVATE_NPM_ENV='MY_EMPTY_VAR' + + export MY_EMPTY_VAR="" + + run $PWD/hooks/pre-command + + assert_failure +} + +@test "creates a npmrc file with supplied registry path and token" { export BUILDKITE_PLUGIN_PRIVATE_NPM_TOKEN='abc123' export BUILDKITE_PLUGIN_PRIVATE_NPM_REGISTRY='//myprivateregistry.org/' @@ -27,9 +92,80 @@ teardown() { assert_equal "$(head -n1 .npmrc)" '//myprivateregistry.org/:_authToken=abc123' } -@test "the command fails if the token is not set" { +@test "creates a npmrc file with supplied registry path and env" { + export BUILDKITE_PLUGIN_PRIVATE_NPM_ENV='MY_ENV_VAR' + export MY_ENV_VAR='abc123' + export BUILDKITE_PLUGIN_PRIVATE_NPM_REGISTRY='//myprivateregistry.org/' + + run $PWD/hooks/pre-command + + assert_success + assert [ -e '.npmrc' ] + assert_equal "$(head -n1 .npmrc)" '//myprivateregistry.org/:_authToken=abc123' +} + +@test "creates a npmrc file with supplied registry path and file" { + export BUILDKITE_PLUGIN_PRIVATE_NPM_FILE='my_token_file' + echo 'abc123' > my_token_file + export BUILDKITE_PLUGIN_PRIVATE_NPM_REGISTRY='//myprivateregistry.org/' + + run $PWD/hooks/pre-command + + assert_success + assert [ -e '.npmrc' ] + assert_equal "$(head -n1 .npmrc)" '//myprivateregistry.org/:_authToken=abc123' +} + +@test "the command fails if none of the fields are not set" { + run $PWD/hooks/pre-command + + assert_failure + refute [ -e '.npmrc' ] +} + +# There is an exclusive relationship between file, env, and token. These tests ensure only value is set and fail with +# a meaninful message otherwise +@test "fails if env and file are both set" { + export BUILDKITE_PLUGIN_PRIVATE_NPM_FILE='my_token_file' + export BUILDKITE_PLUGIN_PRIVATE_NPM_ENV='MY_ENV_VAR' + + run $PWD/hooks/pre-command + + assert_failure + assert_output ':no_entry_sign: :npm: :package: Failed! Only one of file, env or token parameters may be set' + refute [ -e '.npmrc' ] +} + +@test "fails if token and file are both set" { + export BUILDKITE_PLUGIN_PRIVATE_NPM_FILE='my_token_file' + export BUILDKITE_PLUGIN_PRIVATE_NPM_TOKEN='abc123' + + run $PWD/hooks/pre-command + + assert_failure + assert_output ':no_entry_sign: :npm: :package: Failed! Only one of file, env or token parameters may be set' + refute [ -e '.npmrc' ] +} + +@test "fails if env and token are both set" { + export BUILDKITE_PLUGIN_PRIVATE_NPM_TOKEN='abc123' + export BUILDKITE_PLUGIN_PRIVATE_NPM_ENV='MY_ENV_VAR' + + run $PWD/hooks/pre-command + + assert_failure + assert_output ':no_entry_sign: :npm: :package: Failed! Only one of file, env or token parameters may be set' + refute [ -e '.npmrc' ] +} + +@test "fails if env, file and token are all set" { + export BUILDKITE_PLUGIN_PRIVATE_NPM_FILE='my_token_file' + export BUILDKITE_PLUGIN_PRIVATE_NPM_ENV='MY_ENV_VAR' + export BUILDKITE_PLUGIN_PRIVATE_NPM_TOKEN='abc123' + run $PWD/hooks/pre-command assert_failure + assert_output ':no_entry_sign: :npm: :package: Failed! Only one of file, env or token parameters may be set' refute [ -e '.npmrc' ] } \ No newline at end of file