From 6ede33cf094f6c5376403dc68fecc75929551ef4 Mon Sep 17 00:00:00 2001 From: Rob Reed Date: Thu, 27 Aug 2026 19:35:34 -0700 Subject: [PATCH 1/6] Publish releases to GitHub Packages under the @plexinc scope. Replace the git tag and xelp/dist shadow release with a workflow that publishes to GitHub Packages as @plexinc/, matching how the other Plex client repos consume private packages. Authentication is the workflow GITHUB_TOKEN, so the repository stores no publishing secret, and the package name is rewritten in CI rather than on xelp/main, leaving upstream merges unaffected. The lint and test suites previously ran as a side effect of npm version, which triggers preversion. Setting the version with npm pkg set skips lifecycle scripts, so they now run as their own steps. Consumers keep the upstream package name by installing through an npm alias, so imports, bin names and plugin references do not change. --- .github/workflows/xelp_npm_release.yml | 182 +++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 .github/workflows/xelp_npm_release.yml diff --git a/.github/workflows/xelp_npm_release.yml b/.github/workflows/xelp_npm_release.yml new file mode 100644 index 0000000..eff8f13 --- /dev/null +++ b/.github/workflows/xelp_npm_release.yml @@ -0,0 +1,182 @@ +name: Xelp npm Release + +# Builds the current xelp/main, publishes it to GitHub Packages as +# @plexinc/, then tags the commit and creates a GitHub release. +# +# Authentication is the workflow's own GITHUB_TOKEN, so there is no secret to +# provision or rotate. Consumers authenticate the way every other Plex client +# repo does, with a personal access token carrying read:packages. +# +# This replaces xelp_shadow_release.yml, which committed dist/ to the xelp/dist +# branch and served the package to consumers through a git tag. Run one or the +# other, never both: they compute the same version string and so want the same +# tag, and the shadow release force pushes it. +# +# Keep the shadow release around until roku-client, the only consumer of these +# forks, is installing from the @plexinc package. Then delete it. +# + +on: + workflow_dispatch: + inputs: + dryRun: + description: Build and pack, but do not publish, tag, or release. + type: boolean + default: false + +# packages: write is the publish permission. contents: write is only for the +# tag and the release. +# +permissions: + contents: write + packages: write + +jobs: + release: + runs-on: blacksmith-2vcpu-ubuntu-2404 + env: + # Needed by every npm step, to read @plexinc dependencies as well as to + # publish. Setting it per step is easy to get wrong: a fork that depends + # on another @plexinc package cannot even run npm ci without it. + # + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Check out xelp/main + uses: actions/checkout@v5 + with: + ref: xelp/main + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: 20 + registry-url: https://npm.pkg.github.com + scope: '@plexinc' + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Work out the package name and version + id: release + run: | + set -euo pipefail + + UPSTREAM_NAME=$(node -p "require('./package.json').name") + + # Re-scope to @plexinc, dropping any existing scope, so + # @rokucommunity/bslint becomes @plexinc/bslint. GitHub Packages + # requires the scope to match the owner of this repository. + # + SCOPED_NAME="@plexinc/${UPSTREAM_NAME##*/}" + + # Version scheme, unchanged from the shadow release: the upstream major + # and minor, then the build date with the upstream patch appended, so + # 0.70.3 built on 2026-08-28 becomes 0.70.202608283. + # + CURRENT_VERSION=$(node -p "require('./package.json').version") + BASE_VERSION=${CURRENT_VERSION%%[-+]*} + + IFS='.' read -r -a PARTS <<< "$BASE_VERSION" + if [ ${#PARTS[@]} -ne 3 ]; then + echo "::error::Version $CURRENT_VERSION is not MAJOR.MINOR.PATCH" + exit 1 + fi + + VERSION="${PARTS[0]}.${PARTS[1]}.$(date -u +'%Y%m%d')${PARTS[2]}" + + { + echo "upstream_name=$UPSTREAM_NAME" + echo "scoped_name=$SCOPED_NAME" + echo "version=$VERSION" + echo "metadata_version=$VERSION+xelp-$(git rev-parse --short HEAD)" + } >> "$GITHUB_OUTPUT" + + # Published versions are immutable and tags are no longer force-pushed, so + # a same day re-run would collide twice over. Fail before doing the work. + # + - name: Fail if this version is already published + env: + SCOPED_NAME: ${{ steps.release.outputs.scoped_name }} + VERSION: ${{ steps.release.outputs.version }} + run: | + if npm view "$SCOPED_NAME@$VERSION" version >/dev/null 2>&1; then + echo "::error::$SCOPED_NAME@$VERSION is already published. Land another commit, or wait for tomorrow's date stamp." + exit 1 + fi + + # The upstream lockfile is what keeps the transitive dependencies on + # working versions, so install from it rather than re-resolving. A clean + # re-resolve floats vscode-languageserver-protocol onto an exports only + # release that the TypeScript build cannot import. + # + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + # The shadow release ran the lint and test suites as a side effect of + # npm version, which triggers the preversion script. Setting the version + # through npm pkg set does not run lifecycle scripts, so the gate runs + # here where a failure names the step that failed. + # + - name: Lint + run: npm run lint + + - name: Test + run: npm test + + # The rename happens here and is never committed to xelp/main, so a merge + # from upstream never has to resolve a changed package name. The repository + # URL is what links the package to this repo, and GitHub Packages rejects + # the publish if it points anywhere else. + # + - name: Rewrite the package metadata for the @plexinc scope + env: + SCOPED_NAME: ${{ steps.release.outputs.scoped_name }} + VERSION: ${{ steps.release.outputs.version }} + run: | + npm pkg set name="$SCOPED_NAME" + npm pkg set version="$VERSION" + npm pkg set repository.url="git+https://github.com/${{ github.repository }}.git" + + - name: Publish to GitHub Packages + if: ${{ !inputs.dryRun }} + run: npm publish + + - name: Pack without publishing + if: ${{ inputs.dryRun }} + run: npm pack --dry-run + + - name: Tag the release + if: ${{ !inputs.dryRun }} + env: + VERSION: ${{ steps.release.outputs.version }} + METADATA_VERSION: ${{ steps.release.outputs.metadata_version }} + run: | + git tag -a "$VERSION" -m "Release $METADATA_VERSION" + git push origin "$VERSION" + + - name: Create the GitHub release + if: ${{ !inputs.dryRun }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + UPSTREAM_NAME: ${{ steps.release.outputs.upstream_name }} + SCOPED_NAME: ${{ steps.release.outputs.scoped_name }} + VERSION: ${{ steps.release.outputs.version }} + METADATA_VERSION: ${{ steps.release.outputs.metadata_version }} + run: | + gh release create "$VERSION" \ + --repo "$GITHUB_REPOSITORY" \ + --title "$SCOPED_NAME $VERSION" \ + --notes "Built from \`$METADATA_VERSION\`. + + Consume it with an alias, so the package keeps its upstream name inside \`node_modules\`: + + \`\`\`json + \"$UPSTREAM_NAME\": \"npm:$SCOPED_NAME@$VERSION\" + \`\`\`" \ + --prerelease From 6a01a23066b1f00aef10efedc6cc54c6b36a7ae8 Mon Sep 17 00:00:00 2001 From: Rob Reed Date: Thu, 27 Aug 2026 19:35:37 -0700 Subject: [PATCH 2/6] Disable the shadow release in favour of the npm release. Both workflows compute the same MAJOR.MINOR. version and so want the same tag, and the shadow release force pushes it, so running the two against one commit means one clobbers the other. Park the shadow release rather than delete it, matching the .disabled convention already used in this repo, so restoring it is a rename. roku-client is the only consumer of this fork, and it keeps installing from the existing git tag until it moves to the @plexinc package, so nothing depends on this workflow staying runnable in the meantime. --- .../{xelp_shadow_release.yml => xelp_shadow_release.yml.disabled} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{xelp_shadow_release.yml => xelp_shadow_release.yml.disabled} (100%) diff --git a/.github/workflows/xelp_shadow_release.yml b/.github/workflows/xelp_shadow_release.yml.disabled similarity index 100% rename from .github/workflows/xelp_shadow_release.yml rename to .github/workflows/xelp_shadow_release.yml.disabled From e0bd805ee1ce5e2f78a7f8df3b06930d22413b5c Mon Sep 17 00:00:00 2001 From: Rob Reed Date: Thu, 27 Aug 2026 19:35:37 -0700 Subject: [PATCH 3/6] Depend on the published @plexinc/brighterscript package. Point the runtime dependency at the GitHub Packages release rather than a git tag, so installing this package no longer clones the brighterscript repo and consumers no longer need SSH access to GitHub to resolve it. The .npmrc tells npm where the @plexinc scope lives, matching the other Plex client repos. The range is a caret on the minor rather than an exact pin, so it dedupes onto whatever 0.70 build the consuming project pins. Two copies of brighterscript in one tree would break the plugin instanceof checks that bsc relies on. The swap was made with a targeted npm install rather than by regenerating the lockfile, which changed one entry out of 553. A clean re-resolve floats vscode-languageserver-protocol onto a release that ships exports with no main or types entry, and the TypeScript build cannot import it. --- .npmrc | 3 +++ package-lock.json | 8 +++++--- package.json | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..3ccf31e --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +# Use GitHub for @plexinc packages. +@plexinc:registry=https://npm.pkg.github.com/ +//npm.pkg.github.com/:always-auth=true diff --git a/package-lock.json b/package-lock.json index 57e74b5..44a390f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.7.21", "license": "MIT", "dependencies": { - "brighterscript": "https://github.com/plexinc/brighterscript.git#0.70.202603123", + "brighterscript": "npm:@plexinc/brighterscript@^0.70.0", "glob-all": "^3.3.0", "jsonc-parser": "^3.0.0", "source-map": "0.7.4", @@ -1559,8 +1559,10 @@ } }, "node_modules/brighterscript": { - "version": "0.70.202603123", - "resolved": "git+ssh://git@github.com/plexinc/brighterscript.git#b1c0191837cb61b938015f9ba086348820513f97", + "name": "@plexinc/brighterscript", + "version": "0.70.202608283", + "resolved": "https://npm.pkg.github.com/download/@plexinc/brighterscript/0.70.202608283/f0a4be489585bc03e84d8e3a012494f4872fc347", + "integrity": "sha512-ixJVQQmh1UO1th6/0p8Sh9yg1hXctWyK328o5Oi0gi1UhtuWIUftrwtLXRks3ZSpSAgf+Wsb+XoRY65N79cDeQ==", "license": "MIT", "dependencies": { "@rokucommunity/bslib": "^0.1.1", diff --git a/package.json b/package.json index 7a08da2..5c2d012 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "brighterscript-formatter": "dist/cli.js" }, "dependencies": { - "brighterscript": "https://github.com/plexinc/brighterscript.git#0.70.202603123", + "brighterscript": "npm:@plexinc/brighterscript@^0.70.0", "glob-all": "^3.3.0", "jsonc-parser": "^3.0.0", "source-map": "0.7.4", From 7a81f485a342994baff80a8961c18f9727ed19a3 Mon Sep 17 00:00:00 2001 From: Rob Reed Date: Thu, 27 Aug 2026 19:51:17 -0700 Subject: [PATCH 4/6] Give CI access to the @plexinc registry. The build workflow runs npm ci, and this package now depends on @plexinc/brighterscript from GitHub Packages, so the install needs a token. Without it the job fails with a 401 before it gets as far as compiling. setup-node points the @plexinc scope at the registry and the token is given only to the install step, which is the only step here that reaches the network. --- .github/workflows/build.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 049b905..400c0bf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,5 +27,10 @@ jobs: with: node-version: "22" architecture: 'x64' # fix for macos-latest - - run: npm ci + registry-url: 'https://npm.pkg.github.com' + scope: '@plexinc' + - name: npm ci + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npm ci - run: npm run preversion From 0c94a517e7285fecde3bacedc0870ea28841a778 Mon Sep 17 00:00:00 2001 From: Rob Reed Date: Thu, 27 Aug 2026 19:51:21 -0700 Subject: [PATCH 5/6] Scope the registry token to the steps that use npm. The token was on the job, which put it in the environment of every step, including any added later. It is only needed by the three steps that talk to the registry: the published version check, the install, and the publish. Building, linting, testing and packing do not reach the network. A step that runs npm against the registry has to opt in from now on, which is easy to forget, so the reason is written next to the first one. --- .github/workflows/xelp_npm_release.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/xelp_npm_release.yml b/.github/workflows/xelp_npm_release.yml index eff8f13..508e2c3 100644 --- a/.github/workflows/xelp_npm_release.yml +++ b/.github/workflows/xelp_npm_release.yml @@ -34,12 +34,6 @@ permissions: jobs: release: runs-on: blacksmith-2vcpu-ubuntu-2404 - env: - # Needed by every npm step, to read @plexinc dependencies as well as to - # publish. Setting it per step is easy to get wrong: a fork that depends - # on another @plexinc package cannot even run npm ci without it. - # - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Check out xelp/main uses: actions/checkout@v5 @@ -99,6 +93,12 @@ jobs: # - name: Fail if this version is already published env: + # Only the three steps that talk to the registry get the token: this + # one, the install, and the publish. Building, linting, testing and + # packing do not reach the network, so they do not need it. Add it to + # any new step that runs npm against the registry. + # + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCOPED_NAME: ${{ steps.release.outputs.scoped_name }} VERSION: ${{ steps.release.outputs.version }} run: | @@ -113,6 +113,8 @@ jobs: # release that the TypeScript build cannot import. # - name: Install dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm ci - name: Build @@ -145,6 +147,8 @@ jobs: - name: Publish to GitHub Packages if: ${{ !inputs.dryRun }} + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm publish - name: Pack without publishing From f145eaf784fcfc59ffcc3e4fb4eaec540e551fb8 Mon Sep 17 00:00:00 2001 From: Rob Reed Date: Thu, 27 Aug 2026 20:35:23 -0700 Subject: [PATCH 6/6] Read @plexinc dependencies with the organization token. A repository's own GITHUB_TOKEN cannot read a package owned by another repository, so npm ci fails with a 403 on @plexinc/brighterscript. GH_TOKEN is the token the other Plex client repos already use for this. Only the install steps change. The published version check and the publish act on this repository's own package, where GITHUB_TOKEN is correct and already works. The fallback keeps a fork that has no @plexinc dependencies working on the repo token alone. --- .github/workflows/build.yml | 2 +- .github/workflows/xelp_npm_release.yml | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 400c0bf..ea0a90d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,6 +31,6 @@ jobs: scope: '@plexinc' - name: npm ci env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} run: npm ci - run: npm run preversion diff --git a/.github/workflows/xelp_npm_release.yml b/.github/workflows/xelp_npm_release.yml index 508e2c3..6e91c6f 100644 --- a/.github/workflows/xelp_npm_release.yml +++ b/.github/workflows/xelp_npm_release.yml @@ -114,7 +114,12 @@ jobs: # - name: Install dependencies env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Dependencies can live in another repo's package, and a repository's + # own GITHUB_TOKEN cannot read those, so prefer the organization token. + # The fallback keeps this working in a fork that has no @plexinc + # dependencies, where the repo token is enough. + # + NODE_AUTH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} run: npm ci - name: Build