From e99ea4eadf8c9f0b322c20b5612b47cb24613448 Mon Sep 17 00:00:00 2001 From: Luca Rachiteanu Date: Tue, 15 Sep 2026 14:47:42 +0300 Subject: [PATCH 1/5] ci: publish JS packages from GitHub Actions with npm trusted publishing Co-Authored-By: Claude Opus 5 --- .github/workflows/cd-npm.yml | 255 +++++++++++++++++++++++++++ src/CI/azp-js.publish-npm.steps.yaml | 100 +++++++---- src/CI/azp-publish.yaml | 4 +- src/Clients/js/package.json | 6 +- 4 files changed, 323 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/cd-npm.yml diff --git a/.github/workflows/cd-npm.yml b/.github/workflows/cd-npm.yml new file mode 100644 index 00000000..15052729 --- /dev/null +++ b/.github/workflows/cd-npm.yml @@ -0,0 +1,255 @@ +name: Publish @uipath/coreipc to GitHub Packages and npmjs + +# Publishes the two JS client packages — @uipath/coreipc (NodeJS) and +# @uipath/coreipc-web — in two steps: a `pack` job that builds and produces the +# exact .tgz files, and publish jobs that push those same tarballs. Nothing is +# ever built twice, so what lands on npmjs is byte-identical to what lands on +# GitHub Packages. +# +# ADO stays primary: on a green NPM publish stage it POSTs a repository_dispatch +# (publish-npm) with the released commit SHA and the version that stage published, +# and this workflow builds & publishes that commit. There is no workflow_dispatch, +# so it can't fire by accident, and dispatch always runs the default-branch copy +# of this file. +# +# Two registries, two trust models: +# - GitHub Packages gets every build, prerelease included, authenticated with the +# built-in GITHUB_TOKEN (`packages: write`). No PAT, no service connection — +# this is what replaces the classic-PAT `PublishNPM` connection that UiPath +# revoked org-wide after the May 2026 npm supply-chain incident. +# - Public npmjs gets STABLE versions only, from master only, via Trusted +# Publishing (OIDC) — no tokens. Prereqs: an npmjs Trusted Publisher on +# @uipath/coreipc (UiPath/coreipc, workflow cd-npm.yml, env npm) + protected +# `npm` environment. @uipath/coreipc-web is not on npmjs yet, so it is +# GitHub-Packages-only until someone bootstraps that name manually. + +on: + repository_dispatch: + types: [publish-npm] + +permissions: + contents: read + +# Never let two publish runs race for the same packages. +concurrency: + group: publish-npm-coreipc + cancel-in-progress: false + +env: + JS_DIR: src/Clients/js + CSPROJ: src/UiPath.CoreIpc/UiPath.CoreIpc.csproj + NODE_VERSION: '20.11.0' # same Node the ADO build uses (azp-nodejs.yaml) + +jobs: + pack: + name: Pack tarballs + runs-on: uipath-ubuntu-latest + outputs: + version: ${{ steps.resolve.outputs.version }} + stable: ${{ steps.resolve.outputs.stable }} + steps: + # Untrusted input (client_payload.*) is read via env, never inlined into a + # run: script, and validated — script-injection hardening. The SHA must be + # an exact 40-char commit (rejects tags/branch names); the version must be + # plain semver. + - name: Resolve & validate dispatch payload + id: ref + env: + DISPATCH_SHA: ${{ github.event.client_payload.sha }} + DISPATCH_VERSION: ${{ github.event.client_payload.version }} + run: | + set -euo pipefail + REF="$DISPATCH_SHA" + if [ -z "$REF" ]; then echo "::error::No client_payload.sha provided."; exit 1; fi + if ! printf '%s' "$REF" | grep -Eq '^[0-9a-fA-F]{40}$'; then + echo "::error::client_payload.sha '$REF' is not a 40-char commit hash — refusing."; exit 1 + fi + VER="$DISPATCH_VERSION" + if [ -z "$VER" ]; then echo "::error::No client_payload.version provided."; exit 1; fi + if ! printf '%s' "$VER" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then + echo "::error::client_payload.version '$VER' is not a plain semver version — refusing."; exit 1 + fi + echo "ref=$REF" >> "$GITHUB_OUTPUT" + echo "version=$VER" >> "$GITHUB_OUTPUT" + + - name: Checkout exact commit (full history) + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + ref: ${{ steps.ref.outputs.ref }} + fetch-depth: 0 # full graph — needed to prove the commit is on master + + # The csproj is the single source of truth for every package in this + # repo (.NET, Python, JS alike). A dispatched version is legal only if it is + # that version (a stable release) or that version plus an ADO build-number + # suffix (a CI build) — anything else means the payload and the commit + # disagree, and we refuse rather than publish a surprise version. + - name: Resolve & validate version against the commit + id: resolve + env: + DISPATCHED: ${{ steps.ref.outputs.version }} + run: | + set -euo pipefail + BASE=$(grep -oPm1 '(?<=)[^<]+' "$CSPROJ" | tr -d '[:space:]') + if [ -z "$BASE" ]; then + echo "::error::No found in $CSPROJ."; exit 1 + fi + if [ "$DISPATCHED" = "$BASE" ]; then + STABLE=true + elif [ "${DISPATCHED#"$BASE"-}" != "$DISPATCHED" ]; then + STABLE=false + else + echo "::error::Dispatched version '$DISPATCHED' does not match the csproj '$BASE' at this commit." + exit 1 + fi + echo "version=$DISPATCHED" >> "$GITHUB_OUTPUT" + echo "stable=$STABLE" >> "$GITHUB_OUTPUT" + echo "Publishing $DISPATCHED (stable=$STABLE)" + + # Enforce "only master reaches public npmjs" at the source level. CI builds + # (prerelease versions) may come from any branch — they only go to GitHub + # Packages — but a stable version that is not merged into master can never + # reach npmjs. Combined with repository_dispatch always running the + # default-branch copy of this file, unmerged code can't be released. + - name: Refuse stable releases that are not on master + if: steps.resolve.outputs.stable == 'true' + env: + SHA: ${{ steps.ref.outputs.ref }} + run: | + set -euo pipefail + git fetch --no-tags origin master + if ! git merge-base --is-ancestor "$SHA" FETCH_HEAD; then + echo "::error::Commit $SHA is not an ancestor of origin/master — only master-merged code may be published to npmjs." + exit 1 + fi + echo "$SHA is on master — OK" + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: ${{ env.JS_DIR }}/package-lock.json + + - name: Install dependencies + working-directory: ${{ env.JS_DIR }} + run: npm ci + + # Same stamping the ADO build does before `npm run build`: webpack copies the + # version out of package.json into both generated packages. + - name: Stamp version into package.json + working-directory: ${{ env.JS_DIR }} + run: npm version "${{ steps.resolve.outputs.version }}" --allow-same-version --no-git-tag-version + + - name: Build + working-directory: ${{ env.JS_DIR }} + run: npm run build + + # Step one of two: produce the tarballs. `npm pack` on the generated package + # directories, explicitly, rather than relying on the webpack shell plugin's + # side-effect pack into dist-packages/ — the artifact is what gets published, + # so it should be built by a step you can see. + - name: Pack + working-directory: ${{ env.JS_DIR }} + run: | + set -euo pipefail + rm -rf "$RUNNER_TEMP/tarballs" + npm pack ./dist/prepack/node --pack-destination "$RUNNER_TEMP/tarballs" + npm pack ./dist/prepack/web --pack-destination "$RUNNER_TEMP/tarballs" + ls -l "$RUNNER_TEMP/tarballs" + + # Guard against a silently wrong artifact: exactly two tarballs, named for the + # two packages at the version we resolved. + - name: Verify tarballs + env: + VERSION: ${{ steps.resolve.outputs.version }} + run: | + set -euo pipefail + cd "$RUNNER_TEMP/tarballs" + for expected in "uipath-coreipc-$VERSION.tgz" "uipath-coreipc-web-$VERSION.tgz"; do + if [ ! -f "$expected" ]; then + echo "::error::Expected tarball '$expected' was not produced. Got: $(ls)"; exit 1 + fi + done + if [ "$(ls -1 ./*.tgz | wc -l)" -ne 2 ]; then + echo "::error::Expected exactly 2 tarballs, got: $(ls)"; exit 1 + fi + + - name: Upload tarballs + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: npm-tarballs + path: ${{ runner.temp }}/tarballs/*.tgz + + publish-github-packages: + name: Publish to GitHub Packages + needs: pack + runs-on: uipath-ubuntu-latest + permissions: + contents: read + packages: write # the whole auth story — GITHUB_TOKEN, no PAT + steps: + - name: Retrieve tarballs + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: npm-tarballs + path: tarballs/ + + - name: Setup Node for GitHub Packages + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://npm.pkg.github.com + scope: '@uipath' + + # Step two of two: publish the tarballs the pack job produced, untouched. + # `packages/npm/coreipc` and `packages/npm/coreipc-web` are both already + # linked to UiPath/coreipc, so GITHUB_TOKEN is authorized for both. + - name: Publish both packages + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ needs.pack.outputs.version }} + run: | + set -euo pipefail + npm publish "tarballs/uipath-coreipc-$VERSION.tgz" + npm publish "tarballs/uipath-coreipc-web-$VERSION.tgz" + + publish-npmjs: + name: Publish to npmjs (Trusted Publishing) + needs: [pack, publish-github-packages] + # Stable releases only. CI builds stop at GitHub Packages. + if: needs.pack.outputs.stable == 'true' + runs-on: uipath-ubuntu-latest + environment: npm # MANDATORY protection (required reviewers + default-branch only) + permissions: + contents: read + id-token: write # OIDC Trusted Publishing — no long-lived token + steps: + - name: Retrieve tarballs + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: npm-tarballs + path: tarballs/ + + - name: Setup Node for npmjs + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://registry.npmjs.org + scope: '@uipath' + + # Trusted Publishing needs npm >= 11.5.1; Node 20.11.0 ships npm 10.x. + - name: Upgrade npm for Trusted Publishing + run: | + set -euo pipefail + npm install -g npm@^11.5.1 + npm --version + + # No NODE_AUTH_TOKEN: npm exchanges the job's OIDC token for a short-lived + # publish credential and attaches a provenance attestation automatically. + # Only the NodeJS package goes to npmjs — @uipath/coreipc-web has never been + # published there, and a Trusted Publisher can only be configured on a name + # that already exists. + - name: Publish @uipath/coreipc + env: + VERSION: ${{ needs.pack.outputs.version }} + run: npm publish "tarballs/uipath-coreipc-$VERSION.tgz" --access public diff --git a/src/CI/azp-js.publish-npm.steps.yaml b/src/CI/azp-js.publish-npm.steps.yaml index 468965db..ac37df13 100644 --- a/src/CI/azp-js.publish-npm.steps.yaml +++ b/src/CI/azp-js.publish-npm.steps.yaml @@ -31,11 +31,6 @@ steps: artifactName: 'NPM package' targetPath: '$(Pipeline.Workspace)/NPM package' -- task: NodeTool@0 - displayName: 'Use Node.js 20.11.0' - inputs: - versionSpec: '20.11.0' - - task: ExtractFiles@1 displayName: 'Extract Files' inputs: @@ -43,39 +38,68 @@ steps: destinationFolder: '$(System.DefaultWorkingDirectory)/unzipped' cleanDestinationFolder: true -# --------------------------------------------------------------------- -# Secondary target: GitHub Packages (best-effort, currently expected to fail) -# --------------------------------------------------------------------- -# Following the May 11–12, 2026 npm supply-chain incident (Mini Shai-Hulud -# / TanStack), UiPath revoked classic GitHub PATs org-wide and is migrating -# everyone to fine-grained PATs. Fine-grained PATs don't have the Packages -# permission available at org level for UiPath — so the existing -# `PublishNPM` service connection can no longer authenticate. +# --------------------------------------------------------------------------- +# Publishing itself lives in GitHub Actions (.github/workflows/cd-npm.yml). +# --------------------------------------------------------------------------- +# ADO used to `npm publish` the two generated package directories straight to +# GitHub Packages through the `PublishNPM` service connection. That connection is +# a classic GitHub PAT, and UiPath revoked classic PATs org-wide after the +# May 11-12, 2026 npm supply-chain incident (Mini Shai-Hulud / TanStack); the +# fine-grained replacements don't expose the Packages permission at org level, so +# there is no ADO-side auth story left. # -# Per Liviu Bud's #dev announcement on 2026-05-25, a sanctioned pipeline- -# auth replacement is being worked on but not yet available: -# https://uipath.enterprise.slack.com/archives/CMDRA3VFH/p1779699547818419 +# GitHub Actions has one for free: a workflow in UiPath/coreipc gets a GITHUB_TOKEN +# with `packages: write`, and both `coreipc` and `coreipc-web` are already linked to +# this repo. The same workflow additionally publishes STABLE releases to public +# npmjs via Trusted Publishing (OIDC), which ADO cannot do at all — npm only trusts +# GitHub Actions identities. # -# We leave the GitHub Packages publish wired up with continueOnError so -# (a) the run doesn't fail when the publish fails on policy, and -# (b) the publish resumes automatically the moment the service connection -# is updated with whatever the platform team ships. -# -# Each Publish_NPM run will be marked "Succeeded with issues" until then. -# Revert continueOnError when the publish path is healthy again. -# --------------------------------------------------------------------- -- task: Npm@1 - displayName: 'Publish to GitHub Packages — NodeJS (best-effort)' - continueOnError: true - inputs: - command: 'publish' - workingDir: '$(System.DefaultWorkingDirectory)/unzipped/dist/prepack/node' - publishEndpoint: PublishNPM +# So this stage no longer publishes. It resolves what the CI build actually produced +# and whispers to GitHub to publish exactly that commit at exactly that version. +# The version is read out of the packed package.json rather than recomputed, so +# there is one source of truth for it. +# --------------------------------------------------------------------------- +- script: | + set -euo pipefail -- task: Npm@1 - displayName: 'Publish to GitHub Packages — Web (best-effort)' - continueOnError: true - inputs: - command: 'publish' - workingDir: '$(System.DefaultWorkingDirectory)/unzipped/dist/prepack/web' - publishEndpoint: PublishNPM + # 1. The version the CI build stamped into the generated NodeJS package. + PKG="$(System.DefaultWorkingDirectory)/unzipped/dist/prepack/node/package.json" + if [ ! -f "$PKG" ]; then + echo "##vso[task.logissue type=error]No packed package.json at $PKG — did the CI build produce the 'NPM package' artifact?" + exit 1 + fi + VERSION=$(jq -r '.version' "$PKG") + if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then + echo "##vso[task.logissue type=error]Could not read .version from $PKG." + exit 1 + fi + + # 2. Resolve the published build's commit (ADO REST). + build=$(curl -sSf -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \ + "${SYSTEM_COLLECTIONURI}${SYSTEM_TEAMPROJECT}/_apis/build/builds/${BUILD_ID}?api-version=7.1") + SHA=$(printf '%s' "$build" | jq -r '.sourceVersion') + BRANCH=$(printf '%s' "$build" | jq -r '.sourceBranch') + echo "buildId ${BUILD_ID}: branch=${BRANCH} commit=${SHA} version=${VERSION}" + + if ! printf '%s' "$SHA" | grep -Eq '^[0-9a-fA-F]{40}$'; then + echo "##vso[task.logissue type=error]No valid commit SHA for buildId ${BUILD_ID} (got '${SHA}')." + exit 1 + fi + + # 3. Whisper. The workflow re-validates everything it is told (the SHA is a real + # commit, the version matches the csproj at that commit, stable releases are + # on master) — this side is convenience, not a trust boundary. + echo "Dispatching publish-npm for ${VERSION} (${SHA})" + curl -sSf -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GITHUB_DISPATCH_TOKEN" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/UiPath/coreipc/dispatches \ + -d "{\"event_type\":\"publish-npm\",\"client_payload\":{\"sha\":\"${SHA}\",\"version\":\"${VERSION}\"}}" + displayName: 'Dispatch publish-npm → GitHub Actions' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + SYSTEM_COLLECTIONURI: $(System.CollectionUri) + SYSTEM_TEAMPROJECT: $(System.TeamProject) + BUILD_ID: ${{ parameters.reuseArtifactsFromBuildId }} + GITHUB_DISPATCH_TOKEN: $(GITHUB_DISPATCH_TOKEN) diff --git a/src/CI/azp-publish.yaml b/src/CI/azp-publish.yaml index 88c75f31..222dec23 100644 --- a/src/CI/azp-publish.yaml +++ b/src/CI/azp-publish.yaml @@ -32,7 +32,7 @@ parameters: default: true - name: publishNpm - displayName: 'Publish NPM (Node + Web) → uipath-ipc-deps (+ GitHub Packages best-effort)' + displayName: 'Publish NPM (Node + Web) → GitHub Packages (+ npmjs on a stable release)' type: boolean default: true @@ -120,6 +120,8 @@ stages: - stage: Publish_NPM displayName: '🚚 Publish NPM' dependsOn: [] + variables: + - group: github-dispatch # supplies secret GITHUB_DISPATCH_TOKEN jobs: - deployment: Publish_NPM_Packages displayName: '📦 Publish NPM (Node + Web)' diff --git a/src/Clients/js/package.json b/src/Clients/js/package.json index 4a575435..136643ce 100644 --- a/src/Clients/js/package.json +++ b/src/Clients/js/package.json @@ -39,14 +39,14 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/eduard-dumitru/js-multitargeting.git" + "url": "git+https://github.com/UiPath/coreipc.git" }, "author": "Eduard Dumitru", "license": "MIT", "bugs": { - "url": "https://github.com/eduard-dumitru/js-multitargeting/issues" + "url": "https://github.com/UiPath/coreipc/issues" }, - "homepage": "https://github.com/eduard-dumitru/js-multitargeting#readme", + "homepage": "https://github.com/UiPath/coreipc#readme", "devDependencies": { "@babel/core": "^7.20.2", "@babel/preset-env": "^7.20.2", From e3ed678a5eacc2c6e1e15340470cfe0132423895 Mon Sep 17 00:00:00 2001 From: Luca Rachiteanu Date: Tue, 15 Sep 2026 16:12:25 +0300 Subject: [PATCH 2/5] ci: run npm publish on GitHub-hosted runners and gate every publish on master Co-Authored-By: Claude Opus 5 --- .github/workflows/cd-npm.yml | 48 +++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/.github/workflows/cd-npm.yml b/.github/workflows/cd-npm.yml index 15052729..2cc35932 100644 --- a/.github/workflows/cd-npm.yml +++ b/.github/workflows/cd-npm.yml @@ -12,16 +12,23 @@ name: Publish @uipath/coreipc to GitHub Packages and npmjs # so it can't fire by accident, and dispatch always runs the default-branch copy # of this file. # -# Two registries, two trust models: +# NOTHING off master is ever published, to either registry, in either channel. The +# repo is public, so a fork pull-request head is fetchable by SHA from refs/pull/* — +# the ancestor-of-master check in `pack` is what keeps such a commit out. +# +# Two registries, two credentials, neither of them long-lived: # - GitHub Packages gets every build, prerelease included, authenticated with the -# built-in GITHUB_TOKEN (`packages: write`). No PAT, no service connection — -# this is what replaces the classic-PAT `PublishNPM` connection that UiPath -# revoked org-wide after the May 2026 npm supply-chain incident. -# - Public npmjs gets STABLE versions only, from master only, via Trusted -# Publishing (OIDC) — no tokens. Prereqs: an npmjs Trusted Publisher on -# @uipath/coreipc (UiPath/coreipc, workflow cd-npm.yml, env npm) + protected -# `npm` environment. @uipath/coreipc-web is not on npmjs yet, so it is -# GitHub-Packages-only until someone bootstraps that name manually. +# job-scoped GITHUB_TOKEN (`packages: write`). No PAT, no ADO service connection; +# both `coreipc` and `coreipc-web` are already linked to this repo. +# - Public npmjs gets STABLE versions only, via Trusted Publishing (OIDC) — no +# token at all. Prereq: an npmjs Trusted Publisher on @uipath/coreipc +# (UiPath/coreipc, workflow cd-npm.yml, env npm). @uipath/coreipc-web is not on +# npmjs yet, so it is GitHub-Packages-only until someone bootstraps that name. +# +# Both publish jobs sit behind protected environments (`github-packages`, `npm`). +# Configure required reviewers and "Protected branches only" on each — referencing an +# environment that does not exist auto-creates it UNPROTECTED, so until that is done +# the gate is decorative. on: repository_dispatch: @@ -43,7 +50,7 @@ env: jobs: pack: name: Pack tarballs - runs-on: uipath-ubuntu-latest + runs-on: ubuntu-latest outputs: version: ${{ steps.resolve.outputs.version }} stable: ${{ steps.resolve.outputs.stable }} @@ -105,20 +112,20 @@ jobs: echo "stable=$STABLE" >> "$GITHUB_OUTPUT" echo "Publishing $DISPATCHED (stable=$STABLE)" - # Enforce "only master reaches public npmjs" at the source level. CI builds - # (prerelease versions) may come from any branch — they only go to GitHub - # Packages — but a stable version that is not merged into master can never - # reach npmjs. Combined with repository_dispatch always running the - # default-branch copy of this file, unmerged code can't be released. - - name: Refuse stable releases that are not on master - if: steps.resolve.outputs.stable == 'true' + # Enforce "only master publishes" at the source level, for BOTH registries and + # both channels. A commit becomes an ancestor of master only by being merged, so + # this also rejects fork pull-request heads — those are fetchable by SHA from + # refs/pull/* on a public repo, but are never on master. Combined with the + # trigger always running the default-branch copy of this file, unmerged code + # cannot be published anywhere. + - name: Refuse commits that are not on master env: SHA: ${{ steps.ref.outputs.ref }} run: | set -euo pipefail git fetch --no-tags origin master if ! git merge-base --is-ancestor "$SHA" FETCH_HEAD; then - echo "::error::Commit $SHA is not an ancestor of origin/master — only master-merged code may be published to npmjs." + echo "::error::Commit $SHA is not an ancestor of origin/master — only master-merged code may be published." exit 1 fi echo "$SHA is on master — OK" @@ -183,7 +190,8 @@ jobs: publish-github-packages: name: Publish to GitHub Packages needs: pack - runs-on: uipath-ubuntu-latest + runs-on: ubuntu-latest + environment: github-packages # restores the approval gate ADO's NPM-Packages had permissions: contents: read packages: write # the whole auth story — GITHUB_TOKEN, no PAT @@ -218,7 +226,7 @@ jobs: needs: [pack, publish-github-packages] # Stable releases only. CI builds stop at GitHub Packages. if: needs.pack.outputs.stable == 'true' - runs-on: uipath-ubuntu-latest + runs-on: ubuntu-latest environment: npm # MANDATORY protection (required reviewers + default-branch only) permissions: contents: read From f165976b8c01920f0befb01227dd49a83d735ad3 Mon Sep 17 00:00:00 2001 From: Luca Rachiteanu Date: Tue, 15 Sep 2026 16:15:24 +0300 Subject: [PATCH 3/5] style: trim cd-npm comments to contract-only notes Co-Authored-By: Claude Opus 5 --- .github/workflows/cd-npm.yml | 89 ++++++++---------------------------- 1 file changed, 19 insertions(+), 70 deletions(-) diff --git a/.github/workflows/cd-npm.yml b/.github/workflows/cd-npm.yml index 2cc35932..4bc1d6a7 100644 --- a/.github/workflows/cd-npm.yml +++ b/.github/workflows/cd-npm.yml @@ -1,35 +1,5 @@ name: Publish @uipath/coreipc to GitHub Packages and npmjs -# Publishes the two JS client packages — @uipath/coreipc (NodeJS) and -# @uipath/coreipc-web — in two steps: a `pack` job that builds and produces the -# exact .tgz files, and publish jobs that push those same tarballs. Nothing is -# ever built twice, so what lands on npmjs is byte-identical to what lands on -# GitHub Packages. -# -# ADO stays primary: on a green NPM publish stage it POSTs a repository_dispatch -# (publish-npm) with the released commit SHA and the version that stage published, -# and this workflow builds & publishes that commit. There is no workflow_dispatch, -# so it can't fire by accident, and dispatch always runs the default-branch copy -# of this file. -# -# NOTHING off master is ever published, to either registry, in either channel. The -# repo is public, so a fork pull-request head is fetchable by SHA from refs/pull/* — -# the ancestor-of-master check in `pack` is what keeps such a commit out. -# -# Two registries, two credentials, neither of them long-lived: -# - GitHub Packages gets every build, prerelease included, authenticated with the -# job-scoped GITHUB_TOKEN (`packages: write`). No PAT, no ADO service connection; -# both `coreipc` and `coreipc-web` are already linked to this repo. -# - Public npmjs gets STABLE versions only, via Trusted Publishing (OIDC) — no -# token at all. Prereq: an npmjs Trusted Publisher on @uipath/coreipc -# (UiPath/coreipc, workflow cd-npm.yml, env npm). @uipath/coreipc-web is not on -# npmjs yet, so it is GitHub-Packages-only until someone bootstraps that name. -# -# Both publish jobs sit behind protected environments (`github-packages`, `npm`). -# Configure required reviewers and "Protected branches only" on each — referencing an -# environment that does not exist auto-creates it UNPROTECTED, so until that is done -# the gate is decorative. - on: repository_dispatch: types: [publish-npm] @@ -37,7 +7,6 @@ on: permissions: contents: read -# Never let two publish runs race for the same packages. concurrency: group: publish-npm-coreipc cancel-in-progress: false @@ -45,7 +14,7 @@ concurrency: env: JS_DIR: src/Clients/js CSPROJ: src/UiPath.CoreIpc/UiPath.CoreIpc.csproj - NODE_VERSION: '20.11.0' # same Node the ADO build uses (azp-nodejs.yaml) + NODE_VERSION: '20.11.0' jobs: pack: @@ -55,10 +24,8 @@ jobs: version: ${{ steps.resolve.outputs.version }} stable: ${{ steps.resolve.outputs.stable }} steps: - # Untrusted input (client_payload.*) is read via env, never inlined into a - # run: script, and validated — script-injection hardening. The SHA must be - # an exact 40-char commit (rejects tags/branch names); the version must be - # plain semver. + # client_payload is attacker-controlled if the dispatch token leaks: read it + # through env, never inline it into a run: script. - name: Resolve & validate dispatch payload id: ref env: @@ -79,17 +46,14 @@ jobs: echo "ref=$REF" >> "$GITHUB_OUTPUT" echo "version=$VER" >> "$GITHUB_OUTPUT" - - name: Checkout exact commit (full history) + - name: Checkout exact commit uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: ref: ${{ steps.ref.outputs.ref }} - fetch-depth: 0 # full graph — needed to prove the commit is on master + fetch-depth: 0 # the ancestry check below needs the full graph - # The csproj is the single source of truth for every package in this - # repo (.NET, Python, JS alike). A dispatched version is legal only if it is - # that version (a stable release) or that version plus an ADO build-number - # suffix (a CI build) — anything else means the payload and the commit - # disagree, and we refuse rather than publish a surprise version. + # A build-number suffix is the ADO CI convention (2.5.3-20260724-02); anything + # else means the payload and the commit disagree. - name: Resolve & validate version against the commit id: resolve env: @@ -112,12 +76,8 @@ jobs: echo "stable=$STABLE" >> "$GITHUB_OUTPUT" echo "Publishing $DISPATCHED (stable=$STABLE)" - # Enforce "only master publishes" at the source level, for BOTH registries and - # both channels. A commit becomes an ancestor of master only by being merged, so - # this also rejects fork pull-request heads — those are fetchable by SHA from - # refs/pull/* on a public repo, but are never on master. Combined with the - # trigger always running the default-branch copy of this file, unmerged code - # cannot be published anywhere. + # The repo is public, so fork PR heads are fetchable by SHA from refs/pull/*. + # Ancestry of master is what keeps one out. - name: Refuse commits that are not on master env: SHA: ${{ steps.ref.outputs.ref }} @@ -141,8 +101,8 @@ jobs: working-directory: ${{ env.JS_DIR }} run: npm ci - # Same stamping the ADO build does before `npm run build`: webpack copies the - # version out of package.json into both generated packages. + # Must precede the build: webpack copies this version into both generated + # packages. - name: Stamp version into package.json working-directory: ${{ env.JS_DIR }} run: npm version "${{ steps.resolve.outputs.version }}" --allow-same-version --no-git-tag-version @@ -151,10 +111,7 @@ jobs: working-directory: ${{ env.JS_DIR }} run: npm run build - # Step one of two: produce the tarballs. `npm pack` on the generated package - # directories, explicitly, rather than relying on the webpack shell plugin's - # side-effect pack into dist-packages/ — the artifact is what gets published, - # so it should be built by a step you can see. + # Deliberately not reusing the webpack shell plugin's pack into dist-packages/. - name: Pack working-directory: ${{ env.JS_DIR }} run: | @@ -164,8 +121,6 @@ jobs: npm pack ./dist/prepack/web --pack-destination "$RUNNER_TEMP/tarballs" ls -l "$RUNNER_TEMP/tarballs" - # Guard against a silently wrong artifact: exactly two tarballs, named for the - # two packages at the version we resolved. - name: Verify tarballs env: VERSION: ${{ steps.resolve.outputs.version }} @@ -191,10 +146,10 @@ jobs: name: Publish to GitHub Packages needs: pack runs-on: ubuntu-latest - environment: github-packages # restores the approval gate ADO's NPM-Packages had + environment: github-packages permissions: contents: read - packages: write # the whole auth story — GITHUB_TOKEN, no PAT + packages: write steps: - name: Retrieve tarballs uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -209,9 +164,6 @@ jobs: registry-url: https://npm.pkg.github.com scope: '@uipath' - # Step two of two: publish the tarballs the pack job produced, untouched. - # `packages/npm/coreipc` and `packages/npm/coreipc-web` are both already - # linked to UiPath/coreipc, so GITHUB_TOKEN is authorized for both. - name: Publish both packages env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -224,13 +176,12 @@ jobs: publish-npmjs: name: Publish to npmjs (Trusted Publishing) needs: [pack, publish-github-packages] - # Stable releases only. CI builds stop at GitHub Packages. if: needs.pack.outputs.stable == 'true' runs-on: ubuntu-latest - environment: npm # MANDATORY protection (required reviewers + default-branch only) + environment: npm permissions: contents: read - id-token: write # OIDC Trusted Publishing — no long-lived token + id-token: write steps: - name: Retrieve tarballs uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -252,11 +203,9 @@ jobs: npm install -g npm@^11.5.1 npm --version - # No NODE_AUTH_TOKEN: npm exchanges the job's OIDC token for a short-lived - # publish credential and attaches a provenance attestation automatically. - # Only the NodeJS package goes to npmjs — @uipath/coreipc-web has never been - # published there, and a Trusted Publisher can only be configured on a name - # that already exists. + # No NODE_AUTH_TOKEN by design — npm trades the job's OIDC token for a + # short-lived credential. @uipath/coreipc-web is absent because a Trusted + # Publisher can only be configured on a name that already exists on npmjs. - name: Publish @uipath/coreipc env: VERSION: ${{ needs.pack.outputs.version }} From 7d1d6e44d1d020d8affec6fb246cda1e7957f634 Mon Sep 17 00:00:00 2001 From: Luca Rachiteanu Date: Tue, 15 Sep 2026 16:22:08 +0300 Subject: [PATCH 4/5] ci: skip npm publish when the version is already on the registry Co-Authored-By: Claude Opus 5 --- .github/workflows/cd-npm.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd-npm.yml b/.github/workflows/cd-npm.yml index 4bc1d6a7..ffc16aa7 100644 --- a/.github/workflows/cd-npm.yml +++ b/.github/workflows/cd-npm.yml @@ -164,14 +164,22 @@ jobs: registry-url: https://npm.pkg.github.com scope: '@uipath' + # Skip-if-present keeps re-runs safe, matching skip-existing on the PyPI side. - name: Publish both packages env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ needs.pack.outputs.version }} run: | set -euo pipefail - npm publish "tarballs/uipath-coreipc-$VERSION.tgz" - npm publish "tarballs/uipath-coreipc-web-$VERSION.tgz" + publish_if_new() { + if npm view "$1@$VERSION" version >/dev/null 2>&1; then + echo "$1@$VERSION is already on GitHub Packages — skipping." + else + npm publish "$2" + fi + } + publish_if_new '@uipath/coreipc' "tarballs/uipath-coreipc-$VERSION.tgz" + publish_if_new '@uipath/coreipc-web' "tarballs/uipath-coreipc-web-$VERSION.tgz" publish-npmjs: name: Publish to npmjs (Trusted Publishing) @@ -206,7 +214,16 @@ jobs: # No NODE_AUTH_TOKEN by design — npm trades the job's OIDC token for a # short-lived credential. @uipath/coreipc-web is absent because a Trusted # Publisher can only be configured on a name that already exists on npmjs. + # Queried over plain HTTPS, not `npm view`: setup-node writes an _authToken + # placeholder for a token this job deliberately does not have. - name: Publish @uipath/coreipc env: VERSION: ${{ needs.pack.outputs.version }} - run: npm publish "tarballs/uipath-coreipc-$VERSION.tgz" --access public + run: | + set -euo pipefail + code=$(curl -sS -o /dev/null -w '%{http_code}' "https://registry.npmjs.org/@uipath%2Fcoreipc/$VERSION") + if [ "$code" = "200" ]; then + echo "@uipath/coreipc@$VERSION is already on npmjs — skipping." + exit 0 + fi + npm publish "tarballs/uipath-coreipc-$VERSION.tgz" --access public From 9f7a64efe0d68fa6330121ac53d712fb084e7832 Mon Sep 17 00:00:00 2001 From: Luca Rachiteanu Date: Tue, 15 Sep 2026 16:43:41 +0300 Subject: [PATCH 5/5] chore: move the js package metadata fix to its own PR Co-Authored-By: Claude Opus 5 --- src/Clients/js/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Clients/js/package.json b/src/Clients/js/package.json index 136643ce..4a575435 100644 --- a/src/Clients/js/package.json +++ b/src/Clients/js/package.json @@ -39,14 +39,14 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/UiPath/coreipc.git" + "url": "git+https://github.com/eduard-dumitru/js-multitargeting.git" }, "author": "Eduard Dumitru", "license": "MIT", "bugs": { - "url": "https://github.com/UiPath/coreipc/issues" + "url": "https://github.com/eduard-dumitru/js-multitargeting/issues" }, - "homepage": "https://github.com/UiPath/coreipc#readme", + "homepage": "https://github.com/eduard-dumitru/js-multitargeting#readme", "devDependencies": { "@babel/core": "^7.20.2", "@babel/preset-env": "^7.20.2",