Skip to content

Automate SDK bump PRs, and fix the CODEOWNERS and CDN updater bugs - #101

Merged
ritz078 merged 3 commits into
mainfrom
ritz078/automatic-update
Sep 1, 2026
Merged

Automate SDK bump PRs, and fix the CODEOWNERS and CDN updater bugs#101
ritz078 merged 3 commits into
mainfrom
ritz078/automatic-update

Conversation

@ritz078

@ritz078 ritz078 commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

⚡ TL;DR

A new @nutrient-sdk/viewer release now opens its own bump PR, already formatted and e2e-tested, instead of waiting for someone to notice. Two silent bugs that let stale versions sit in the repo for months are fixed along the way.

🎯 What this improves

  • 🤖 No manual bumps: a daily job detects a new release, bumps every example, and opens the PR by itself.
  • 👀 Reviewers actually get asked: CODEOWNERS pointed at @PSPDFKit/nickel, a team that does not exist, so GitHub silently requested nobody on every PR in this repo.
  • 🔎 Stale CDN versions surface instead of hiding: examples/gatsbyjs sat eight minor versions behind because of a one-character typo nothing was checking.
  • 🛑 Failures stop being green: a run that pushes a branch and then dies, or an e2e suite that fails, now fails the job rather than reporting success.
  • ♻️ Closed bumps stay closed: declining a version no longer means it reappears tomorrow, and every day after.

🔧 What changed

  • Was: version bumps were run by hand via npm run update-nutrient-version; the CDN updater keyed one example as gatsby while the directory is gatsbyjs, so that entry was never looked up and never updated; CODEOWNERS named a nonexistent team.
  • Now: .github/workflows/update-nutrient-sdk.yml runs daily (and on workflow_dispatch, optionally against a given version). scripts/check-nutrient-update.sh decides whether to act and refuses to start a bump already in flight. update-nutrient-in-cdn.js rejects a map key matching no directory, rejects a mapped file that is missing, and rejects a replace that matched nothing. A workflow-level grep then catches any example carrying a CDN tag that the map does not know about at all.
  • Impact: no user-facing or API change. Repository automation and CI only.

🔍 Root cause

  • CDN staleness: cdnOcurrences is a hand-maintained map from example name to file list. A key that matches no directory is simply never looked up, so the gatsby/gatsbyjs typo produced no error, no warning, and no diff. examples/nuxtjs had a second variant of the same failure: it was absent from the map entirely.
  • The replace could not fail either: the old pattern /pspdfkit-web@([0-9]+.[0-9]+.[0-9]+)?/g made the version group optional, so a file with no version still "matched", got rewritten byte-identical, and was reported as updated.
  • CODEOWNERS: GitHub treats an unresolvable team as a no-op rather than an error, so @PSPDFKit/nickel reviewed nothing and said nothing.

🤔 Why

  • Decision: guard the hand-maintained map at three levels in the script, and add one independent check in the workflow. The script guards give a precise early error; the workflow grep is the one that catches an example missing from the map, which no in-script guard can see.
  • Alternative considered: deriving the file map by scanning examples/ for CDN tags, removing the hand-maintained list entirely. Rejected for this PR: it trades a small include-list for an exclude-list (examples/salesforce/README.md documents the CDN URL with a worked @1.0.0 example and must never be bumped), and it rewrites a script this PR is already changing. Worth doing separately.
  • Decision: run the e2e suite inside the bump job. A PR opened with GITHUB_TOKEN does not trigger the Biome or Playwright workflows, and main requires no status checks, so the bump would otherwise arrive with no signal at all.
  • Decision: a failing suite opens the PR as a draft and fails the run. GitHub does not request code owners on drafts, so without the explicit failure the bad path would notify nobody while staying green.

🧪 How to test

  • Detection, without opening anything:
./scripts/check-nutrient-update.sh            # 1.18.0 -> 1.21.0, should_update=true
./scripts/check-nutrient-update.sh 1.18.0     # already on 1.18.0
./scripts/check-nutrient-update.sh 1.19.0     # a pull request already exists
./scripts/check-nutrient-update.sh 1.20.0-nightly   # exits 1, prereleases are never shipped
  • The CDN guard that reproduces the original bug. Rename the gatsbyjs key to gatsby and it now refuses to run:
node scripts/update-nutrient-in-cdn.js webpack 9.9.9
# Keys matching no example directory: gatsby
  • Confirm the salesforce README is left alone while its Visualforce page is bumped:
node scripts/update-nutrient-in-cdn.js salesforce 9.9.9
grep -rn 'pspdfkit-web@' examples/salesforce/ | grep -v node_modules
git checkout -- examples/salesforce/
  • Lint and version consistency:
npx @biomejs/biome@1.9.4 ci .
./scripts/check-biome-version.sh
  • End to end, against a version already released:
gh workflow run update-nutrient-sdk.yml -f version=1.21.0

⚠️ Risks / notes

  • 🔑 Needs "Allow GitHub Actions to create and approve pull requests" enabled for the repo, or the gh pr create step fails with a permissions error.
  • 📉 Two examples are still behind on this branch: nuxtjs is on 1.3.0 and gatsbyjs on 1.8.0. This PR does not bump them; it makes the next scheduled run bump them and fail loudly if it cannot.
  • 🧹 Formatting is staged from examples/ only: pnpm run format writes unsafe Biome fixes repository-wide, so the commit step uses git add -u examples/ and discards the rest.
  • ⏱️ The job installs every example and runs the full e2e suite, so a bump run takes a while; the timeout is set to 90 minutes.
  • 🧭 update-nutrient-in-examples.sh still hard-codes its 20 example directories. A new example is silently never bumped, which is the same class of drift this PR fixes in the CDN map. Left out because generalizing it changes which examples get bumped (wasm-benchmark pins a range deliberately) and needs its own verification.

The CODEOWNERS file referenced @PSPDFKit/nickel, a team that does not
exist, so no review was ever auto-requested. Point it at @PSPDFKit/web,
which has write access to this repository.

The CDN updater keyed its map on "gatsby" while the directory is
"gatsbyjs", and omitted nuxtjs and the Salesforce README entirely. Those
three files were left pinned at 1.8.0, 1.3.0 and 1.0.0 while every other
example moved to 1.18.0. Correct the key, add the missing entries, and
validate all paths before writing so a moved file fails the run instead
of silently half-updating an example.

Both scripts now take the target version as a required argument instead
of resolving @latest at install time, so a release landing mid-run cannot
produce a bump whose title and lockfiles disagree.
Adds a daily workflow that checks the npm latest dist-tag, bumps every
example, and opens a pull request. @PSPDFKit/web is requested for review
through CODEOWNERS rather than by the workflow, so no token needs
permission to resolve org teams.

Detection refuses to act twice on the same release: it exits when the
repository is already on the version, when the branch exists, or when a
pull request for it was opened before, so a bump closed without merging
is not reopened on the next run. Prereleases are rejected outright, so
nightly builds never trigger a bump.

Biome and the e2e suite run inside the job before the pull request is
opened. A pull request created with GITHUB_TOKEN does not trigger the
Biome or Playwright workflows, and main requires no status checks, so a
bump would otherwise arrive with nothing having verified it. A failing
suite still opens the pull request, as a draft, with the report attached.

pnpm is pinned to 10 because pnpm 11 stops reading the pnpm.overrides
field in package.json, which would silently drop the security overrides
added in #98.
@ebardonerocheel213-jpg

This comment has been minimized.

@ritz078
ritz078 marked this pull request as draft August 27, 2026 08:32
Refuse to reopen a closed bump, fail loudly when a run stops part-way,
and reject a CDN map key or file that matches nothing. Exempt the
salesforce README, whose version is an illustration rather than a pin.
@ritz078
ritz078 requested a review from MahmoudElsayad August 27, 2026 09:05
@ritz078 ritz078 self-assigned this Aug 27, 2026
@ritz078
ritz078 marked this pull request as ready for review August 27, 2026 09:05
@ritz078
ritz078 enabled auto-merge (squash) August 27, 2026 09:08

@miguelcalderon miguelcalderon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR looks solid to me. The verification table in the description made this easy to review.

I've left some comments, none should be a blocker, just for your consideration. Please make sure to create an issue for generalizing the hardcoded example list in update-nutrient-in-examples.sh, I agree it belongs in a follow-up.

Thank you for this! This is removing overhead.

Comment on lines +67 to +71
stale="$(grep -rEn "pspdfkit-web@[0-9]+\.[0-9]+\.[0-9]+" examples/ \
--exclude-dir=node_modules --exclude-dir=dist \
--exclude-dir=.next --exclude-dir=.nuxt \
| grep -v "pspdfkit-web@${VERSION}" \
| grep -v '^examples/salesforce/README.md:' || true)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small blind spots in this filter:

  1. grep -v "pspdfkit-web@${VERSION}" drops the whole line, so a line containing both an updated and a stale reference (e.g. a README listing two CDN URLs on one line) would hide the stale one.
  2. The dots in ${VERSION} are regex metacharacters, so the exclusion is slightly looser than intended.

Both are covered by making the first grep emit one match per line and the exclusion a fixed string:

stale="$(grep -rEon "pspdfkit-web@[0-9]+\.[0-9]+\.[0-9]+" examples/ \
  --exclude-dir=node_modules --exclude-dir=dist \
  --exclude-dir=.next --exclude-dir=.nuxt \
  | grep -vF "pspdfkit-web@${VERSION}" \
  | grep -v '^examples/salesforce/README.md:' || true)"

Low probability in practice, so fine as a follow-up if you prefer.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #103, using your form verbatim.

Reproduced the whole-line case before changing it, and it is worse than "hides the stale one" — the output is completely empty:

$ echo "pspdfkit-web@1.21.0 and pspdfkit-web@1.8.0 on one line" > /tmp/t.txt
$ grep -rEn  "pspdfkit-web@[0-9]+\.[0-9]+\.[0-9]+" /tmp/t.txt | grep -v  "pspdfkit-web@1.21.0"
$ grep -rEon "pspdfkit-web@[0-9]+\.[0-9]+\.[0-9]+" /tmp/t.txt | grep -vF "pspdfkit-web@1.21.0"
/tmp/t.txt:1:pspdfkit-web@1.8.0

examples/salesforce/force-app/.../Nutrient_InitNutrient.page already carries two CDN references, on lines 7 and 16, so the shape is one edit away from being real here.

Comment on lines +16 to +20
if [ -n "${requested}" ]; then
latest="${requested}"
else
latest="$(curl -fsSL --retry 3 --retry-delay 2 "${DIST_TAGS_URL}" | jq -er '.latest')"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A manually requested version is taken at face value: nothing checks it exists on the registry, and nothing prevents it being older than current (the script only tests inequality, so a dispatch with 1.17.0 would happily open a downgrade PR titled as a bump).

The nonexistent-version case fails soon enough (the first npm install errors), just with a less obvious message. The downgrade case may even be a feature (rollback) — if so, a one-line comment saying it's intentional would help; if not, a dist-tags/npm view existence check here would make both fail in the check step with a clear message.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both, in the end — #103 adds a registry existence check and keeps the downgrade, with a comment saying it is deliberate.

The typo case had no upside, and it now fails in the check step with the version named rather than as an npm 404 well into the bump, after the branch already exists:

$ ./scripts/check-nutrient-update.sh 1.17.99
@nutrient-sdk/viewer@1.17.99 is not published on the registry.

The downgrade is a feature: dispatching an older version is the only rollback path there is, so it stays, now documented as such. The check only runs for a dispatched version, so the scheduled path makes no extra request.

Comment thread scripts/update-nutrient-in-examples.sh
Comment on lines +168 to +175
- name: Fail the run when the e2e suite failed
if: steps.check.outputs.should_update == 'true' && steps.e2e.outcome != 'success'
env:
VERSION: ${{ steps.check.outputs.version }}
run: |
echo "::error::E2E failed for ${VERSION}. The pull request was opened as a draft,"
echo "::error::and GitHub does not request code owners on drafts."
exit 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once this draft exists, check-nutrient-update.sh will skip that version on every future run ("a pull request already exists") — which is the right behavior for a deliberately declined bump, but it also means a transient e2e flake permanently parks the version on a human, and re-running the e2es isn't possible from the PR itself (no checks are attached to it).

The recovery path (mark ready + verify by hand, or close the PR, delete the branch, and workflow_dispatch again) isn't written down anywhere. Suggest adding it to the draft PR body in the failure branch above, so the person looking at the draft knows what to do without archaeology.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to the draft body in #103. The failure branch now appends a "This draft needs a human" section spelling out the three ways out — accept it after checking the run log, close and delete the branch then re-dispatch, or close it and leave the branch so the version is never proposed again.

Rendered both variants by running the step's own script to confirm the section appears only on failure.

@ritz078
ritz078 merged commit 08b4c9d into main Sep 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants