From 9818f74831c5478f43427685a30cdea0c95def45 Mon Sep 17 00:00:00 2001 From: Daniel Sogl Date: Mon, 27 Jul 2026 21:18:10 +0200 Subject: [PATCH] ci: publish to npm via trusted publishing (OIDC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm validates the OIDC claim of the entry-point workflow, not the one that runs `npm publish`. Calling publish.yml via workflow_call meant the trust check could never match, so the publish job moves inline into release-please.yml and publish.yml is removed. setup-node with Node 22 ships npm 10.x; trusted publishing needs >= 11.5.1, hence the explicit npm upgrade before publishing. NPM_TOKEN stays as a fallback for packages that have no trusted publisher yet — a brand new plugin cannot get one until it exists on npm. OIDC takes precedence wherever a config is present. Adds scripts/tasks/trust-packages.sh to configure the trusted publisher for all 258 packages; npm has no multi-package call and `npm trust` requires interactive 2FA, so it runs locally, in parallel, and is resumable. --- .github/workflows/publish.yml | 39 ---------- .github/workflows/release-please.yml | 41 +++++++++- .gitignore | 2 + scripts/tasks/trust-packages.sh | 109 +++++++++++++++++++++++++++ 4 files changed, 150 insertions(+), 41 deletions(-) delete mode 100644 .github/workflows/publish.yml create mode 100755 scripts/tasks/trust-packages.sh diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 8a18aae4b..000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Publish to npm - -on: - workflow_call: - workflow_dispatch: - -permissions: - contents: read - id-token: write - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.3.1 - - - name: Setup Node.js - uses: actions/setup-node@v4.4.0 - with: - node-version: 22 - cache: npm - registry-url: https://registry.npmjs.org - - - name: Install dependencies - run: npm ci - - - name: Lint - run: npm run lint -- --quiet - - - name: Test - run: npm test - - - name: Build - run: npm run build - - - name: Publish to npm - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npm run npmpub diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 89fb1e3bb..d4dafa8a4 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -27,8 +27,45 @@ jobs: config-file: release-please-config.json manifest-file: .release-please-manifest.json + # npm trusted publishing validates the OIDC claim of the *entry point* workflow, + # not the one that runs `npm publish`. Keep this job inline — a reusable workflow + # here breaks the trust check. See https://github.com/npm/documentation/issues/1755 publish: needs: release-please if: ${{ needs.release-please.outputs.release_created }} - uses: ./.github/workflows/publish.yml - secrets: inherit + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4.3.1 + + - name: Setup Node.js + uses: actions/setup-node@v4.4.0 + with: + node-version: 22 + cache: npm + registry-url: https://registry.npmjs.org + + # node 22 ships npm 10.x; trusted publishing (OIDC) requires npm >= 11.5.1 + - name: Update npm + run: npm install -g npm@latest + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint -- --quiet + + - name: Test + run: npm test + + - name: Build + run: npm run build + + - name: Publish to npm + env: + # fallback for packages without a trusted publisher yet (e.g. brand new plugins). + # OIDC takes precedence whenever the package has one configured. + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm run npmpub diff --git a/.gitignore b/.gitignore index aaa28142e..f3d0095ca 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ src/@awesome-cordova-plugins/plugins/**/ngx *.d.ts injectable-classes.json +# npm trust bulk setup progress +.npm-trust-done diff --git a/scripts/tasks/trust-packages.sh b/scripts/tasks/trust-packages.sh new file mode 100755 index 000000000..26e2acbb2 --- /dev/null +++ b/scripts/tasks/trust-packages.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# One-time (and after adding new plugins) bulk setup of npm trusted publishers. +# Run locally after `npm login` — `npm trust` needs interactive 2FA and cannot run in CI. +# +# ./scripts/tasks/trust-packages.sh # 4 parallel workers +# JOBS=8 ./scripts/tasks/trust-packages.sh # more, at the risk of rate limiting +# +# npm has no multi-package call, so this loops. The first package runs on its own to +# trigger the browser 2FA prompt — tick "skip 2FA for the next 5 minutes" there. That +# window is server-side, so the parallel batch afterwards runs unattended inside it. +# Parallelism is what keeps the whole set inside a single window instead of ~4. +# +# A trust call is ~2s, so JOBS=4 is ~2 req/s — 4x npm's serial guidance, and the +# reason there is no extra sleep here. If npm rate-limits, the affected packages +# just land in the failed list and the next run picks them up. +# +# Packages that already have a trust config come back as E409 — npm only ever creates, +# there is no update path — so those are treated as done. To actually *change* a config, +# revoke it first: npm trust list && npm trust revoke --id=, then re-run. +# +# Progress is recorded in .npm-trust-done; re-running resumes where it stopped. +# Packages must already exist on npm; a brand new plugin publishes via NPM_TOKEN +# on its first release, then gets its trust config here. +set -euo pipefail + +REPO=danielsogl/awesome-cordova-plugins +WORKFLOW=release-please.yml +JOBS="${JOBS:-4}" +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +SELF="$ROOT/scripts/tasks/trust-packages.sh" +DONE="$ROOT/.npm-trust-done" + +create() { + npm trust github "$1" --file "$WORKFLOW" --repo "$REPO" --allow-publish --yes +} + +trust_one() { + local pkg="$1" err rc=0 + err=$(mktemp) + # stderr is buffered so E409 can be detected; stdout flows through untouched, + # otherwise npm's "Press ENTER to open in the browser" prompt would be invisible + create "$pkg" 2>"$err" || rc=$? + + if [ "$rc" -eq 0 ]; then + # single short line + O_APPEND is atomic, so parallel workers can't interleave + echo "$pkg" >> "$DONE" + elif grep -q "E409" "$err"; then + # already has a trust config; npm only ever creates, so this is a no-op, not a failure + echo ">> $pkg already configured, skipping" >&2 + echo "$pkg" >> "$DONE" + rc=0 + else + cat "$err" >&2 + fi + + rm -f "$err" + return "$rc" +} + +# re-entry point for xargs workers +if [ "${1:-}" = "--one" ]; then + trust_one "$2" + exit +fi + +packages=(core) +for dir in "$ROOT"/src/@awesome-cordova-plugins/plugins/*/; do + packages+=("$(basename "$dir")") +done + +touch "$DONE" +todo=() +for name in "${packages[@]}"; do + pkg="@awesome-cordova-plugins/$name" + grep -qxF "$pkg" "$DONE" || todo+=("$pkg") +done + +if [ ${#todo[@]} -eq 0 ]; then + echo "All ${#packages[@]} packages already configured." + exit 0 +fi + +echo "${#todo[@]} of ${#packages[@]} packages left for $REPO/.github/workflows/$WORKFLOW" +echo +echo ">> ${todo[0]} runs first. Approve in the browser and tick" +echo ">> \"skip 2FA for the next 5 minutes\" — the rest then runs unattended." +echo +trust_one "${todo[0]}" || true + +if [ ${#todo[@]} -gt 1 ]; then + echo + echo "Configuring the remaining $((${#todo[@]} - 1)) with $JOBS parallel workers..." + printf '%s\n' "${todo[@]:1}" | xargs -P "$JOBS" -n1 "$SELF" --one || true +fi + +failed=() +for pkg in "${todo[@]}"; do + grep -qxF "$pkg" "$DONE" || failed+=("$pkg") +done + +if [ ${#failed[@]} -gt 0 ]; then + echo + printf 'failed: %s\n' "${failed[@]}" + echo + echo "An expired 2FA window is the usual cause — re-run and approve again." + echo "Re-run retries only the failures; successes are recorded in $DONE." + exit 1 +fi +echo "Done."