Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions .github/workflows/cd-npm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
name: Publish @uipath/coreipc to GitHub Packages and npmjs

on:
repository_dispatch:
types: [publish-npm]

permissions:
contents: read

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'

jobs:
pack:
name: Pack tarballs
runs-on: ubuntu-latest
outputs:
version: ${{ steps.resolve.outputs.version }}
stable: ${{ steps.resolve.outputs.stable }}
steps:
# 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:
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
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0 # the ancestry check below needs the full graph

# 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:
DISPATCHED: ${{ steps.ref.outputs.version }}
run: |
set -euo pipefail
BASE=$(grep -oPm1 '(?<=<Version>)[^<]+' "$CSPROJ" | tr -d '[:space:]')
if [ -z "$BASE" ]; then
echo "::error::No <Version> 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 <Version> '$BASE' at this commit."
exit 1
fi
echo "version=$DISPATCHED" >> "$GITHUB_OUTPUT"
echo "stable=$STABLE" >> "$GITHUB_OUTPUT"
echo "Publishing $DISPATCHED (stable=$STABLE)"

# 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 }}
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."
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

# 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

- name: Build
working-directory: ${{ env.JS_DIR }}
run: npm run build

# Deliberately not reusing the webpack shell plugin's pack into dist-packages/.
- 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"

- 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: ubuntu-latest
environment: github-packages
permissions:
contents: read
packages: write
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'

# 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
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)
needs: [pack, publish-github-packages]
if: needs.pack.outputs.stable == 'true'
runs-on: ubuntu-latest
environment: npm
permissions:
contents: read
id-token: write
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 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: |
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
100 changes: 62 additions & 38 deletions src/CI/azp-js.publish-npm.steps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,51 +31,75 @@ 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:
archiveFilePatterns: '$(Pipeline.Workspace)/NPM package/*.zip'
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)
4 changes: 3 additions & 1 deletion src/CI/azp-publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)'
Expand Down