Skip to content

Fix stale glossary/phrases pipeline and Pavlovia bundle cache-busting #39

@khahani

Description

@khahani

Problem Statement

Scientists and staff who update the parameter glossary or UI phrases in Google Sheets, trigger the corresponding GitHub Action, and wait for Netlify to deploy still find that participants see stale explanation text and outdated default values when running experiments on Pavlovia.

There are three compounding problems:

  1. Silent glossary failures. The `npm run glossary` script suppresses all errors with `|| true`. This was added to avoid a false red indicator when nothing had changed in the sheet. The side effect is that real failures — bad credentials, network errors, API limits — are also silenced. The old `glossary.ts` stays in place, Netlify never detects a diff, and the bundle is never rebuilt.

  2. False red on both individual workflows. When the Google Sheet has not changed, `git commit` finds nothing to stage and exits non-zero. Both `update-glossary` and `update-phrases` workflows show Red even though nothing was wrong. Staff have learned to ignore red indicators, which means they also miss genuine failures.

  3. Browser caching of the Pavlovia experiment bundle. The experiment bundle (`threshold.min.js`) is pushed to Pavlovia under a fixed filename every time a scientist compiles. When the scientist recompiles after a glossary or phrases update, the new bundle lands at the same URL. Participants' browsers serve the cached old version until they manually clear browser data.

Solution

Fix the glossary script so real fetch failures propagate correctly. Fix both individual GitHub Actions workflows so they succeed (Green) with a clear message when the sheet has not changed, and fail (Red) only on genuine errors.

Add a new unified GitHub Action that fetches both glossary and phrases in a single run, applies the same no-changes-is-success behaviour, uses all-or-nothing commit semantics (if either fetch fails, nothing is committed), always writes a plain-language job summary and sends a single combined Slack notification so staff can see at a glance what was updated, what was unchanged, and what failed.

Update the `update-phrases` workflow's "no changes" Slack notification to include explicit guidance for staff, since the workflow is triggered almost always right after an edit to the spreadsheet.

Fix the Pavlovia cache problem by injecting a content hash into the experiment bundle script reference in `index.html` and `index-stepper-bool.html` at compile time. Every compile produces a unique URL for the bundle. Browsers always fetch the version that was current when the scientist compiled, with no manual cache clearing required.

User Stories

  1. As a staff member, I want the `update-glossary` Action to complete successfully (Green) when the glossary sheet has not changed, so that I can immediately distinguish "nothing to do" from a real failure.
  2. As a staff member, I want the `update-phrases` Action to complete successfully (Green) when the phrases sheet has not changed, so that I stop mistaking false reds for real errors.
  3. As a staff member, I want a single unified GitHub Action that updates both glossary and phrases in one run, so that I do not need to remember to trigger two separate Actions after editing the sheet.
  4. As a staff member, I want the unified Action to show Green when at least one update was committed successfully, so that I know the deployment pipeline has been triggered.
  5. As a staff member, I want the unified Action to show Green when neither glossary nor phrases has changed, so that I know the run was a no-op with no errors.
  6. As a staff member, I want the unified Action to show Red when either fetch fails, so that I know I need to investigate and re-run.
  7. As a staff member, I want a single Slack notification on every unified Action run summarising what was updated, what was unchanged, and what failed — and when nothing changed, I want the message to tell me no build was triggered and guide me to verify my edits — so that I do not need to read raw logs.
  8. As a staff member, I want the unified Action to commit nothing if either fetch fails, so that I can safely re-run after fixing the problem without worrying about partial updates already being deployed.
  9. As a staff member, I want to continue using the individual `update-glossary` and `update-phrases` Actions as a fallback while evaluating the unified one, so that operations are not disrupted during the transition.
  10. As an EasyEyes developer, I want a genuine glossary fetch failure to exit non-zero and fail the workflow visibly, so that stale glossary data is never silently committed.
  11. As an EasyEyes developer, I want the `update-glossary` workflow to exit 0 cleanly when there are no changes to commit, rather than letting `git commit` fail with "nothing to commit", so that the failure signal is meaningful.
  12. As an EasyEyes developer, I want the same no-changes-is-success behaviour on all three workflows (individual glossary, individual phrases, unified), so that the visual language is consistent.
  13. As a scientist, I want participants to always load the experiment bundle that was current when I compiled my experiment, so that they see the correct explanation text and defaults I intended.
  14. As a scientist, I want recompiling my experiment to be sufficient to deliver updated content to participants, so that I do not need to ask participants to clear their browser cache.
  15. As a scientist, I want each compile to produce a distinct bundle URL on Pavlovia, so that browser caches are automatically invalidated on every recompile.
  16. As a participant, I want to see the correct explanation text and default values every time I run an experiment, so that the instructions are not confusingly out of date.
  17. As a participant, I want my browser to never serve me a stale version of the experiment, so that I do not need to clear my browser data before starting.
  18. As an EasyEyes developer, I want to roll back a Netlify deploy without affecting any experiment already running on Pavlovia, so that a hotfix or rollback does not disrupt live studies.
  19. As an EasyEyes developer, I want the content hash to be derived from the exact bytes of the compiled bundle, so that the same build always produces the same hash and a changed build always produces a different one.
  20. As an EasyEyes developer, I want the hash injection to target only the experiment bundle script tag, so that other assets in the HTML are not accidentally modified.
  21. As an EasyEyes developer, I want the hash injection to be idempotent, so that compiling twice in a row with the same bundle produces the same HTML.

Implementation Decisions

Modules modified

Glossary npm script

  • Remove the `|| true` suffix from the `glossary` script entry in `package.json`
  • The script must propagate the exit code of the underlying fetch node script faithfully

`update-glossary` workflow

  • After `npm run glossary` runs successfully, check whether any glossary source files are staged using `git diff --cached --quiet`
  • If nothing is staged: write a step summary — "The glossary sheet was checked. No updates were found. Nothing was committed and no Netlify build was triggered. If you expected your edits to be picked up, verify they were saved in the spreadsheet and re-run." — and exit with code 0
  • If changes are staged: commit, push to all three repositories, write a "Glossary updated" step summary
  • No Slack notifications added to this workflow; it is a fallback and the unified workflow is the primary path

`update-phrases` workflow

  • Apply the identical no-changes-is-success pattern as the glossary workflow fix
  • No changes to the underlying `npm run phrases` script
  • Update the existing "no changes" Slack notification message to: "No phrase changes were detected. No commit was made and no Netlify build was triggered. If you expected your edits to be picked up, verify they were saved in the spreadsheet and re-run."

New unified `update-all` workflow

  • Single manually-dispatched workflow that runs both fetches sequentially but independently (so both results are always captured)
  • All-or-nothing commit semantics: if either fetch exited non-zero, write a failure summary and exit non-zero without committing anything
  • If both succeeded and nothing changed: write a warning block above the step summary table — "No changes were found in either spreadsheet. No commit was made and no Netlify build was triggered. If you expected your edits to be picked up, verify they were saved in the spreadsheet and re-run this workflow." — followed by the status table, then exit with code 0 (Green)
  • The warning block is shown only when both sheets are unchanged; a partial result (one updated, one unchanged) does not show the warning
  • If both succeeded and at least one changed: commit and push all staged changes to all three repositories
  • Always writes a `$GITHUB_STEP_SUMMARY` table: Glossary (updated / unchanged / failed), Phrases (updated / unchanged / failed)
  • Sends one combined Slack notification per run:
    • At least one sheet updated: names which sheets were updated and confirms the Netlify build was triggered
    • Neither sheet changed: "No changes were found in either spreadsheet. No commit was made and no Netlify build was triggered. If you expected your edits to be picked up, verify they were saved in the spreadsheet and re-run."
    • Failure: names which sheet(s) failed, states that nothing was committed, and instructs to fix the named fetch and re-run
  • Individual workflows remain intact and fixed alongside the unified one

Content hash utility (new pure function)

  • Accepts file content as a buffer or string, returns a short deterministic hex string
  • Uses Node's built-in `crypto` module (SHA-256, truncated to 8 characters)
  • No file system access, no network access, no side effects

HTML version injector (new pure function)

  • Accepts an HTML string, a target script filename, and a hash string; returns a new HTML string with `?v=` appended to the `src` attribute of the matching `<script>` tag
  • Targets both `index.html` and `index-stepper-bool.html` — both reference `js/threshold.min.js` and both are pushed to Pavlovia
  • Must not modify any other `<script>` or `` tags
  • Must be idempotent: if `?v=` already exists on the target tag, replace it rather than appending a second one
  • Returns the HTML unchanged if the target filename is not found

Pavlovia upload coordinator

  • Pre-read `js/threshold.min.js` content once before the upload loop begins; compute the content hash from this content; store the hash for use during the loop
  • `index.html` appears at loop position 9 and `js/threshold.min.js` at position 18 — the pre-read ensures the hash is available when `index.html` and `index-stepper-bool.html` are processed
  • When the loop processes `index.html` or `index-stepper-bool.html`, pass the file content and the pre-computed hash through the HTML version injector before building the commit action
  • `js/threshold.min.js` continues to be read and uploaded normally at its loop position
  • No changes to the file list, upload loop structure, or GitLab API interaction

Key constraints

  • Participants always receive the bundle current at the scientist's compile time — no runtime fetching from Netlify
  • Experiments on Pavlovia are fully self-contained; no runtime dependency on EasyEyes infrastructure is introduced
  • The Netlify build pipeline is unchanged
  • The file list pushed to Pavlovia is unchanged; the filename `threshold.min.js` is preserved on disk
  • A Netlify rollback does not affect any experiment already compiled and running on Pavlovia

Testing Decisions

Good tests verify observable external behaviour given known inputs, with no assertions about internal implementation details and no mocking of the module under test.

Content hash utility

  • Same input always produces the same output (determinism)
  • Different inputs produce different outputs (sensitivity)
  • Output is a non-empty hex string of the expected fixed length

HTML version injector

  • Injects `?v=` into the correct `<script>` tag and no other tags
  • Applies correctly to both `index.html` and `index-stepper-bool.html`
  • Is idempotent: a second call with a new hash replaces the old `?v=` value
  • Returns HTML unchanged when the target filename is not present

Prior art: Jest is already configured in the threshold package. New tests should follow the file-adjacent `*.test.ts` convention used elsewhere in the package. Tests are deferred and will be added in a follow-up.

Out of Scope

  • Fetching the glossary or phrases dynamically at participant runtime
  • Loading the experiment bundle from Netlify or a CDN at participant runtime
  • Versioned CDN distribution of the experiment bundle
  • Fixing the retry/fallback behaviour in the phrases fetch script
  • Cache-control headers on Pavlovia's servers
  • Removing the individual `update-glossary` and `update-phrases` workflows (deferred until the unified workflow is validated by staff)
  • Cache-busting for `js/first.min.js` (deferred to a follow-up issue)

Further Notes

The `|| true` on the glossary script was added to prevent a false red when `git commit` found nothing to stage. The correct fix is to guard the commit step in the workflow. The distinction matters: a failed Google Sheets fetch and a "sheet unchanged" state must produce visually different outcomes (Red vs Green) so staff can act appropriately on each.

The content hash approach was chosen over full Vite content-addressed filenames because it achieves identical browser cache-busting with far fewer changes — no build manifest, no changes to the file list, no changes to the build pipeline. The hash is appended as a query string to the script `src` in `index.html` and `index-stepper-bool.html` at Pavlovia push time, not at Netlify build time.

The `index.html` and `index-stepper-bool.html` files appear at loop positions 9 and 8 respectively in the Pavlovia upload loop, before `js/threshold.min.js` at position 18. The bundle is therefore pre-read outside the loop solely to compute the hash; the loop then reads and uploads it again normally at its regular position.

The unified workflow uses all-or-nothing commit semantics so that every re-run is equivalent to the first run. If a partial update were committed before a failure, a subsequent re-run would detect "no changes" for the already-committed half and skip it silently.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions