Skip to content

feat(doenetml-versions): pin tracked versions to concrete releases#3011

Open
dqnykamp wants to merge 2 commits into
Doenet:mainfrom
dqnykamp:feat/pin-doenetml-versions
Open

feat(doenetml-versions): pin tracked versions to concrete releases#3011
dqnykamp wants to merge 2 commits into
Doenet:mainfrom
dqnykamp:feat/pin-doenetml-versions

Conversation

@dqnykamp

@dqnykamp dqnykamp commented Jul 18, 2026

Copy link
Copy Markdown
Member

Context

doenetmlVersions.fullVersion currently holds a moving npm dist-tag — 0.7latest, 0.7devdev. That value is passed straight to @doenet/doenetml-iframe, which builds the bundle URL https://cdn.jsdelivr.net/npm/@doenet/standalone@${fullVersion}/doenet-standalone.js. Because the URL contains the tag, its content changes on a new publish but the URL does not — so browsers keep serving the cached old bundle until users manually clear their cache. jsDelivr purges fix the CDN edge, not end-user browsers.

This PR lets us store a concrete version (0.7.21, 0.7.21-dev.343) so the URL is immutable and version-specific: browsers cache it forever and pick up a new bundle the instant the row changes (content references the row by FK, so one update covers all content on that channel).

Changes

  • Schema: add nullable, unique trackingNpmTag to doenetmlVersions. Migration backfills 0.7latest, 0.7devdev; 0.6 stays unpinned (never auto-updated).
  • Endpoint: POST /api/info/updateTrackedDoenetmlVersion — machine-to-machine (no session), timing-safe bearer-secret auth (DOENETML_VERSION_UPDATE_SECRET, modeled on the Discourse SSO shared-secret pattern), zod-validated, idempotent update: it pins the existing row that tracks the given tag and returns 404 if no row tracks it (it does not create rows). Both an explicit request-body version and a version resolved from npm (when the body omits it) are validated against the SemVer grammar so the value is safe to interpolate into the CDN URL.
  • Omit trackingNpmTag from the public version-list responses (API surface unchanged).
  • Wire the secret into .env.example and the prod + dev3 ECS task definitions (SSM parameter /{env}/doenet/DoenetmlVersionUpdateSecret).
  • Seed + tests.

The endpoint is called by DoenetML's publish workflow (separate PR in Doenet/DoenetML) with the exact version just published.

Rollout / operational prerequisites

Ship this first (expand-migrate-contract; the migration is additive and safe alone). Before/after deploy:

  1. Create the SSM parameter /{env}/doenet/DoenetmlVersionUpdateSecret for prod and dev3, and set the matching DOENET_VERSION_UPDATE_SECRET repo secret in Doenet/DoenetML.
  2. After deploy, one-time pin the initial concrete versions by calling the endpoint once per tag (omit version to have it read npm), then merge the DoenetML PR.

Verification

  • Migration SQL validated on a throwaway DB (backfill correct; multiple NULLs coexist under the unique index).
  • Typecheck, ESLint, Prettier clean; 8/8 unit tests pass against the real DB (schema grammar, explicit-version pin + idempotency, npm-resolved pin, rejection of a malformed npm-resolved version, and missing-tag).
  • Live endpoint driven locally: 401 (missing/wrong secret), 400 (bad version / unknown tag), changed:truechanged:false idempotency, public read route reflects the pin and does not leak trackingNpmTag.

🤖 Generated with Claude Code

Store a concrete @doenet/standalone version (e.g. 0.7.21, 0.7.21-dev.343) in
doenetmlVersions.fullVersion instead of a moving npm tag ("latest"/"dev"), so
the jsDelivr bundle URL is immutable and browsers pick up new DoenetML releases
without waiting out a browser cache.

- Add nullable, unique `trackingNpmTag` mapping a row to an npm dist-tag
  (migration backfills 0.7 -> latest, 0.7dev -> dev; 0.6 stays unpinned).
- Add a secret-guarded, idempotent POST /api/info/updateTrackedDoenetmlVersion
  endpoint that pins the tracked row to a published version. Auth is a
  timing-safe bearer secret (DOENETML_VERSION_UPDATE_SECRET), modeled on the
  Discourse SSO shared-secret pattern; the version is validated against the
  SemVer grammar so it is safe to interpolate into the CDN URL.
- Omit trackingNpmTag from the public version-list responses.
- Wire the secret into .env.example and the prod/dev3 ECS task definitions.

Called by DoenetML's publish workflow (separate PR) after each npm publish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014i6e7VFUmE7svTjjSyida7
The npm dist-tag fallback path wrote the registry-supplied version into
fullVersion (which builds the immutable jsDelivr bundle URL) with only a
non-empty-string check, bypassing the SemVer validation applied to an
explicit request-body version. Validate the resolved version with the same
schema at this choke point so a malformed npm response fails loudly instead
of silently corrupting the pinned version. Adds a covering test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dqnykamp
dqnykamp requested a review from cqnykamp July 19, 2026 02:51
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.

1 participant