From 496a3eef0ab8c3f90bc34f44644a12298c3780ea Mon Sep 17 00:00:00 2001 From: Nish <257724087+nish3451@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:29:10 +0530 Subject: [PATCH] fix(public): keep the live heading-hierarchy guard out of the blocking ci chain PR #78 wired test-public-live-heading-hierarchy.mjs into npm run ci and npm test, so the pull request stayed red while the deployment is stale (the live site still serves the June-20 bundle; the release lane is fail-closed until the Cloudflare Pages token is provisioned). The blocking chain must stay green on repo state alone. Land the guard without the blocking wiring, matching the live-site soft-404 pattern: - scripts/test-public-live-heading-hierarchy.mjs now asserts it runs standalone via site:check-live-heading-hierarchy and that ci/test do not include it - npm run site:check-live-heading-hierarchy entry point - .github/workflows/live-site-check.yml: nightly + manual dispatch job for the guard, next to the soft-404 detector npm run ci and npm test pass with the live site stale; the guard fails loudly on demand and in the nightly net instead. Co-authored-by: CommandCodeBot --- .github/workflows/live-site-check.yml | 49 +++++++ package.json | 1 + .../test-public-live-heading-hierarchy.mjs | 123 ++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 .github/workflows/live-site-check.yml create mode 100644 scripts/test-public-live-heading-hierarchy.mjs diff --git a/.github/workflows/live-site-check.yml b/.github/workflows/live-site-check.yml new file mode 100644 index 00000000..d0eb1e81 --- /dev/null +++ b/.github/workflows/live-site-check.yml @@ -0,0 +1,49 @@ +name: Live Site Check + +# Guards the deployed tinystudio.in against soft-404s, stale bundles, and +# regressed heading hierarchies without blocking pull requests. The deploy +# lane (deploy-public-site.yml) verifies the live site after every publish; +# this workflow is the nightly net that catches a stale or misconfigured +# deployment within a day even when nothing merges. It is deliberately NOT +# part of `npm run ci` - that blocking chain must stay green on repo state +# alone. +# +# This job is expected to FAIL while the deployment is stale (currently the +# June-20 bundle); the red run is the loud signal to re-deploy from +# origin/main, not a reason to disable the workflow. + +on: + schedule: + # Nightly: catch stale or broken deployments (soft-404s, offline site) + # within a day even when no pull requests are open. + - cron: "23 3 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: live-site-check-${{ github.workflow }} + cancel-in-progress: false + +jobs: + live-soft-404: + runs-on: [self-hosted, linux, x64, vps-verify] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 20 + cache: npm + - run: npm ci --ignore-scripts + - run: node scripts/check-public-live-soft-404.mjs + live-heading-hierarchy: + runs-on: [self-hosted, linux, x64, vps-verify] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 20 + cache: npm + - run: npm ci --ignore-scripts + - run: node scripts/test-public-live-heading-hierarchy.mjs diff --git a/package.json b/package.json index 6f5a589b..6150a46b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "client:proof-review": "node scripts/review-client-proof.mjs", "retention:automation-check": "node scripts/check-retention-automation.mjs", "site:prepare": "node scripts/prepare-static-site-bundle.mjs", + "site:check-live-heading-hierarchy": "node scripts/test-public-live-heading-hierarchy.mjs", "claims:check": "node scripts/check-outbound-claim-safety.mjs", "config:check": "node scripts/check-agency-defaults.mjs", "send:configure": "node scripts/configure-sender-setup.mjs", diff --git a/scripts/test-public-live-heading-hierarchy.mjs b/scripts/test-public-live-heading-hierarchy.mjs new file mode 100644 index 00000000..563adc03 --- /dev/null +++ b/scripts/test-public-live-heading-hierarchy.mjs @@ -0,0 +1,123 @@ +import { readFileSync } from "node:fs" +import { fileURLToPath } from "node:url" +import { dirname, join } from "node:path" + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..") +const read = (p) => readFileSync(join(ROOT, p), "utf8") + +let failures = 0 +let checks = 0 +const ok = (cond, msg) => { + checks++ + if (cond) console.log(` ok ${msg}`) + else { + failures++ + console.error(` FAIL ${msg}`) + } +} + +// The deployed /contact/ page must keep the repaired heading outline +// (H1 -> H2 cards -> H2 footer -> H3 footer columns, no skipped levels). +// The local suite (test-public-heading-hierarchy.mjs) asserts the same +// outline against the worktree HTML; this guard re-asserts it against the +// page the live site actually serves, so a stale deployment (like the +// June-20 bundle still serving H3 cards) fails loudly instead of silently +// re-opening the 2026-08-09 dogfood item "Heading hierarchy needs cleanup +// on /contact" (finding 52753880dfc7, repaired in source by PR #18). +const LIVE_PAGES = [ + { + name: "Contact", + url: "https://tinystudio.in/contact/", + source: "public/contact/index.html" + } +] +const LIVE_CSS_URL = "https://tinystudio.in/styles.css" +const FETCH_TIMEOUT_MS = 10_000 + +// Heading levels in document order, e.g. [1, 2, 2, 2, 2, 3, 3, 3]. +const headingLevelsOf = (html) => + [...html.matchAll(/]*>/gi)].map((m) => Number(m[1])) + +// Count

used as .info-card card titles (inside
). +const infoCardTitleCount = (html) => + (html.match(/
]*>[\s\S]*? { + try { + const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }) + if (!res.ok) { + console.log(` ok skipped: ${url} answered ${res.status}, deployment not reachable - no assertions run for it`) + return null + } + return await res.text() + } catch (err) { + console.log(` ok skipped: ${url} unreachable (${err?.cause?.code ?? err?.name ?? "network error"}) - no assertions run for it`) + return null + } +} + +const assertRepairedOutline = (name, source, html) => { + const levels = headingLevelsOf(html) + ok(levels.length > 0, `live ${name} page contains at least one heading`) + ok(levels.filter((l) => l === 1).length === 1, `live ${name} page has exactly one H1`) + ok(levels[0] === 1, `live ${name} page has the H1 as the first heading in the outline`) + ok(infoCardTitleCount(html) === 3, `live ${name} page has the three card headings as H2s inside .info-card articles`) + const cardH2s = levels.filter((l) => l === 2).length + ok(cardH2s >= 4, `live ${name} page keeps the flat H2 band (card H2s plus the footer H2) before the footer H3s`) + let jumps = 0 + for (let i = 1; i < levels.length; i++) { + if (levels[i] - levels[i - 1] > 1) { + jumps++ + console.error(` bad transition H${levels[i - 1]} -> H${levels[i]} on ${name}`) + } + } + ok(jumps === 0, `live ${name} page has no heading-level jump greater than one (no H1 -> H3 skip)`) + if (failures > 0) { + console.error(` the deployed ${source} is stale: it misses the heading-hierarchy repair that the worktree copy of ${source} already has. Refresh the live deployment from origin/main.`) + } +} + +console.log("test-public-live-heading-hierarchy: the deployed tinystudio.in pages keep the repaired heading outline (no skipped levels)") + +console.log("A. live pages carry the repaired heading hierarchy") +for (const page of LIVE_PAGES) { + const html = await fetchLive(page.url) + if (html !== null) assertRepairedOutline(page.name, page.source, html) +} + +console.log("B. live stylesheet keeps the shared card-heading rule at the former card scale") +const css = await fetchLive(LIVE_CSS_URL) +if (css !== null) { + const ruleStart = css.indexOf(".info-card :is(h2, h3) {") + ok(ruleStart !== -1, `live styles.css defines .info-card :is(h2, h3) {`) + const ruleEnd = ruleStart === -1 ? -1 : css.indexOf("}", ruleStart) + const ruleBody = ruleStart === -1 ? "" : css.slice(ruleStart, ruleEnd) + for (const decl of ["margin-top: 12px", "font-size: clamp(1.65rem, 2vw, 2.35rem)", "max-width: none"]) { + ok(ruleBody.includes(decl), `live card rule keeps ${decl}`) + } + ok( + !/\.info-card\s+h3\s*{/.test(css), + "live styles.css replaced the old .info-card h3-only rule with the shared :is(h2, h3) rule" + ) + if (failures > 0) { + console.error(" the deployed stylesheet is stale: it misses the card-heading pairing that public/styles.css already has. Refresh the live deployment from origin/main.") + } +} + +console.log("C. standalone script wiring (out of the blocking ci/test chain)") +const pkg = JSON.parse(read("package.json")) +ok( + pkg.scripts["site:check-live-heading-hierarchy"] === "node scripts/test-public-live-heading-hierarchy.mjs", + "npm run site:check-live-heading-hierarchy runs this guard on demand" +) +ok( + !pkg.scripts.ci.includes("test-public-live-heading-hierarchy.mjs"), + "npm run ci does not block on the live-site guard (blocking chain stays green on repo state alone)" +) +ok( + !pkg.scripts.test.includes("test-public-live-heading-hierarchy.mjs"), + "npm test does not block on the live-site guard" +) + +console.log(`\n${checks} checks, ${failures} failures`) +process.exit(failures === 0 ? 0 : 1)