-
Notifications
You must be signed in to change notification settings - Fork 3
Automate SDK bump PRs, and fix the CODEOWNERS and CDN updater bugs #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| # Web SDK team owns all examples | ||
| * @PSPDFKit/nickel | ||
| * @PSPDFKit/web |
| 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 | ||
|
Comment on lines
+168
to
+175
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once this draft exists, The recovery path (mark ready + verify by hand, or close the PR, delete the branch, and
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 The nonexistent-version case fails soon enough (the first
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: 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}" | ||
There was a problem hiding this comment.
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:
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.${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:
Low probability in practice, so fine as a follow-up if you prefer.
There was a problem hiding this comment.
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:
examples/salesforce/force-app/.../Nutrient_InitNutrient.pagealready carries two CDN references, on lines 7 and 16, so the shape is one edit away from being real here.