etherpad-lite/bin/setup-trusted-publishers.sh
John McLear b57b25a4d7
fix: setup-trusted-publishers.sh works with real npm trust CLI (#7491)
* fix: setup-trusted-publishers.sh works with real npm trust CLI

Two issues found when running the script for the first time after #7490:

1. `npm trust github --file` wants ONLY the workflow filename basename
   (e.g. `test-and-release.yml`), not the full
   `.github/workflows/test-and-release.yml` path. npm errors out with
   "GitHub Actions workflow must be just a file not a path" otherwise.
   Constants updated.

2. `npm trust github` requires 2FA on accounts that have it enabled,
   and there is no way to disable that requirement. Add a `--otp <code>`
   pass-through flag and forward it to every call so a maintainer can
   batch-process multiple packages within a single TOTP window.
   Documented the limitation in the script header.

Also reword the call site so the npm command line is built without
shell-string round-tripping (passing $CMD through `$( $CMD )` was
unrelated to this bug but was bad practice).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: setup-trusted-publishers.sh recognizes 409 as already-configured

When --skip-existing is set, treat HTTP 409 Conflict from
POST /-/package/<name>/trust as 'already configured' so re-runs of
the bulk script don't fail on packages that were configured in a
previous run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: cover setup-trusted-publishers.sh, harden against set -e, document --otp

Addresses qodo review on #7491:

- Add backend regression test that shims `npm` on PATH and asserts
  `--file` is given the workflow basename (never a path), `--otp` is
  forwarded to every `npm trust github` call when supplied, and the
  loop survives a non-zero exit so `--skip-existing` can absorb 409
  Conflict responses from the registry.
- Wrap the `npm trust github` invocation in `set +e` / `set -e`. The
  `if configure_one` already shields the function from errexit in
  practice, but a future refactor moving the call site out of an `if`
  would silently reintroduce the bug — the explicit shim makes intent
  obvious and survives such refactors.
- Document `--otp` and the 2FA / TOTP-expiry workflow in
  doc/npm-trusted-publishing.md so maintainers don't follow the docs
  and hit EOTP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:56:13 +01:00

170 lines
5.5 KiB
Bash
Executable File

#!/bin/sh
#
# Configure npm Trusted Publishers (OIDC) for ep_etherpad and every
# ether/ep_* plugin in bulk.
#
# Prerequisites:
# - npm CLI >= 11.5.1 (the version that ships `npm trust github`)
# - Logged into npmjs.com as a maintainer of the packages: `npm login`
# - `gh` CLI logged in (only needed for plugin discovery; pass --packages
# to skip discovery and use a static list)
#
# Usage:
# bin/setup-trusted-publishers.sh # all ether/ep_* plugins + ep_etherpad
# bin/setup-trusted-publishers.sh --dry-run # print what would happen
# bin/setup-trusted-publishers.sh --packages ep_align,ep_webrtc
# bin/setup-trusted-publishers.sh --skip-existing # don't fail if already configured
# bin/setup-trusted-publishers.sh --otp 123456 # supply 2FA OTP up front
#
# Note: `npm trust github` requires 2FA. If your account has 2FA enabled
# (it should), pass --otp once and the same code will be reused for every
# package call inside the same minute. The TOTP code typically expires
# every 30s, so you may need to run the script in chunks via --packages.
#
# Each package gets a GitHub Actions trusted publisher pointing at the
# canonical workflow file used by that package family:
# - plugins: .github/workflows/test-and-release.yml
# - ep_etherpad: .github/workflows/releaseEtherpad.yml
#
# Existing configurations cannot be overwritten — only one trust relationship
# per package is allowed today. Use `--skip-existing` to ignore those failures.
set -eu
# `npm trust github --file` wants ONLY the workflow filename (basename),
# not the full .github/workflows/<name> path.
PLUGIN_WORKFLOW="test-and-release.yml"
CORE_WORKFLOW="releaseEtherpad.yml"
CORE_PACKAGE="ep_etherpad"
CORE_REPO="etherpad-lite"
ORG="ether"
DRY_RUN=0
SKIP_EXISTING=0
PACKAGES=""
OTP=""
usage() {
sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
exit "${1:-0}"
}
# ---------- arg parsing ----------
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--skip-existing) SKIP_EXISTING=1; shift ;;
--packages) PACKAGES="$2"; shift 2 ;;
--otp) OTP="$2"; shift 2 ;;
-h|--help) usage 0 ;;
*) printf 'Unknown flag: %s\n' "$1" >&2; usage 1 ;;
esac
done
# ---------- prereq checks ----------
is_cmd() { command -v "$1" >/dev/null 2>&1; }
is_cmd npm || { echo "npm CLI not found." >&2; exit 1; }
NPM_MAJOR=$(npm --version | cut -d. -f1)
NPM_MINOR=$(npm --version | cut -d. -f2)
NPM_PATCH=$(npm --version | cut -d. -f3)
if [ "$NPM_MAJOR" -lt 11 ] || \
{ [ "$NPM_MAJOR" -eq 11 ] && [ "$NPM_MINOR" -lt 5 ]; } || \
{ [ "$NPM_MAJOR" -eq 11 ] && [ "$NPM_MINOR" -eq 5 ] && [ "$NPM_PATCH" -lt 1 ]; }; then
echo "npm >= 11.5.1 required (you have $(npm --version)). Run: npm install -g npm@latest" >&2
exit 1
fi
# Verify auth (whoami fails if not logged in). Skipped in --dry-run.
if [ "$DRY_RUN" != "1" ]; then
if ! npm whoami >/dev/null 2>&1; then
echo "Not logged into npm. Run 'npm login' first." >&2
exit 1
fi
fi
# ---------- discover packages ----------
if [ -z "$PACKAGES" ]; then
is_cmd gh || {
echo "gh CLI not found. Either install it or pass --packages ep_a,ep_b,..." >&2
exit 1
}
echo "Discovering ether/ep_* repos..."
PACKAGES=$(gh repo list "$ORG" --limit 300 --json name,isArchived \
--jq '.[] | select(.name | startswith("ep_")) | select(.isArchived | not) | .name' \
| tr '\n' ',' | sed 's/,$//')
PACKAGES="${CORE_PACKAGE},${PACKAGES}"
fi
# ---------- per-package setup ----------
configure_one() {
PKG="$1"
if [ "$PKG" = "$CORE_PACKAGE" ]; then
REPO="$CORE_REPO"
WORKFLOW="$CORE_WORKFLOW"
else
REPO="$PKG"
WORKFLOW="$PLUGIN_WORKFLOW"
fi
printf '%-40s -> %s/%s @ %s\n' "$PKG" "$ORG" "$REPO" "$WORKFLOW"
if [ "$DRY_RUN" = "1" ]; then
printf ' (dry-run) would run: npm trust github %s --repository %s/%s --file %s --yes\n' \
"$PKG" "$ORG" "$REPO" "$WORKFLOW"
return 0
fi
# Disable -e around the npm call so a non-zero exit can never short-circuit
# the STATUS / --skip-existing handling below. In practice the wrapping
# `if configure_one` already suppresses errexit inside this function (POSIX
# errexit-in-conditional behaviour), but relying on that is fragile — anyone
# later refactoring the call site out of an `if` would silently reintroduce
# the bug. The explicit shim makes the intent obvious and survives such
# refactors.
set +e
if [ -n "$OTP" ]; then
OUTPUT=$(npm trust github "$PKG" --repository "$ORG/$REPO" --file "$WORKFLOW" --otp "$OTP" --yes 2>&1)
else
OUTPUT=$(npm trust github "$PKG" --repository "$ORG/$REPO" --file "$WORKFLOW" --yes 2>&1)
fi
STATUS=$?
set -e
if [ "$STATUS" -eq 0 ]; then
printf ' ok\n'
else
# The npm registry returns 409 Conflict when a trust relationship
# already exists (you can only have one per package today). Treat
# that as success when --skip-existing is set, alongside the older
# "already exists/configured" string match.
if [ "$SKIP_EXISTING" = "1" ] && \
echo "$OUTPUT" | grep -qiE "409 Conflict|already (exists|configured)"; then
printf ' already configured (skipped)\n'
return 0
fi
printf ' FAILED:\n%s\n' "$OUTPUT" | sed 's/^/ /'
return 1
fi
}
FAILED=""
TOTAL=0
OK=0
IFS=','
for PKG in $PACKAGES; do
TOTAL=$((TOTAL + 1))
if configure_one "$PKG"; then
OK=$((OK + 1))
else
FAILED="$FAILED $PKG"
fi
done
unset IFS
printf '\n%d/%d packages configured\n' "$OK" "$TOTAL"
if [ -n "$FAILED" ]; then
printf 'Failed:%s\n' "$FAILED"
exit 1
fi