Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(scripts): Add npm-dist-tag.sh --otp-stream for a better CLI experience #9658

Merged
merged 9 commits into from
Jul 7, 2024
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
Loading