') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(web): revalidate dashboard index.html and guard stale static bundle in CI by jrob5756 · Pull Request #321 · microsoft/conductor · GitHub
Skip to content

fix(web): revalidate dashboard index.html and guard stale static bundle in CI - #321

Merged
Jason Robert (jrob5756) merged 2 commits into
mainfrom
fix/dashboard-static-freshness
Jul 21, 2026
Merged

fix(web): revalidate dashboard index.html and guard stale static bundle in CI#321
Jason Robert (jrob5756) merged 2 commits into
mainfrom
fix/dashboard-static-freshness

Conversation

@jrob5756

Copy link
Copy Markdown
Collaborator

Problem

After a conductor update, the web dashboard can fail to show a frontend change that is in the shipped package. Two independent causes:

  1. Browser over-caches index.html.server.py served / with no Cache-Control, so a browser reuses a cached index.html that still references the previous build's version-hashed /assets/index-*.js bundle — pinning the dashboard to the old UI even though the new bundle is installed. (This is what happened when the v0.1.23 subworkflow expand/collapse feature "didn't show up" despite being present in the shipped bundle.)
  2. CI never verified the committed static/ bundle. The Frontend job ran npm run build but discarded the result, so a frontend source change merged without a matching make build-frontend would ship a stale bundle silently.

Changes

  • src/conductor/web/server.py — serve index.html with Cache-Control: no-cache. The browser revalidates it on every load (cheap 304 via ETag/Last-Modified) and always picks up the current build's hashed bundle after an upgrade. The hashed /assets/* files remain cacheable (their names change whenever their contents do).
  • .github/workflows/ci.yml — after npm run build, fail the Frontend job if git status shows any uncommitted change under src/conductor/web/static, with an actionable message ("run make build-frontend and commit"). Scoped to static/ only (tsconfig.tsbuildinfo churns per build and is excluded).
  • tests/test_web/test_server.py — new test_index_sent_with_no_cache asserting the header.

Validation

  • git status --porcelain -- src/conductor/web/static is empty after a fresh npm run build (verified locally on Node 24 vs CI's Node 20 → byte-identical), so the guard is reliable, not flaky.
  • uv run pytest tests/test_web/test_server.py — 85 passed.
  • ruff check / ruff format --check clean on changed files.

Jason Robertand others added 2 commits July 20, 2026 21:35
Two independent fixes for "the dashboard doesn't show a shipped frontend
change after upgrading":
- server.py serves index.html with `Cache-Control: no-cache`, so browsers
revalidate it (cheap 304 via ETag/Last-Modified) instead of reusing a
cached copy that still points at the previous build's hashed asset
bundle. The hashed /assets/* files stay cacheable (names change on
content change).
- CI now fails the frontend job when the committed src/conductor/web/static
bundle is out of date with the frontend source: it runs `npm run build`
and checks `git status` for changes under static/. The build reproduces
static/ byte-identically, so this is reliable rather than flaky.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ion tests
- Fix inaccurate comment/docstring claims that revalidation yields a
cheap 304: FileResponse doesn't handle conditional requests, so
every load re-downloads the full index.html. The fix still solves
the stale-bundle bug; only the "cheap 304" framing was wrong.
- Add regression tests pinning that /favicon.svg and hashed /assets/*
files do NOT inherit index.html's Cache-Control: no-cache header,
guarding against the fix accidentally widening into a blanket
no-cache policy that would defeat asset caching.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jrob5756
Jason Robert (jrob5756) merged commit 25eaebd into mainJul 21, 2026
10 checks passed
@jrob5756
Jason Robert (jrob5756) deleted the fix/dashboard-static-freshness branch July 21, 2026 15:25
Sign up for freeto 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

@jrob5756