Skip to content
Open
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)"

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
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

# 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