Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/live-site-check.yml
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Point the soft-404 job at an existing script

Every scheduled or manually dispatched run reaches this step and exits with MODULE_NOT_FOUND before making any live request, so the workflow remains red regardless of whether the deployment has a soft 404. A repo-wide filename/reference search and the reviewed commit tree contain only scripts/test-public-soft-404.mjs (the local fixture test) and scripts/check-public-live-deploy.mjs; scripts/check-public-live-soft-404.mjs does not exist, so this job needs to invoke or add an actual live checker.

Useful? React with 👍 / 👎.

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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
123 changes: 123 additions & 0 deletions scripts/test-public-live-heading-hierarchy.mjs
Original file line number Diff line number Diff line change
@@ -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(/<h([1-6])\b[^>]*>/gi)].map((m) => Number(m[1]))

// Count <h2> used as .info-card card titles (inside <article class="info-card">).
const infoCardTitleCount = (html) =>
(html.match(/<article class="info-card[^"]*"[^>]*>[\s\S]*?<h2\b/gi) || []).length
Comment on lines +42 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound card-title matching to each article

When the third card title regresses from H2 to H3, this regex crosses the card's closing </article> and consumes the footer H2 instead, so it still reports three matching card titles. On the current contact page the additional H2s also satisfy the cardH2s >= 4 check and the resulting outline has no upward level jump, allowing precisely this single-card heading regression to pass; parse each article independently or prevent the match from crossing </article>.

Useful? React with 👍 / 👎.


const fetchLive = async (url) => {
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
Comment on lines +48 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail when a live resource returns a non-success status

When /contact/ or /styles.css returns a persistent 404 or 5xx response, fetch has successfully reached the deployment, but this branch reports the resource as skipped and returns null; both callers then omit every live assertion and the script can exit 0 after checking only its package wiring. This lets the new live guard pass when the contact page or stylesheet is absent, so reserve the network-tolerant skip for actual connection failures and count non-success HTTP responses as failures.

Useful? React with 👍 / 👎.

}
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)