diff --git a/scripts/npm-dist-tag.sh b/scripts/npm-dist-tag.sh index 5c9e6f5f576..884ff5e7336 100755 --- a/scripts/npm-dist-tag.sh +++ b/scripts/npm-dist-tag.sh @@ -1,16 +1,26 @@ #! /bin/bash usage() { cat << END_USAGE -Usage: -$0 [--dry-run] [lerna] add [-] - Add to package dist-tags for current version or specified . -$0 [--dry-run] [lerna] - Remove from package dist-tags. -$0 [--dry-run] [lerna] [] - List package dist-tags, or just the one named . +Usage: $0 [--dry-run|--otp-stream=] [lerna] []... + +Commands: +add [-] + Read package name and version from package.json and add to its dist-tags + on npm for either that version or version x.y.z-. + + Read package name from package.json and remove from its dist-tags on npm. + [] + Read package name from package.json and list its dist-tag mappings from npm + (optionally limited to the dist-tag named ). With "--dry-run", npm commands are printed to standard error rather than executed. +With "--otp-stream=", for each npm command the value of an "--otp" option +is read from the specified file (generally expected to be a named pipe created +by \`mkfifo\`) and failing commands are retried until the read value is empty. +Alternatively, environment variable configuration \`npm_config_auth_type=legacy\` +causes npm itself to prompt for and read OTP values from standard input. + If the first operand is "lerna", the operation is extended to all packages. END_USAGE exit 1 @@ -25,91 +35,134 @@ fail() { # Exit on any errors. set -ueo pipefail -# Check for `--dry-run`. +# Check for `--dry-run` and `--otp-stream`. npm=npm -dryrun= -if test "${1:-}" = "--dry-run"; then - dryrun=$1 - npm="echo-to-stderr npm" - shift -fi +style= +otpfile= +case "${1:-}" in + --dry-run) + style="$1" + npm="echo-to-stderr npm" + shift + ;; + --otp-stream=*) + style="$1" + otpfile="${1##--otp-stream=}" + npm="npm-otp" + shift + ;; +esac echo-to-stderr() { echo "$@"; } 1>&2 +npm-otp() { + printf 1>&2 "Reading OTP from %s ... " "$otpfile" + otp=$(head -n 1 "$otpfile") + if test -z "$otp"; then + echo 1>&2 "No OTP" + return 66 + fi + echo 1>&2 OK + npm --otp="$otp" "$@" +} -# Check the first argument. +# Check for `lerna`. case "${1-}" in lerna) - # npm-dist-tag.sh lerna [args]... - # Run `npm-dist-tag.sh [args]...` in every package directory. + # npm-dist-tag.sh lerna [arg]... + # Run `npm-dist-tag.sh [arg]...` in every package directory. # Find the absolute path to this script. thisdir=$(cd "$(dirname -- "${BASH_SOURCE[0]}")" > /dev/null && pwd -P) thisprog=$(basename -- "${BASH_SOURCE[0]}") + cd "$thisdir" # Strip the first argument (`lerna`), so that `$@` gives us remaining args. shift - exec npm run -- lerna exec --concurrency=1 --no-bail "$thisdir/$thisprog" -- $dryrun ${1+"$@"} + exec npm run -- lerna exec --concurrency=1 --no-bail -- "$thisdir/$thisprog" "$style" "$@" ;; esac +CMD="${1-"--help"}" -# If the package.json says it's private, we don't have a published version whose -# tags we can manipulate. -priv=$(jq -r .private package.json) -case "$priv" in - true) - echo 1>&2 "Skipping $(basename "$0") for private package $(jq .name package.json)" - exit 0 - ;; -esac +# Read current-directory package.json fields "name"/"version"/"private" into shell variables +# by evaluating single-quoted assignments like `='...'`. +eval "$(jq < package.json -r --arg Q "'" ' + pick(.name, .version, .private) + | to_entries + | .[] + # Replace a null/false value with empty string. + | ((.value // "") | tostring) as $str_value + # Enclosing single-quote `$Q`s preserve the literal value of each character + # except actual single-quotes, which are replaced with an escape sequence by + # the `gsub`. + | (.key + "=" + $Q + ($str_value | gsub($Q; $Q + "\\" + $Q + $Q)) + $Q) +')" -# Get the second argument, if any. -TAG=${2-} +# dist-tags are only applicable to published packages. +if test "$private" = true -a "$CMD" != "--help"; then + echo 1>&2 "Skipping private package $name" + exit 0 +fi -# Read package.json for the package name and current version. -pkg=$(jq -r .name package.json) +# Process command arguments: [ [-]]. +TAG="${2-}" case ${3-} in -*) - # Instead of current package version, reference an already-published version - # with the specified pre-release suffix. - version=$(npm view "$pkg" versions --json \ - | - # cf. https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions - jq --arg s "$3" -r '.[] | select(sub("^((^|[.])(0|[1-9][0-9]*)){3}"; "") == $s)' || true) + # "add -" scans published versions for an exact match of + # the specified pre-release suffix and applies the new dist-tag to that + # version rather than to the version read from package.json. + + # cf. https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions + semver_prefix="^((^|[.])(0|[1-9][0-9]*)){3}" + + version=$(npm view "$name" versions --json \ + | jq --arg p "$semver_prefix" --arg suffix "$3" -r '.[] | select(sub($p; "") == $suffix)' \ + | tail -n 1) ;; *) test "$#" -le 2 || fail "Invalid pre-release suffix!" - version=$(jq -r .version package.json) ;; esac -case "${1-}" in +case "$CMD" in add) - # Add $TAG to the current-directory package's dist-tags. + # Add $TAG to dist-tags. test -n "$TAG" || fail "Missing tag!" test "$#" -le 3 || fail "Too many arguments!" - $npm dist-tag add "$pkg@$version" "$TAG" + while true; do + $npm dist-tag add "$name@$version" "$TAG" && break || ret=$? + [[ "$style" =~ --otp-stream ]] || exit $ret + [ $ret -ne 66 ] && continue + echo Aborting + exit 1 + done ;; remove | rm) - # Remove $TAG from the current-directory package's dist-tags. + # Remove $TAG from dist-tags. test -n "$TAG" || fail "Missing tag!" test "$#" -le 2 || fail "Too many arguments!" - $npm dist-tag rm "$pkg" "$TAG" + while true; do + $npm dist-tag rm "$name" "$TAG" && break || ret=$? + [[ "$style" =~ --otp-stream ]] || exit $ret + [ $ret -ne 66 ] && continue + echo Aborting + exit 1 + done ;; list | ls) - # List the current-directory package's dist-tags, or just the specific $TAG. + # List either all dist-tags or just the specific $TAG. test "$#" -le 2 || fail "Too many arguments!" if test -n "$TAG"; then - if test -n "$dryrun"; then + if test "$style" = "--dry-run"; then # Print the entire pipeline. - $npm dist-tag ls "$pkg" \| sed -ne "s/^$TAG: //p" + $npm dist-tag ls "$name" \| awk -vP="$TAG" -F: '$1==P' else - $npm dist-tag ls "$pkg" | sed -ne "s/^$TAG: //p" + $npm dist-tag ls "$name" | awk -vP="$TAG" -F: '$1==P' fi else - $npm dist-tag ls "$pkg" + $npm dist-tag ls "$name" fi ;; *) - test "${1-"--help"}" = "--help" || fail "Bad command!" + test "$CMD" = "--help" || fail "Bad command!" usage ;; esac