Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Web SDK team owns all examples
* @PSPDFKit/nickel
* @PSPDFKit/web
175 changes: 175 additions & 0 deletions .github/workflows/update-nutrient-sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
name: Update Nutrient SDK

on:
schedule:
# Daily, deliberately off the hour: GitHub deprioritises schedules that
# bunch on :00, which delays them further.
- cron: "17 6 * * *"
workflow_dispatch:
inputs:
version:
description: "Version to bump to. Defaults to the npm latest dist-tag."
required: false
type: string

permissions:
contents: write
pull-requests: write

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
update:
timeout-minutes: 90
runs-on: ubuntu-latest
env:
# Root install runs `prepare`, which points core.hooksPath at .husky and
# would gate the bot's commit on lint-staged and the Biome version check.
HUSKY: "0"
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Setup pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with:
version: 10

- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .tool-versions

- name: Check for a new SDK release
id: check
env:
GH_TOKEN: ${{ github.token }}
REQUESTED_VERSION: ${{ inputs.version }}
run: ./scripts/check-nutrient-update.sh "${REQUESTED_VERSION:-}"

- name: Bump the SDK in every example
if: steps.check.outputs.should_update == 'true'
env:
VERSION: ${{ steps.check.outputs.version }}
run: ./scripts/update-nutrient-in-examples.sh "$VERSION"

- name: Check every CDN reference was bumped
if: steps.check.outputs.should_update == 'true'
env:
VERSION: ${{ steps.check.outputs.version }}
run: |
# The CDN file map in update-nutrient-in-cdn.js is hand-maintained, so an
# example that gains a script tag without an entry is left behind in
# silence. examples/salesforce/README.md is exempt: its version is an
# illustration, not a pin.
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)"
Comment on lines +67 to +71

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.


if [ -n "$stale" ]; then
echo "CDN references not updated to ${VERSION}:" >&2
echo "$stale" >&2
exit 1
fi

- name: Install root dependencies
if: steps.check.outputs.should_update == 'true'
run: pnpm install --frozen-lockfile

- name: Format
if: steps.check.outputs.should_update == 'true'
run: pnpm run format

- name: Install Playwright browsers
if: steps.check.outputs.should_update == 'true'
run: pnpm exec playwright install chromium --with-deps

# A pull request 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.
- name: Run e2e smoke tests
id: e2e
if: steps.check.outputs.should_update == 'true'
continue-on-error: true
run: pnpm run e2e-tests

- name: Upload Playwright report
if: steps.check.outputs.should_update == 'true' && steps.e2e.outcome != 'success'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: playwright-report
path: playwright-report/
retention-days: 30

- name: Commit and push the bump
if: steps.check.outputs.should_update == 'true'
env:
VERSION: ${{ steps.check.outputs.version }}
BRANCH: ${{ steps.check.outputs.branch }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
# `pnpm run format` writes unsafe Biome fixes repository-wide, and a dev
# server may leave build output behind; neither belongs in a bump.
git add -u examples/
if git diff --cached --quiet; then
echo "Detection reported ${VERSION} was needed but nothing changed." >&2
exit 1
fi
git commit -m "Update examples with Nutrient SDK version $VERSION"
git push origin "$BRANCH"

- name: Open the pull request
if: steps.check.outputs.should_update == 'true'
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.check.outputs.version }}
CURRENT: ${{ steps.check.outputs.current }}
BRANCH: ${{ steps.check.outputs.branch }}
E2E_OUTCOME: ${{ steps.e2e.outcome }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
BASE: ${{ github.ref_name }}
run: |
if [ "$E2E_OUTCOME" = "success" ]; then
e2e_result="βœ… passed"
draft=""
else
e2e_result="❌ failed. The failing examples are named at the end of the run log."
draft="--draft"
fi

cat > /tmp/pr-body.md <<BODY
Automated bump of \`@nutrient-sdk/viewer\` from ${CURRENT} to ${VERSION}.

## Verification

Checks ran inside the workflow before this pull request was opened. A
pull request created by \`GITHUB_TOKEN\` does not trigger the Biome or
Playwright workflows, so the checks tab here will be empty.

- Formatting (\`pnpm run format\`): applied to \`examples/\`
- E2E smoke tests (\`pnpm run e2e-tests\`): ${e2e_result}

[Workflow run](${RUN_URL})
BODY

gh pr create \
--title "Update examples with Nutrient SDK version $VERSION" \
--body-file /tmp/pr-body.md \
--base "$BASE" \
--head "$BRANCH" \
$draft

- 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
Comment on lines +168 to +175

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.

12 changes: 10 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ nutrient-web-examples/
β”‚ β”œβ”€β”€ audit-dependencies.sh # npm audit fix across examples
β”‚ β”œβ”€β”€ update-nutrient-in-examples.sh # Bump SDK version everywhere
β”‚ β”œβ”€β”€ update-nutrient-in-cdn.js # Update CDN URLs
β”‚ β”œβ”€β”€ check-nutrient-update.sh # Detect a new SDK release (CI)
β”‚ └── check-biome-version.sh # Verify Biome version consistency
β”œβ”€β”€ playwright.config.ts # Playwright config (uses SERVER_DIR env var)
└── biome.json # Biome formatter config
Expand Down Expand Up @@ -85,8 +86,8 @@ SERVER_DIR=examples/javascript-vite npm run test
# Audit and fix vulnerabilities across all examples
npm run audit-fix

# Bump Nutrient SDK version in all examples
npm run update-nutrient-version
# Bump Nutrient SDK version in all examples (version is required)
npm run update-nutrient-version -- <version>
```

## Adding a New Example
Expand Down Expand Up @@ -124,6 +125,13 @@ svelte-kit, vue-composition-api.

- **Biome** β€” Code formatting check on every push/PR
- **Playwright** β€” Smoke tests on push/PR to main (installs all deps, runs e2e)
- **Update Nutrient SDK** β€” Daily check for a new `@nutrient-sdk/viewer` release.
Bumps every example listed in `update-nutrient-in-examples.sh`, formats with
Biome, runs the e2e suite inside the job, then opens a PR. A passing suite
opens it ready for review, so CODEOWNERS requests `@PSPDFKit/web`; a failing
one opens it as a draft and fails the run, because GitHub does not request
code owners on drafts. Can be run on demand via `workflow_dispatch`,
optionally against a specific version.

## Code Style

Expand Down
88 changes: 88 additions & 0 deletions scripts/check-nutrient-update.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/bin/bash
# Decides whether a Nutrient SDK bump is needed, refusing one already in flight.
set -euo pipefail

SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
REPO_ROOT="${SCRIPT_DIR}/.."

# Every example is held at the same version, so one of them reports the version
# the repository is on.
REFERENCE_EXAMPLE="react"

DIST_TAGS_URL="https://registry.npmjs.org/-/package/@nutrient-sdk/viewer/dist-tags"

requested="${1:-}"

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

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.


# Anything carrying a prerelease suffix is a nightly, never a release we ship.
if ! [[ "${latest}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Refusing to act on non-stable version \"${latest}\"." >&2
exit 1
fi

# jq -r prints "null" and exits 0 for a missing key, which would silently
# disable the already-on-this-version check.
if ! current="$(jq -er '.dependencies["@nutrient-sdk/viewer"]' \
"${REPO_ROOT}/examples/${REFERENCE_EXAMPLE}/package.json")"; then
echo "examples/${REFERENCE_EXAMPLE} has no @nutrient-sdk/viewer dependency." >&2
echo "The reference example moved or was renamed; update REFERENCE_EXAMPLE." >&2
exit 1
fi

branch="update-examples-${latest}"

if [ "${latest}" = "${current}" ]; then
should_update="false"
reason="already on ${latest}"
else
# --state all: a bump closed without merging must not be reopened on every
# scheduled run. A failed lookup must not read as "no pull request".
if ! pull_requests="$(gh pr list --state all --head "${branch}" \
--json number --jq '.[].number')"; then
echo "Could not list pull requests for ${branch}; refusing to guess." >&2
exit 1
fi

if [ -n "${pull_requests}" ]; then
should_update="false"
reason="a pull request for ${branch} already exists"
else
ls_remote_status=0
git -C "${REPO_ROOT}" ls-remote --exit-code --heads origin "${branch}" \
>/dev/null || ls_remote_status=$?

case "${ls_remote_status}" in
# An earlier run pushed the branch and then failed before opening its
# pull request. Skipping quietly would bury this version for good.
0)
echo "Branch ${branch} exists with no pull request, so an earlier run" >&2
echo "failed part-way. Delete it or open its pull request by hand." >&2
exit 1
;;
# --exit-code reserves 2 for "no matching ref"; anything else is a real
# failure that must not read as "the branch is free".
2)
should_update="true"
reason="${current} -> ${latest}"
;;
*)
echo "git ls-remote failed with status ${ls_remote_status}." >&2
exit 1
;;
esac
fi
fi

echo "should_update=${should_update} (${reason})"

{
echo "version=${latest}"
echo "current=${current}"
echo "branch=${branch}"
echo "should_update=${should_update}"
} >> "${GITHUB_OUTPUT:-/dev/stdout}"
Loading
Loading