Skip to content

Commit

Permalink
feat(scripts): Add npm-dist-tag.sh --otp-stream for a better CLI expe…
Browse files Browse the repository at this point in the history
…rience (#9658)

Ref #9079

## Description
Refactor npm-dist-tag.sh to improve readability, efficiency, correctness, and most importantly user experience (in particular by avoiding breakouts to browser tabs for entering OTP values with default npm configuration).

### Security Considerations
n/a

### Scaling Considerations
n/a

### Documentation Considerations
Usage output and internal comments better explain how to use the script and what it does.

### Testing Considerations
Tested manually.

### Upgrade Considerations
n/a
  • Loading branch information
mergify[bot] authored Jul 7, 2024
2 parents 2c28414 + 5920b6b commit 62ea793
Showing 1 changed file with 102 additions and 49 deletions.
151 changes: 102 additions & 49 deletions scripts/npm-dist-tag.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
#! /bin/bash
usage() {
cat << END_USAGE
Usage:
$0 [--dry-run] [lerna] add <tag> [-<pre-release>]
Add <tag> to package dist-tags for current version or specified <pre-release>.
$0 [--dry-run] [lerna] <remove|rm> <tag>
Remove <tag> from package dist-tags.
$0 [--dry-run] [lerna] <list|ls> [<tag>]
List package dist-tags, or just the one named <tag>.
Usage: $0 [--dry-run|--otp-stream=<fifo>] [lerna] <command> [<argument>]...
Commands:
add <tag> [-<pre-release>]
Read package name and version from package.json and add <tag> to its dist-tags
on npm for either that version or version x.y.z-<pre-release>.
<remove|rm> <tag>
Read package name from package.json and remove <tag> from its dist-tags on npm.
<list|ls> [<tag>]
Read package name from package.json and list its dist-tag mappings from npm
(optionally limited to the dist-tag named <tag>).
With "--dry-run", npm commands are printed to standard error rather than executed.
With "--otp-stream=<fifo>", 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
Expand All @@ -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 `<name>='...'`.
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> [-<pre-release>]].
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 <tag> -<pre-release>" 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

0 comments on commit 62ea793

Please sign in to comment.