diff --git a/.github/pages/builds.html b/.github/pages/builds.html index 8f35c5c5d..5deadf310 100644 --- a/.github/pages/builds.html +++ b/.github/pages/builds.html @@ -364,7 +364,11 @@

Save and apply

const r = repo.includes("/") ? ` -R ${repo}` : ""; return `gh workflow run reconfigure.yml${r} \\\n -f builds="${buildsInput()}"`; } - function workflowUrl() { return repo.includes("/") ? "https://github.com/" + repo + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; } + function workflowUrl() { + const parts = repo.trim().split("/").filter(Boolean); + const repoPath = parts.length === 2 ? parts.map(encodeURIComponent).join("/") : ""; + return repoPath ? "https://github.com/" + repoPath + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; + } function update() { $("ghCmd").textContent = genGhCmd(); const wf = $("wfLink"); if (wf) wf.href = workflowUrl(); @@ -379,9 +383,19 @@

Save and apply

qp.set("actions", "write"); return "https://github.com/settings/personal-access-tokens/new?" + qp.toString(); } - function setApplyStatus(html, kind) { + function setApplyStatus(message, kind, workflowLink) { const st = $("applyStatus"); if (!st) return; - st.innerHTML = html || ""; st.className = "status" + (kind ? " " + kind : ""); + st.replaceChildren(message || ""); + if (workflowLink) { + const link = document.createElement("a"); + link.href = workflowLink; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = "View runs"; + st.appendChild(link); + st.appendChild(document.createTextNode(".")); + } + st.className = "status" + (kind ? " " + kind : ""); } function showTokenPanel(msg) { const p = $("tokPanel"); if (!p) return; @@ -407,7 +421,7 @@

Save and apply

if (btn) { btn.disabled = false; btn.textContent = "Save and apply"; } if (r.status === 204) { hideTokenPanel(); - setApplyStatus("Queued Reconfigure. It will open a pull request when the settings file is updated. View runs.", "ok"); + setApplyStatus("Queued Reconfigure. It will open a pull request when the settings file is updated. ", "ok", workflowUrl()); return; } if (r.status === 401) { try { localStorage.removeItem(TOK_KEY); } catch (_) {} showTokenPanel("That token was rejected. Paste a valid fine-grained token with Actions: Read and write for this repository."); setApplyStatus("Token rejected (401).", "err"); return; } @@ -416,7 +430,7 @@

Save and apply

setApplyStatus("Dispatch failed (HTTP " + r.status + ").", "err"); } catch (e) { if (btn) { btn.disabled = false; btn.textContent = "Save and apply"; } - setApplyStatus("Network error: " + escapeHtml(String(e && e.message || e)), "err"); + setApplyStatus("Network error: " + String(e && e.message || e), "err"); } } diff --git a/.github/pages/configure.html b/.github/pages/configure.html index 75c78c32e..f674d428f 100644 --- a/.github/pages/configure.html +++ b/.github/pages/configure.html @@ -1243,8 +1243,9 @@

Save and apply

} function workflowUrl() { - const repo = $("repo").value.trim(); - return repo.includes("/") ? "https://github.com/" + repo + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; + const parts = $("repo").value.trim().split("/").filter(Boolean); + const repoPath = parts.length === 2 ? parts.map(encodeURIComponent).join("/") : ""; + return repoPath ? "https://github.com/" + repoPath + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; } function token() { try { return localStorage.getItem(TOK_KEY) || ""; } catch (_) { return ""; } } function tokenUrl() { @@ -1257,9 +1258,18 @@

Save and apply

qp.set("actions", "write"); return "https://github.com/settings/personal-access-tokens/new?" + qp.toString(); } - function setApplyStatus(html, kind) { + function setApplyStatus(message, kind, workflowLink) { const st = $("applyStatus"); if (!st) return; - st.innerHTML = html || ""; + st.replaceChildren(message || ""); + if (workflowLink) { + const link = document.createElement("a"); + link.href = workflowLink; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = "View runs"; + st.appendChild(link); + st.appendChild(document.createTextNode(".")); + } st.className = "status" + (kind ? " " + kind : ""); } function showTokenPanel(msg) { @@ -1301,7 +1311,7 @@

Save and apply

if (btn) { btn.disabled = false; btn.textContent = "Save and apply"; } if (r.status === 204) { hideTokenPanel(); - setApplyStatus("Queued Reconfigure. It will open a pull request when the settings files are updated. View runs.", "ok"); + setApplyStatus("Queued Reconfigure. It will open a pull request when the settings files are updated. ", "ok", workflowUrl()); return; } if (r.status === 401) { try { localStorage.removeItem(TOK_KEY); } catch (_) {} showTokenPanel("That token was rejected. Paste a valid fine-grained token with Actions: Read and write for this repository."); setApplyStatus("Token rejected (401).", "err"); return; } @@ -1316,7 +1326,7 @@

Save and apply

setApplyStatus("Dispatch failed (HTTP " + r.status + ").", "err"); } catch (e) { if (btn) { btn.disabled = false; btn.textContent = "Save and apply"; } - setApplyStatus("Network error: " + escapeHtml(String(e && e.message || e)), "err"); + setApplyStatus("Network error: " + String(e && e.message || e), "err"); } } diff --git a/.github/pages/integrate.html b/.github/pages/integrate.html index 615b9f1d7..15df1dba9 100644 --- a/.github/pages/integrate.html +++ b/.github/pages/integrate.html @@ -743,21 +743,42 @@

4. Finish setup

const repo = $("repo").value.trim() || "/"; const rel = selectedRelease(); const pinned = rel && catalog.containers && rel.id !== catalog.containers.default; + const varsStep = $("varsStep"); + if (!varsStep) return; + const code = text => { + const el = document.createElement("code"); + el.className = "inl"; + el.textContent = text; + return el; + }; if (isGitLab()) { - $("varsStep").innerHTML = - `Optional GitLab CI/CD variables can override installer defaults later; the initial scaffold records LabVIEW ${ver} in .github/labview-ci.yml.`; + varsStep.replaceChildren( + `Optional GitLab CI/CD variables can override installer defaults later; the initial scaffold records LabVIEW ${ver} in `, + code(".github/labview-ci.yml"), + "." + ); } else if (pinned) { const s = source(); const srcGhcr = "ghcr.io/" + s.repo.toLowerCase(); - $("varsStep").innerHTML = - `Pinning this repo to ${esc(rel.label)}: the browser install sets ` + - `LCWC_BASE_IMAGE=${esc(srcGhcr)}-labview-base:${esc(rel.id)} and ` + - `LCWC_LINUX_BASE_IMAGE=${esc(srcGhcr)}-labview:linux-${esc(rel.id)} as Actions variables ` + - `(command-line installs: set them with gh variable set).`; + const release = document.createElement("strong"); + release.textContent = rel.label; + varsStep.replaceChildren( + "Pinning this repo to ", + release, + ": the browser install sets ", + code(`LCWC_BASE_IMAGE=${srcGhcr}-labview-base:${rel.id}`), + " and ", + code(`LCWC_LINUX_BASE_IMAGE=${srcGhcr}-labview:linux-${rel.id}`), + " as Actions variables (command-line installs: set them with ", + code("gh variable set"), + ")." + ); } else { - $("varsStep").innerHTML = - `Optionally pin settings as Actions variables: ` + - `gh variable set LABVIEW_VERSION -R ${repo} -b ${ver} (has a safe fallback).`; + varsStep.replaceChildren( + "Optionally pin settings as Actions variables: ", + code(`gh variable set LABVIEW_VERSION -R ${repo} -b ${ver}`), + " (has a safe fallback)." + ); } } @@ -2351,27 +2372,94 @@

4. Finish setup

const dashLink0 = $("insDashLink"); if (dashLink0) dashLink0.style.display = "none"; const showDashLink = () => { const dl = $("insDashLink"); - if (dl) { dl.href = url; dl.innerHTML = "Open your dashboard ↗"; dl.style.display = ""; } + if (dl) { dl.href = url; dl.textContent = "Open your dashboard \u2197"; dl.style.display = ""; } }; setInsStatus(`✓ Done \u2014 opened pull request #${pr.number} in ${esc(repo)}.`, "ok"); const done = []; - if (setup.kept) done.push(`Kept your existing settings — ${esc(setup.kept)}.`); - if (setup.removed && setup.removed.length) done.push(`Removed ${setup.removed.length} old tooling file${setup.removed.length > 1 ? "s" : ""} no longer used (in the pull request).`); - if (setup.readme) done.push(`Added a CI dashboard badge & link to your README (in the pull request).`); - if (setup.workflowPerms === "done") done.push(`Set Workflow permissions to read & write.`); - if (setup.toolingToken === "done") done.push(`Saved a one-time update token — future Update now clicks open their pull request automatically.`); - if (setup.buildImages && autoMerge) done.push(`Watching your worker container image build${setup.buildImage && setup.buildLinuxImage ? "s" : ""} — Windows and Linux image workflows are tracked separately below so it is clear which background work is still running.`); + const addDone = build => { + const item = document.createElement("li"); + build(item); + done.push(item); + }; + const appendText = (parent, text) => parent.appendChild(document.createTextNode(text)); + const appendEmphasis = (parent, tag, text, className) => { + const el = document.createElement(tag); + if (className) el.className = className; + el.textContent = text; + parent.appendChild(el); + return el; + }; + if (setup.kept) addDone(item => { + appendText(item, "Kept your existing settings - "); + appendEmphasis(item, "strong", setup.kept); + appendText(item, "."); + }); + if (setup.removed && setup.removed.length) addDone(item => { + appendText(item, "Removed "); + appendEmphasis(item, "strong", setup.removed.length); + appendText(item, ` old tooling file${setup.removed.length > 1 ? "s" : ""} no longer used (in the pull request).`); + }); + if (setup.readme) addDone(item => appendText(item, "Added a CI dashboard badge and link to your README (in the pull request).")); + if (setup.workflowPerms === "done") addDone(item => { + appendText(item, "Set "); + appendEmphasis(item, "strong", "Workflow permissions"); + appendText(item, " to read and write."); + }); + if (setup.toolingToken === "done") addDone(item => { + appendText(item, "Saved a one-time "); + appendEmphasis(item, "strong", "update token"); + appendText(item, " - future "); + appendEmphasis(item, "em", "Update now"); + appendText(item, " clicks open their pull request automatically."); + }); + if (setup.buildImages && autoMerge) addDone(item => { + appendText(item, "Watching your "); + appendEmphasis(item, "strong", `worker container image build${setup.buildImage && setup.buildLinuxImage ? "s" : ""}`); + appendText(item, " - Windows and Linux image workflows are tracked separately below so it is clear which background work is still running."); + }); // Show the dashboard URL as PLAIN TEXT until Pages is confirmed serving it // (revealLiveLink upgrades #insLiveUrl to a real link) so we never present a // link that 404s. When Pages already existed it's almost certainly live, so // that case links right away. - if (setup.pages === "done") done.push(`Enabled GitHub Pages — your dashboard will be at ${esc(url)}It publishes within a minute of the install merge — no CI run required; the link activates here the moment it’s live.`); - else if (setup.pages === "exists") done.push(`GitHub Pages is already on — dashboard at ${esc(url)}.`); - else if (setup.hasDashboard) done.push(`Your dashboard will be at ${esc(url)} once GitHub Pages is on.`); + const appendDashboardUrl = (item, live) => { + const location = document.createElement("span"); + location.id = "insLiveUrl"; + if (live) { + const link = document.createElement("a"); + link.href = url; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = url; + location.appendChild(link); + } else { + appendEmphasis(location, "code", url, "inl"); + } + item.appendChild(location); + }; + if (setup.pages === "done") addDone(item => { + appendText(item, "Enabled "); + appendEmphasis(item, "strong", "GitHub Pages"); + appendText(item, " - your dashboard will be at "); + appendDashboardUrl(item, false); + appendEmphasis(item, "small", "It publishes within a minute of the install merge - no CI run required; the link activates here the moment it's live.").id = "insLiveNote"; + }); + else if (setup.pages === "exists") addDone(item => { + appendEmphasis(item, "strong", "GitHub Pages"); + appendText(item, " is already on - dashboard at "); + appendDashboardUrl(item, true); + appendText(item, "."); + }); + else if (setup.hasDashboard) addDone(item => { + appendText(item, "Your dashboard will be at "); + appendDashboardUrl(item, false); + appendText(item, " once "); + appendEmphasis(item, "strong", "GitHub Pages"); + appendText(item, " is on."); + }); const doneEl = $("insDone"); - if (doneEl) { doneEl.innerHTML = done.map(d => `
  • ${d}
  • `).join(""); doneEl.style.display = done.length ? "" : "none"; } + if (doneEl) { doneEl.replaceChildren(...done); doneEl.style.display = done.length ? "" : "none"; } const steps = []; steps.push(`Review and merge pull request #${pr.number}.`); diff --git a/.github/pages/lvci-header.js b/.github/pages/lvci-header.js index c590dfbec..5609320d6 100644 --- a/.github/pages/lvci-header.js +++ b/.github/pages/lvci-header.js @@ -1284,7 +1284,7 @@ sel.addEventListener('change', function () { syncSteps(); var v = sel.value; - if (v && v !== cfg.sha) window.location.href = docDest(v); + if (v && v !== cfg.sha && SHA_RE.test(v)) window.location.href = docDest(v); }); sel._lvciSync = function () { rp.refresh(); syncSteps(); }; wrap.appendChild(lbl); wrap.appendChild(prev); wrap.appendChild(rp.button); wrap.appendChild(next); @@ -1953,10 +1953,20 @@ pendbar.id = 'lvci-pendbar'; pendbar.className = 'lvci-depbar'; pendbar.setAttribute('role', 'alert'); - pendbar.innerHTML = - '\u26A0\uFE0F Dependencies need to be installed into your containers. ' + - 'Your project declares dependencies that are not yet baked into the worker container(s); container CI may error or show broken code until you update them. ' + - 'Review & update dependencies \u2197'; + var pendText = document.createElement('span'); + pendText.className = 'lvci-dep-txt'; + var pendTitle = document.createElement('strong'); + pendTitle.textContent = '\u26A0\uFE0F Dependencies need to be installed into your containers. '; + var pendDescription = document.createElement('span'); + pendDescription.className = 'lvci-dep-sub'; + pendDescription.textContent = 'Your project declares dependencies that are not yet baked into the worker container(s); container CI may error or show broken code until you update them. '; + var pendLink = document.createElement('a'); + pendLink.href = navBase + '/dependencies.html'; + pendLink.textContent = 'Review & update dependencies \u2197'; + pendText.appendChild(pendTitle); + pendText.appendChild(pendDescription); + pendText.appendChild(pendLink); + pendbar.appendChild(pendText); // Global attention bar (failure banner) — hidden until the activity poll // finds a workflow whose newest run failed; names it + links to the run. diff --git a/.github/pages/unit-tests.html b/.github/pages/unit-tests.html index 09ee5927c..aadbf1fdb 100644 --- a/.github/pages/unit-tests.html +++ b/.github/pages/unit-tests.html @@ -332,7 +332,7 @@

    4. Review & apply

    const locs = (s.locations||[]).map(x=>x.trim()).filter(Boolean); if (locs.length){ lines.push(" locations:"); - locs.forEach(l => lines.push(` - "${l.replace(/"/g,'\\"')}"`)); + locs.forEach(l => lines.push(` - ${JSON.stringify(l)}`)); } else { lines.push(" locations: []"); } @@ -491,8 +491,9 @@

    4. Review & apply

    } function workflowUrl(){ - const repo = $("repo").value; - return repo.includes("/") ? "https://github.com/" + repo + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; + const parts = $("repo").value.trim().split("/").filter(Boolean); + const repoPath = parts.length === 2 ? parts.map(encodeURIComponent).join("/") : ""; + return repoPath ? "https://github.com/" + repoPath + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; } function token(){ try { return localStorage.getItem(TOK_KEY) || ""; } catch (_) { return ""; } } function tokenUrl(){ @@ -505,9 +506,18 @@

    4. Review & apply

    qp.set("actions", "write"); return "https://github.com/settings/personal-access-tokens/new?" + qp.toString(); } - function setApplyStatus(html, kind){ + function setApplyStatus(message, kind, workflowLink){ const st = $("applyStatus"); if (!st) return; - st.innerHTML = html || ""; + st.replaceChildren(message || ""); + if (workflowLink){ + const link = document.createElement("a"); + link.href = workflowLink; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = "View runs"; + st.appendChild(link); + st.appendChild(document.createTextNode(".")); + } st.className = "status" + (kind ? " " + kind : ""); } function showTokenPanel(msg){ @@ -534,7 +544,7 @@

    4. Review & apply

    if (btn){ btn.disabled = false; btn.textContent = "Save & Apply"; } if (r.status === 204){ hideTokenPanel(); - setApplyStatus("Queued Reconfigure. It will open a pull request when the settings file is updated. View runs.", "ok"); + setApplyStatus("Queued Reconfigure. It will open a pull request when the settings file is updated. ", "ok", workflowUrl()); return; } if (r.status === 401){ try { localStorage.removeItem(TOK_KEY); } catch (_) {} showTokenPanel("That token was rejected. Paste a valid fine-grained token with Actions: Read and write for this repository."); setApplyStatus("Token rejected (401).", "err"); return; } @@ -543,7 +553,7 @@

    4. Review & apply

    setApplyStatus("Dispatch failed (HTTP " + r.status + ").", "err"); }catch(e){ if (btn){ btn.disabled = false; btn.textContent = "Save & Apply"; } - setApplyStatus("Network error: " + esc(String(e && e.message || e)), "err"); + setApplyStatus("Network error: " + String(e && e.message || e), "err"); } } diff --git a/.github/pages/vi-analyzer.html b/.github/pages/vi-analyzer.html index 00e15050b..e5fadd171 100644 --- a/.github/pages/vi-analyzer.html +++ b/.github/pages/vi-analyzer.html @@ -370,7 +370,11 @@

    Save and apply

    ` -f via_default="${ci.def}"` + (ci.rules ? ` \\\n -f via_rules="${ci.rules}"` : ""); } - function workflowUrl() { return repo.includes("/") ? "https://github.com/" + repo + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; } + function workflowUrl() { + const parts = repo.trim().split("/").filter(Boolean); + const repoPath = parts.length === 2 ? parts.map(encodeURIComponent).join("/") : ""; + return repoPath ? "https://github.com/" + repoPath + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; + } function update() { $("ghCmd").textContent = genGhCmd(); const wf = $("wfLink"); if (wf) wf.href = workflowUrl(); @@ -385,9 +389,18 @@

    Save and apply

    qp.set("actions", "write"); return "https://github.com/settings/personal-access-tokens/new?" + qp.toString(); } - function setApplyStatus(html, kind) { + function setApplyStatus(message, kind, workflowLink) { const st = $("applyStatus"); if (!st) return; - st.innerHTML = html || ""; + st.replaceChildren(message || ""); + if (workflowLink) { + const link = document.createElement("a"); + link.href = workflowLink; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = "View runs"; + st.appendChild(link); + st.appendChild(document.createTextNode(".")); + } st.className = "status" + (kind ? " " + kind : ""); } function showTokenPanel(msg) { @@ -415,7 +428,7 @@

    Save and apply

    if (btn) { btn.disabled = false; btn.textContent = "Save and apply"; } if (r.status === 204) { hideTokenPanel(); - setApplyStatus("Queued Reconfigure. It will open a pull request when the settings file is updated. View runs.", "ok"); + setApplyStatus("Queued Reconfigure. It will open a pull request when the settings file is updated. ", "ok", workflowUrl()); return; } if (r.status === 401) { try { localStorage.removeItem(TOK_KEY); } catch (_) {} showTokenPanel("That token was rejected. Paste a valid fine-grained token with Actions: Read and write for this repository."); setApplyStatus("Token rejected (401).", "err"); return; } @@ -424,7 +437,7 @@

    Save and apply

    setApplyStatus("Dispatch failed (HTTP " + r.status + ").", "err"); } catch (e) { if (btn) { btn.disabled = false; btn.textContent = "Save and apply"; } - setApplyStatus("Network error: " + escapeHtml(String(e && e.message || e)), "err"); + setApplyStatus("Network error: " + String(e && e.message || e), "err"); } } diff --git a/.github/pages/whats-new.html b/.github/pages/whats-new.html index de1dee00f..d3c31e29b 100644 --- a/.github/pages/whats-new.html +++ b/.github/pages/whats-new.html @@ -682,16 +682,41 @@

    Finish the update — add a one-time token

    prog.phase === PH.PUBLISHING ? ("Publishing v" + prog.ver + "\u2026") : prog.phase === PH.ERROR ? "Update didn\u2019t start" : ("Updating to v" + prog.ver + "\u2026"); - const rp = $("upRepo"); if (rp) rp.innerHTML = state.repo ? `${esc(state.repo)}` : ""; - if (msg != null) { const m = $("upMsg"); if (m) m.innerHTML = msg; } - const links = []; - if (prog.phase === PH.DONE) { - const dash = clientPagesUrl(); - if (dash) links.push(`Go to your dashboard ↗`); + const rp = $("upRepo"); + if (rp) { + rp.replaceChildren(); + if (state.repo) { + const link = document.createElement("a"); + link.href = ghUrl(state.repo); + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = state.repo; + rp.appendChild(link); + } + } + if (msg != null) { const m = $("upMsg"); if (m) m.textContent = msg; } + const lz = $("upLinks"); + if (lz) { + lz.replaceChildren(); + const appendLink = (url, text, primary) => { + let safeUrl; + try { + const parsed = new URL(url, location.href); + if (parsed.protocol !== "https:") return; + safeUrl = parsed.href; + } catch (_) { return; } + const link = document.createElement("a"); + if (primary) link.className = "primary"; + link.href = safeUrl; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = text; + lz.appendChild(link); + }; + if (prog.phase === PH.DONE) appendLink(clientPagesUrl(), "Go to your dashboard \u2197", true); + if (prog.prUrl) appendLink(prog.prUrl, `Open pull request${prog.prNum ? " #" + prog.prNum : ""} \u2197`); + if (prog.runUrl) appendLink(prog.runUrl, "Open the workflow run \u2197"); } - if (prog.prUrl) links.push(`Open pull request${prog.prNum ? " #" + prog.prNum : ""} ↗`); - if (prog.runUrl) links.push(`Open the workflow run ↗`); - const lz = $("upLinks"); if (lz) lz.innerHTML = links.join(""); const foot = $("upProgFoot"); if (foot) { // Dismiss only when the update has truly stopped (done/failed). While it @@ -755,13 +780,13 @@

    Finish the update — add a one-time token

    prog.runId = run.id; prog.runUrl = run.html_url; if (run.status === "completed") { if (run.conclusion === "success") { startPrPoll(); return; } - setPhase(PH.ERROR, `The update workflow finished with ${esc(run.conclusion || "a failure")}. Open the run to see why, then try again.`); + setPhase(PH.ERROR, `The update workflow finished with ${run.conclusion || "a failure"}. Open the run to see why, then try again.`); return; } setPhase(run.status === "in_progress" ? PH.RUNNING : PH.QUEUED, run.status === "in_progress" - ? "Building the update on GitHub Actions… this usually takes under a minute." - : "Queued on GitHub Actions… waiting for a runner."); + ? "Building the update on GitHub Actions\u2026 this usually takes under a minute." + : "Queued on GitHub Actions\u2026 waiting for a runner."); } if (tries < 40) later(() => pollRun(tries + 1), 3000); else startPrPoll(); // give up tracking the run; fall back to PR detection @@ -769,7 +794,7 @@

    Finish the update — add a one-time token

    } function startPrPoll() { - if (prog.phase !== PH.APPLYING) setPhase(PH.APPLYING, "Applying the update to your repository…"); + if (prog.phase !== PH.APPLYING) setPhase(PH.APPLYING, "Applying the update to your repository\u2026"); pollOutcome(prog.ver, 0); } @@ -783,7 +808,7 @@

    Finish the update — add a one-time token

    prog.ver = ver; prog.ts = Date.now(); prog.runId = 0; prog.runUrl = ""; prog.prUrl = ""; prog.prNum = 0; clearTimers(); setCmdStatus("", null); - setPhase(PH.DISPATCH, "Starting the update…"); + setPhase(PH.DISPATCH, "Starting the update\u2026"); resolveBranch().then((branch) => { return fetch(`https://api.github.com/repos/${state.repo}/actions/workflows/apply-tooling-update.yml/dispatches`, { method: "POST", @@ -793,7 +818,7 @@

    Finish the update — add a one-time token

    body: JSON.stringify({ ref: branch, inputs: { source_ref: "v" + ver } }) }); }).then(async (r) => { - if (r.status === 204) { setPhase(PH.QUEUED, "Queued on GitHub Actions… waiting for a runner."); pollRun(0); return; } + if (r.status === 204) { setPhase(PH.QUEUED, "Queued on GitHub Actions\u2026 waiting for a runner."); pollRun(0); return; } if (r.status === 401) { setPhase(PH.IDLE); clearTok(); refreshTokForget(); setCmdStatus("That token was rejected (401). Paste a valid one.", "err"); showTokenPanel(); return; } let ghMsg = ""; try { const j = await r.json(); ghMsg = (j && j.message) || ""; } catch (_) {} let hint; @@ -998,14 +1023,13 @@

    Finish the update — add a one-time token

    // actually paused a check ("Action required") -- point at Approve and run. function reviewMsg(pr, ver, lead, note) { return (lead || "") + - `Pull request #${pr.number} is open. ` + - `Merge it to finish updating to v${esc(ver)}.` + + `Pull request #${pr.number} is open. Merge it to finish updating to v${ver}.` + (note ? " " + note : ""); } function showReview(pr, ver, lead) { prog.prUrl = pr.html_url; prog.prNum = pr.number; setPhase(PH.REVIEW, reviewMsg(pr, ver, lead, - `Most updates change only configuration and tooling, so there are no checks to wait for — you can merge right away.`)); + "Most updates change only configuration and tooling, so there are no checks to wait for - you can merge right away.")); const sha = pr.head && pr.head.sha; if (!sha) return; // Best-effort: read the head commit's checks and tailor the note to reality. @@ -1019,13 +1043,13 @@

    Finish the update — add a one-time token

    const running = runs.filter((r) => r.status !== "completed" && r.status !== "waiting" && r.status !== "action_required"); let note; if (paused.length) - note = `GitHub paused a check (“Action required”) because the pull request was opened by automation — open its Checks tab (or the repo’s Actions tab), click Approve and run, then merge.`; + note = "GitHub paused a check (\"Action required\") because the pull request was opened by automation - open its Checks tab (or the repository's Actions tab), click Approve and run, then merge."; else if (running.length) - note = `Its checks are still running — they finish on their own, then you can merge.`; + note = "Its checks are still running - they finish on their own, then you can merge."; else if (!runs.length) - note = `This update has no checks to wait for — it’s ready to merge now.`; + note = "This update has no checks to wait for - it's ready to merge now."; else - note = `Its checks have passed — it’s ready to merge now.`; + note = "Its checks have passed - it's ready to merge now."; renderProgress(reviewMsg(pr, ver, lead, note)); }) .catch(() => {}); @@ -1364,8 +1388,8 @@

    Finish the update — add a one-time token

    prog.ts = s.ts || Date.now(); prog.runId = s.runId || 0; prog.runUrl = s.runUrl || ""; prog.prUrl = s.prUrl || ""; prog.prNum = s.prNum || 0; if (s.phase === PH.PUBLISHING) { startPublishPoll(prog.ver); } - else if (prog.prNum) { setPhase(PH.REVIEW, `Resuming… pull request #${prog.prNum} is open. Merge it to finish updating to v${esc(prog.ver)}. Most updates have no checks to wait for — you can merge right away.`); pollPrMerged(prog.prNum, 0); } - else if (prog.runId || s.phase >= PH.QUEUED) { setPhase(PH.RUNNING, "Resuming… checking the update workflow’s progress."); pollRun(0); } + else if (prog.prNum) { setPhase(PH.REVIEW, `Resuming\u2026 pull request #${prog.prNum} is open. Merge it to finish updating to v${prog.ver}. Most updates have no checks to wait for - you can merge right away.`); pollPrMerged(prog.prNum, 0); } + else if (prog.runId || s.phase >= PH.QUEUED) { setPhase(PH.RUNNING, "Resuming\u2026 checking the update workflow's progress."); pollRun(0); } else { startPrPoll(); } } diff --git a/actions/dashboard/builds.html b/actions/dashboard/builds.html index 8f35c5c5d..5deadf310 100644 --- a/actions/dashboard/builds.html +++ b/actions/dashboard/builds.html @@ -364,7 +364,11 @@

    Save and apply

    const r = repo.includes("/") ? ` -R ${repo}` : ""; return `gh workflow run reconfigure.yml${r} \\\n -f builds="${buildsInput()}"`; } - function workflowUrl() { return repo.includes("/") ? "https://github.com/" + repo + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; } + function workflowUrl() { + const parts = repo.trim().split("/").filter(Boolean); + const repoPath = parts.length === 2 ? parts.map(encodeURIComponent).join("/") : ""; + return repoPath ? "https://github.com/" + repoPath + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; + } function update() { $("ghCmd").textContent = genGhCmd(); const wf = $("wfLink"); if (wf) wf.href = workflowUrl(); @@ -379,9 +383,19 @@

    Save and apply

    qp.set("actions", "write"); return "https://github.com/settings/personal-access-tokens/new?" + qp.toString(); } - function setApplyStatus(html, kind) { + function setApplyStatus(message, kind, workflowLink) { const st = $("applyStatus"); if (!st) return; - st.innerHTML = html || ""; st.className = "status" + (kind ? " " + kind : ""); + st.replaceChildren(message || ""); + if (workflowLink) { + const link = document.createElement("a"); + link.href = workflowLink; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = "View runs"; + st.appendChild(link); + st.appendChild(document.createTextNode(".")); + } + st.className = "status" + (kind ? " " + kind : ""); } function showTokenPanel(msg) { const p = $("tokPanel"); if (!p) return; @@ -407,7 +421,7 @@

    Save and apply

    if (btn) { btn.disabled = false; btn.textContent = "Save and apply"; } if (r.status === 204) { hideTokenPanel(); - setApplyStatus("Queued Reconfigure. It will open a pull request when the settings file is updated. View runs.", "ok"); + setApplyStatus("Queued Reconfigure. It will open a pull request when the settings file is updated. ", "ok", workflowUrl()); return; } if (r.status === 401) { try { localStorage.removeItem(TOK_KEY); } catch (_) {} showTokenPanel("That token was rejected. Paste a valid fine-grained token with Actions: Read and write for this repository."); setApplyStatus("Token rejected (401).", "err"); return; } @@ -416,7 +430,7 @@

    Save and apply

    setApplyStatus("Dispatch failed (HTTP " + r.status + ").", "err"); } catch (e) { if (btn) { btn.disabled = false; btn.textContent = "Save and apply"; } - setApplyStatus("Network error: " + escapeHtml(String(e && e.message || e)), "err"); + setApplyStatus("Network error: " + String(e && e.message || e), "err"); } } diff --git a/actions/dashboard/configure.html b/actions/dashboard/configure.html index 75c78c32e..2fba5bd4f 100644 --- a/actions/dashboard/configure.html +++ b/actions/dashboard/configure.html @@ -1243,8 +1243,9 @@

    Save and apply

    } function workflowUrl() { - const repo = $("repo").value.trim(); - return repo.includes("/") ? "https://github.com/" + repo + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; + const parts = $("repo").value.trim().split("/").filter(Boolean); + const repoPath = parts.length === 2 ? parts.map(encodeURIComponent).join("/") : ""; + return repoPath ? "https://github.com/" + repoPath + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; } function token() { try { return localStorage.getItem(TOK_KEY) || ""; } catch (_) { return ""; } } function tokenUrl() { @@ -1257,9 +1258,18 @@

    Save and apply

    qp.set("actions", "write"); return "https://github.com/settings/personal-access-tokens/new?" + qp.toString(); } - function setApplyStatus(html, kind) { + function setApplyStatus(message, kind, workflowLink) { const st = $("applyStatus"); if (!st) return; - st.innerHTML = html || ""; + st.replaceChildren(message || ""); + if (workflowLink) { + const link = document.createElement("a"); + link.href = workflowLink; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = "View runs"; + st.appendChild(link); + st.appendChild(document.createTextNode(".")); + } st.className = "status" + (kind ? " " + kind : ""); } function showTokenPanel(msg) { @@ -1281,7 +1291,7 @@

    Save and apply

    async function explainDispatch404(repo, file) { try { const r = await fetch("https://api.github.com/repos/" + repo + "/contents/.github/workflows/" + file, { headers: { "Accept": "application/vnd.github+json" }, cache: "no-cache" }); - if (r.ok) return "Not a token problem — " + escapeHtml(file) + " exists in this repository, but GitHub has not registered it as a runnable workflow, so dispatching it returns 404. This happens when workflow files were pushed while GitHub Actions was disabled (typical for fork installs). Fix: run What's New ▸ Update now — tooling updates rewrite every workflow file (each carries a version stamp), and that push makes GitHub register them — or push any commit that edits .github/workflows/" + escapeHtml(file) + ", then retry."; + if (r.ok) return `Not a token problem - ${file} exists in this repository, but GitHub has not registered it as a runnable workflow, so dispatching it returns 404. This happens when workflow files were pushed while GitHub Actions was disabled (typical for fork installs). Fix: run What's New > Update now - tooling updates rewrite every workflow file (each carries a version stamp), and that push makes GitHub register them - or push any commit that edits .github/workflows/${file}, then retry.`; } catch (_) {} return ""; } @@ -1301,7 +1311,7 @@

    Save and apply

    if (btn) { btn.disabled = false; btn.textContent = "Save and apply"; } if (r.status === 204) { hideTokenPanel(); - setApplyStatus("Queued Reconfigure. It will open a pull request when the settings files are updated. View runs.", "ok"); + setApplyStatus("Queued Reconfigure. It will open a pull request when the settings files are updated. ", "ok", workflowUrl()); return; } if (r.status === 401) { try { localStorage.removeItem(TOK_KEY); } catch (_) {} showTokenPanel("That token was rejected. Paste a valid fine-grained token with Actions: Read and write for this repository."); setApplyStatus("Token rejected (401).", "err"); return; } @@ -1316,7 +1326,7 @@

    Save and apply

    setApplyStatus("Dispatch failed (HTTP " + r.status + ").", "err"); } catch (e) { if (btn) { btn.disabled = false; btn.textContent = "Save and apply"; } - setApplyStatus("Network error: " + escapeHtml(String(e && e.message || e)), "err"); + setApplyStatus("Network error: " + String(e && e.message || e), "err"); } } diff --git a/actions/dashboard/integrate.html b/actions/dashboard/integrate.html index 615b9f1d7..15df1dba9 100644 --- a/actions/dashboard/integrate.html +++ b/actions/dashboard/integrate.html @@ -743,21 +743,42 @@

    4. Finish setup

    const repo = $("repo").value.trim() || "/"; const rel = selectedRelease(); const pinned = rel && catalog.containers && rel.id !== catalog.containers.default; + const varsStep = $("varsStep"); + if (!varsStep) return; + const code = text => { + const el = document.createElement("code"); + el.className = "inl"; + el.textContent = text; + return el; + }; if (isGitLab()) { - $("varsStep").innerHTML = - `Optional GitLab CI/CD variables can override installer defaults later; the initial scaffold records LabVIEW ${ver} in .github/labview-ci.yml.`; + varsStep.replaceChildren( + `Optional GitLab CI/CD variables can override installer defaults later; the initial scaffold records LabVIEW ${ver} in `, + code(".github/labview-ci.yml"), + "." + ); } else if (pinned) { const s = source(); const srcGhcr = "ghcr.io/" + s.repo.toLowerCase(); - $("varsStep").innerHTML = - `Pinning this repo to ${esc(rel.label)}: the browser install sets ` + - `LCWC_BASE_IMAGE=${esc(srcGhcr)}-labview-base:${esc(rel.id)} and ` + - `LCWC_LINUX_BASE_IMAGE=${esc(srcGhcr)}-labview:linux-${esc(rel.id)} as Actions variables ` + - `(command-line installs: set them with gh variable set).`; + const release = document.createElement("strong"); + release.textContent = rel.label; + varsStep.replaceChildren( + "Pinning this repo to ", + release, + ": the browser install sets ", + code(`LCWC_BASE_IMAGE=${srcGhcr}-labview-base:${rel.id}`), + " and ", + code(`LCWC_LINUX_BASE_IMAGE=${srcGhcr}-labview:linux-${rel.id}`), + " as Actions variables (command-line installs: set them with ", + code("gh variable set"), + ")." + ); } else { - $("varsStep").innerHTML = - `Optionally pin settings as Actions variables: ` + - `gh variable set LABVIEW_VERSION -R ${repo} -b ${ver} (has a safe fallback).`; + varsStep.replaceChildren( + "Optionally pin settings as Actions variables: ", + code(`gh variable set LABVIEW_VERSION -R ${repo} -b ${ver}`), + " (has a safe fallback)." + ); } } @@ -2351,27 +2372,94 @@

    4. Finish setup

    const dashLink0 = $("insDashLink"); if (dashLink0) dashLink0.style.display = "none"; const showDashLink = () => { const dl = $("insDashLink"); - if (dl) { dl.href = url; dl.innerHTML = "Open your dashboard ↗"; dl.style.display = ""; } + if (dl) { dl.href = url; dl.textContent = "Open your dashboard \u2197"; dl.style.display = ""; } }; setInsStatus(`✓ Done \u2014 opened pull request #${pr.number} in ${esc(repo)}.`, "ok"); const done = []; - if (setup.kept) done.push(`Kept your existing settings — ${esc(setup.kept)}.`); - if (setup.removed && setup.removed.length) done.push(`Removed ${setup.removed.length} old tooling file${setup.removed.length > 1 ? "s" : ""} no longer used (in the pull request).`); - if (setup.readme) done.push(`Added a CI dashboard badge & link to your README (in the pull request).`); - if (setup.workflowPerms === "done") done.push(`Set Workflow permissions to read & write.`); - if (setup.toolingToken === "done") done.push(`Saved a one-time update token — future Update now clicks open their pull request automatically.`); - if (setup.buildImages && autoMerge) done.push(`Watching your worker container image build${setup.buildImage && setup.buildLinuxImage ? "s" : ""} — Windows and Linux image workflows are tracked separately below so it is clear which background work is still running.`); + const addDone = build => { + const item = document.createElement("li"); + build(item); + done.push(item); + }; + const appendText = (parent, text) => parent.appendChild(document.createTextNode(text)); + const appendEmphasis = (parent, tag, text, className) => { + const el = document.createElement(tag); + if (className) el.className = className; + el.textContent = text; + parent.appendChild(el); + return el; + }; + if (setup.kept) addDone(item => { + appendText(item, "Kept your existing settings - "); + appendEmphasis(item, "strong", setup.kept); + appendText(item, "."); + }); + if (setup.removed && setup.removed.length) addDone(item => { + appendText(item, "Removed "); + appendEmphasis(item, "strong", setup.removed.length); + appendText(item, ` old tooling file${setup.removed.length > 1 ? "s" : ""} no longer used (in the pull request).`); + }); + if (setup.readme) addDone(item => appendText(item, "Added a CI dashboard badge and link to your README (in the pull request).")); + if (setup.workflowPerms === "done") addDone(item => { + appendText(item, "Set "); + appendEmphasis(item, "strong", "Workflow permissions"); + appendText(item, " to read and write."); + }); + if (setup.toolingToken === "done") addDone(item => { + appendText(item, "Saved a one-time "); + appendEmphasis(item, "strong", "update token"); + appendText(item, " - future "); + appendEmphasis(item, "em", "Update now"); + appendText(item, " clicks open their pull request automatically."); + }); + if (setup.buildImages && autoMerge) addDone(item => { + appendText(item, "Watching your "); + appendEmphasis(item, "strong", `worker container image build${setup.buildImage && setup.buildLinuxImage ? "s" : ""}`); + appendText(item, " - Windows and Linux image workflows are tracked separately below so it is clear which background work is still running."); + }); // Show the dashboard URL as PLAIN TEXT until Pages is confirmed serving it // (revealLiveLink upgrades #insLiveUrl to a real link) so we never present a // link that 404s. When Pages already existed it's almost certainly live, so // that case links right away. - if (setup.pages === "done") done.push(`Enabled GitHub Pages — your dashboard will be at ${esc(url)}It publishes within a minute of the install merge — no CI run required; the link activates here the moment it’s live.`); - else if (setup.pages === "exists") done.push(`GitHub Pages is already on — dashboard at ${esc(url)}.`); - else if (setup.hasDashboard) done.push(`Your dashboard will be at ${esc(url)} once GitHub Pages is on.`); + const appendDashboardUrl = (item, live) => { + const location = document.createElement("span"); + location.id = "insLiveUrl"; + if (live) { + const link = document.createElement("a"); + link.href = url; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = url; + location.appendChild(link); + } else { + appendEmphasis(location, "code", url, "inl"); + } + item.appendChild(location); + }; + if (setup.pages === "done") addDone(item => { + appendText(item, "Enabled "); + appendEmphasis(item, "strong", "GitHub Pages"); + appendText(item, " - your dashboard will be at "); + appendDashboardUrl(item, false); + appendEmphasis(item, "small", "It publishes within a minute of the install merge - no CI run required; the link activates here the moment it's live.").id = "insLiveNote"; + }); + else if (setup.pages === "exists") addDone(item => { + appendEmphasis(item, "strong", "GitHub Pages"); + appendText(item, " is already on - dashboard at "); + appendDashboardUrl(item, true); + appendText(item, "."); + }); + else if (setup.hasDashboard) addDone(item => { + appendText(item, "Your dashboard will be at "); + appendDashboardUrl(item, false); + appendText(item, " once "); + appendEmphasis(item, "strong", "GitHub Pages"); + appendText(item, " is on."); + }); const doneEl = $("insDone"); - if (doneEl) { doneEl.innerHTML = done.map(d => `
  • ${d}
  • `).join(""); doneEl.style.display = done.length ? "" : "none"; } + if (doneEl) { doneEl.replaceChildren(...done); doneEl.style.display = done.length ? "" : "none"; } const steps = []; steps.push(`Review and merge pull request #${pr.number}.`); diff --git a/actions/dashboard/lvci-header.js b/actions/dashboard/lvci-header.js index c590dfbec..5609320d6 100644 --- a/actions/dashboard/lvci-header.js +++ b/actions/dashboard/lvci-header.js @@ -1284,7 +1284,7 @@ sel.addEventListener('change', function () { syncSteps(); var v = sel.value; - if (v && v !== cfg.sha) window.location.href = docDest(v); + if (v && v !== cfg.sha && SHA_RE.test(v)) window.location.href = docDest(v); }); sel._lvciSync = function () { rp.refresh(); syncSteps(); }; wrap.appendChild(lbl); wrap.appendChild(prev); wrap.appendChild(rp.button); wrap.appendChild(next); @@ -1953,10 +1953,20 @@ pendbar.id = 'lvci-pendbar'; pendbar.className = 'lvci-depbar'; pendbar.setAttribute('role', 'alert'); - pendbar.innerHTML = - '\u26A0\uFE0F Dependencies need to be installed into your containers. ' + - 'Your project declares dependencies that are not yet baked into the worker container(s); container CI may error or show broken code until you update them. ' + - 'Review & update dependencies \u2197'; + var pendText = document.createElement('span'); + pendText.className = 'lvci-dep-txt'; + var pendTitle = document.createElement('strong'); + pendTitle.textContent = '\u26A0\uFE0F Dependencies need to be installed into your containers. '; + var pendDescription = document.createElement('span'); + pendDescription.className = 'lvci-dep-sub'; + pendDescription.textContent = 'Your project declares dependencies that are not yet baked into the worker container(s); container CI may error or show broken code until you update them. '; + var pendLink = document.createElement('a'); + pendLink.href = navBase + '/dependencies.html'; + pendLink.textContent = 'Review & update dependencies \u2197'; + pendText.appendChild(pendTitle); + pendText.appendChild(pendDescription); + pendText.appendChild(pendLink); + pendbar.appendChild(pendText); // Global attention bar (failure banner) — hidden until the activity poll // finds a workflow whose newest run failed; names it + links to the run. diff --git a/actions/dashboard/unit-tests.html b/actions/dashboard/unit-tests.html index 09ee5927c..aadbf1fdb 100644 --- a/actions/dashboard/unit-tests.html +++ b/actions/dashboard/unit-tests.html @@ -332,7 +332,7 @@

    4. Review & apply

    const locs = (s.locations||[]).map(x=>x.trim()).filter(Boolean); if (locs.length){ lines.push(" locations:"); - locs.forEach(l => lines.push(` - "${l.replace(/"/g,'\\"')}"`)); + locs.forEach(l => lines.push(` - ${JSON.stringify(l)}`)); } else { lines.push(" locations: []"); } @@ -491,8 +491,9 @@

    4. Review & apply

    } function workflowUrl(){ - const repo = $("repo").value; - return repo.includes("/") ? "https://github.com/" + repo + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; + const parts = $("repo").value.trim().split("/").filter(Boolean); + const repoPath = parts.length === 2 ? parts.map(encodeURIComponent).join("/") : ""; + return repoPath ? "https://github.com/" + repoPath + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; } function token(){ try { return localStorage.getItem(TOK_KEY) || ""; } catch (_) { return ""; } } function tokenUrl(){ @@ -505,9 +506,18 @@

    4. Review & apply

    qp.set("actions", "write"); return "https://github.com/settings/personal-access-tokens/new?" + qp.toString(); } - function setApplyStatus(html, kind){ + function setApplyStatus(message, kind, workflowLink){ const st = $("applyStatus"); if (!st) return; - st.innerHTML = html || ""; + st.replaceChildren(message || ""); + if (workflowLink){ + const link = document.createElement("a"); + link.href = workflowLink; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = "View runs"; + st.appendChild(link); + st.appendChild(document.createTextNode(".")); + } st.className = "status" + (kind ? " " + kind : ""); } function showTokenPanel(msg){ @@ -534,7 +544,7 @@

    4. Review & apply

    if (btn){ btn.disabled = false; btn.textContent = "Save & Apply"; } if (r.status === 204){ hideTokenPanel(); - setApplyStatus("Queued Reconfigure. It will open a pull request when the settings file is updated. View runs.", "ok"); + setApplyStatus("Queued Reconfigure. It will open a pull request when the settings file is updated. ", "ok", workflowUrl()); return; } if (r.status === 401){ try { localStorage.removeItem(TOK_KEY); } catch (_) {} showTokenPanel("That token was rejected. Paste a valid fine-grained token with Actions: Read and write for this repository."); setApplyStatus("Token rejected (401).", "err"); return; } @@ -543,7 +553,7 @@

    4. Review & apply

    setApplyStatus("Dispatch failed (HTTP " + r.status + ").", "err"); }catch(e){ if (btn){ btn.disabled = false; btn.textContent = "Save & Apply"; } - setApplyStatus("Network error: " + esc(String(e && e.message || e)), "err"); + setApplyStatus("Network error: " + String(e && e.message || e), "err"); } } diff --git a/actions/dashboard/vi-analyzer.html b/actions/dashboard/vi-analyzer.html index 00e15050b..e5fadd171 100644 --- a/actions/dashboard/vi-analyzer.html +++ b/actions/dashboard/vi-analyzer.html @@ -370,7 +370,11 @@

    Save and apply

    ` -f via_default="${ci.def}"` + (ci.rules ? ` \\\n -f via_rules="${ci.rules}"` : ""); } - function workflowUrl() { return repo.includes("/") ? "https://github.com/" + repo + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; } + function workflowUrl() { + const parts = repo.trim().split("/").filter(Boolean); + const repoPath = parts.length === 2 ? parts.map(encodeURIComponent).join("/") : ""; + return repoPath ? "https://github.com/" + repoPath + "/actions/workflows/reconfigure.yml" : "https://github.com/actions/workflows/reconfigure.yml"; + } function update() { $("ghCmd").textContent = genGhCmd(); const wf = $("wfLink"); if (wf) wf.href = workflowUrl(); @@ -385,9 +389,18 @@

    Save and apply

    qp.set("actions", "write"); return "https://github.com/settings/personal-access-tokens/new?" + qp.toString(); } - function setApplyStatus(html, kind) { + function setApplyStatus(message, kind, workflowLink) { const st = $("applyStatus"); if (!st) return; - st.innerHTML = html || ""; + st.replaceChildren(message || ""); + if (workflowLink) { + const link = document.createElement("a"); + link.href = workflowLink; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = "View runs"; + st.appendChild(link); + st.appendChild(document.createTextNode(".")); + } st.className = "status" + (kind ? " " + kind : ""); } function showTokenPanel(msg) { @@ -415,7 +428,7 @@

    Save and apply

    if (btn) { btn.disabled = false; btn.textContent = "Save and apply"; } if (r.status === 204) { hideTokenPanel(); - setApplyStatus("Queued Reconfigure. It will open a pull request when the settings file is updated. View runs.", "ok"); + setApplyStatus("Queued Reconfigure. It will open a pull request when the settings file is updated. ", "ok", workflowUrl()); return; } if (r.status === 401) { try { localStorage.removeItem(TOK_KEY); } catch (_) {} showTokenPanel("That token was rejected. Paste a valid fine-grained token with Actions: Read and write for this repository."); setApplyStatus("Token rejected (401).", "err"); return; } @@ -424,7 +437,7 @@

    Save and apply

    setApplyStatus("Dispatch failed (HTTP " + r.status + ").", "err"); } catch (e) { if (btn) { btn.disabled = false; btn.textContent = "Save and apply"; } - setApplyStatus("Network error: " + escapeHtml(String(e && e.message || e)), "err"); + setApplyStatus("Network error: " + String(e && e.message || e), "err"); } } diff --git a/actions/dashboard/whats-new.html b/actions/dashboard/whats-new.html index de1dee00f..d3c31e29b 100644 --- a/actions/dashboard/whats-new.html +++ b/actions/dashboard/whats-new.html @@ -682,16 +682,41 @@

    Finish the update — add a one-time token

    prog.phase === PH.PUBLISHING ? ("Publishing v" + prog.ver + "\u2026") : prog.phase === PH.ERROR ? "Update didn\u2019t start" : ("Updating to v" + prog.ver + "\u2026"); - const rp = $("upRepo"); if (rp) rp.innerHTML = state.repo ? `${esc(state.repo)}` : ""; - if (msg != null) { const m = $("upMsg"); if (m) m.innerHTML = msg; } - const links = []; - if (prog.phase === PH.DONE) { - const dash = clientPagesUrl(); - if (dash) links.push(`Go to your dashboard ↗`); + const rp = $("upRepo"); + if (rp) { + rp.replaceChildren(); + if (state.repo) { + const link = document.createElement("a"); + link.href = ghUrl(state.repo); + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = state.repo; + rp.appendChild(link); + } + } + if (msg != null) { const m = $("upMsg"); if (m) m.textContent = msg; } + const lz = $("upLinks"); + if (lz) { + lz.replaceChildren(); + const appendLink = (url, text, primary) => { + let safeUrl; + try { + const parsed = new URL(url, location.href); + if (parsed.protocol !== "https:") return; + safeUrl = parsed.href; + } catch (_) { return; } + const link = document.createElement("a"); + if (primary) link.className = "primary"; + link.href = safeUrl; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = text; + lz.appendChild(link); + }; + if (prog.phase === PH.DONE) appendLink(clientPagesUrl(), "Go to your dashboard \u2197", true); + if (prog.prUrl) appendLink(prog.prUrl, `Open pull request${prog.prNum ? " #" + prog.prNum : ""} \u2197`); + if (prog.runUrl) appendLink(prog.runUrl, "Open the workflow run \u2197"); } - if (prog.prUrl) links.push(`Open pull request${prog.prNum ? " #" + prog.prNum : ""} ↗`); - if (prog.runUrl) links.push(`Open the workflow run ↗`); - const lz = $("upLinks"); if (lz) lz.innerHTML = links.join(""); const foot = $("upProgFoot"); if (foot) { // Dismiss only when the update has truly stopped (done/failed). While it @@ -755,13 +780,13 @@

    Finish the update — add a one-time token

    prog.runId = run.id; prog.runUrl = run.html_url; if (run.status === "completed") { if (run.conclusion === "success") { startPrPoll(); return; } - setPhase(PH.ERROR, `The update workflow finished with ${esc(run.conclusion || "a failure")}. Open the run to see why, then try again.`); + setPhase(PH.ERROR, `The update workflow finished with ${run.conclusion || "a failure"}. Open the run to see why, then try again.`); return; } setPhase(run.status === "in_progress" ? PH.RUNNING : PH.QUEUED, run.status === "in_progress" - ? "Building the update on GitHub Actions… this usually takes under a minute." - : "Queued on GitHub Actions… waiting for a runner."); + ? "Building the update on GitHub Actions\u2026 this usually takes under a minute." + : "Queued on GitHub Actions\u2026 waiting for a runner."); } if (tries < 40) later(() => pollRun(tries + 1), 3000); else startPrPoll(); // give up tracking the run; fall back to PR detection @@ -769,7 +794,7 @@

    Finish the update — add a one-time token

    } function startPrPoll() { - if (prog.phase !== PH.APPLYING) setPhase(PH.APPLYING, "Applying the update to your repository…"); + if (prog.phase !== PH.APPLYING) setPhase(PH.APPLYING, "Applying the update to your repository\u2026"); pollOutcome(prog.ver, 0); } @@ -783,7 +808,7 @@

    Finish the update — add a one-time token

    prog.ver = ver; prog.ts = Date.now(); prog.runId = 0; prog.runUrl = ""; prog.prUrl = ""; prog.prNum = 0; clearTimers(); setCmdStatus("", null); - setPhase(PH.DISPATCH, "Starting the update…"); + setPhase(PH.DISPATCH, "Starting the update\u2026"); resolveBranch().then((branch) => { return fetch(`https://api.github.com/repos/${state.repo}/actions/workflows/apply-tooling-update.yml/dispatches`, { method: "POST", @@ -793,7 +818,7 @@

    Finish the update — add a one-time token

    body: JSON.stringify({ ref: branch, inputs: { source_ref: "v" + ver } }) }); }).then(async (r) => { - if (r.status === 204) { setPhase(PH.QUEUED, "Queued on GitHub Actions… waiting for a runner."); pollRun(0); return; } + if (r.status === 204) { setPhase(PH.QUEUED, "Queued on GitHub Actions\u2026 waiting for a runner."); pollRun(0); return; } if (r.status === 401) { setPhase(PH.IDLE); clearTok(); refreshTokForget(); setCmdStatus("That token was rejected (401). Paste a valid one.", "err"); showTokenPanel(); return; } let ghMsg = ""; try { const j = await r.json(); ghMsg = (j && j.message) || ""; } catch (_) {} let hint; @@ -998,14 +1023,13 @@

    Finish the update — add a one-time token

    // actually paused a check ("Action required") -- point at Approve and run. function reviewMsg(pr, ver, lead, note) { return (lead || "") + - `Pull request #${pr.number} is open. ` + - `Merge it to finish updating to v${esc(ver)}.` + + `Pull request #${pr.number} is open. Merge it to finish updating to v${ver}.` + (note ? " " + note : ""); } function showReview(pr, ver, lead) { prog.prUrl = pr.html_url; prog.prNum = pr.number; setPhase(PH.REVIEW, reviewMsg(pr, ver, lead, - `Most updates change only configuration and tooling, so there are no checks to wait for — you can merge right away.`)); + "Most updates change only configuration and tooling, so there are no checks to wait for - you can merge right away.")); const sha = pr.head && pr.head.sha; if (!sha) return; // Best-effort: read the head commit's checks and tailor the note to reality. @@ -1019,13 +1043,13 @@

    Finish the update — add a one-time token

    const running = runs.filter((r) => r.status !== "completed" && r.status !== "waiting" && r.status !== "action_required"); let note; if (paused.length) - note = `GitHub paused a check (“Action required”) because the pull request was opened by automation — open its Checks tab (or the repo’s Actions tab), click Approve and run, then merge.`; + note = "GitHub paused a check (\"Action required\") because the pull request was opened by automation - open its Checks tab (or the repository's Actions tab), click Approve and run, then merge."; else if (running.length) - note = `Its checks are still running — they finish on their own, then you can merge.`; + note = "Its checks are still running - they finish on their own, then you can merge."; else if (!runs.length) - note = `This update has no checks to wait for — it’s ready to merge now.`; + note = "This update has no checks to wait for - it's ready to merge now."; else - note = `Its checks have passed — it’s ready to merge now.`; + note = "Its checks have passed - it's ready to merge now."; renderProgress(reviewMsg(pr, ver, lead, note)); }) .catch(() => {}); @@ -1364,8 +1388,8 @@

    Finish the update — add a one-time token

    prog.ts = s.ts || Date.now(); prog.runId = s.runId || 0; prog.runUrl = s.runUrl || ""; prog.prUrl = s.prUrl || ""; prog.prNum = s.prNum || 0; if (s.phase === PH.PUBLISHING) { startPublishPoll(prog.ver); } - else if (prog.prNum) { setPhase(PH.REVIEW, `Resuming… pull request #${prog.prNum} is open. Merge it to finish updating to v${esc(prog.ver)}. Most updates have no checks to wait for — you can merge right away.`); pollPrMerged(prog.prNum, 0); } - else if (prog.runId || s.phase >= PH.QUEUED) { setPhase(PH.RUNNING, "Resuming… checking the update workflow’s progress."); pollRun(0); } + else if (prog.prNum) { setPhase(PH.REVIEW, `Resuming\u2026 pull request #${prog.prNum} is open. Merge it to finish updating to v${prog.ver}. Most updates have no checks to wait for - you can merge right away.`); pollPrMerged(prog.prNum, 0); } + else if (prog.runId || s.phase >= PH.QUEUED) { setPhase(PH.RUNNING, "Resuming\u2026 checking the update workflow's progress."); pollRun(0); } else { startPrPoll(); } }