Skip to content
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
27 changes: 19 additions & 8 deletions .github/workflows/live-site-check.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
name: Live Site Check

# Guards the deployed tinystudio.in against soft-404s and stale bundles
# 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.
# Guards the deployed tinystudio.in against soft-404s, WCAG 2.2 tap-target
# drift (SC 2.5.8 24px minimum), and stale bundles 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.
# Nightly: catch stale or broken deployments (soft-404s, sub-24px link
# targets, offline site) within a day even when no pull requests are open.
- cron: "23 3 * * *"
workflow_dispatch:

Expand All @@ -36,6 +37,16 @@ jobs:
cache: npm
- run: npm ci --ignore-scripts
- run: node scripts/check-public-live-soft-404.mjs
live-tap-targets:
runs-on: [self-hosted, linux, x64, vps-verify]
steps:
- uses: actions/checkout@v7

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm this job has no later Git operation that requires checkout credentials.
sed -n '40,60p' .github/workflows/live-site-check.yml
rg -n -C 2 '\bgit\b|actions/checkout' .github/workflows/live-site-check.yml

Repository: nish3451/tinystudio-in

Length of output: 796


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- workflow ---'
cat -n .github/workflows/live-site-check.yml

printf '%s\n' '--- checkout and Git references in workflows ---'
rg -n -C 3 'actions/checkout|persist-credentials|\bgit\b|GITHUB_TOKEN|github\.token' .github/workflows

Repository: nish3451/tinystudio-in

Length of output: 4485


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for file in scripts/check-public-live-soft-404.mjs scripts/check-public-live-tap-targets.mjs; do
  if [ -f "$file" ]; then
    printf '%s\n' "--- $file ---"
    rg -n -C 2 'child_process|exec|spawn|git|GITHUB_TOKEN|github\.token' "$file" || true
  fi
done

printf '%s\n' '--- package scripts and Git dependencies ---'
rg -n -C 2 '"(prepare|preinstall|install|postinstall|ci)"|git[[:space:]]' package.json package-lock.json 2>/dev/null || true

Repository: nish3451/tinystudio-in

Length of output: 3367


Disable checkout credential persistence.

This self-hosted job performs no Git operations after checkout. Set persist-credentials: false so the GITHUB_TOKEN is not retained in .git/config during later steps.

🧰 Tools
🪛 zizmor (1.29.0)

[warning] 43-43: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/live-site-check.yml at line 43, Update the
actions/checkout step to set persist-credentials to false, ensuring the
GITHUB_TOKEN is not retained in .git/config after checkout.

Source: Linters/SAST tools

- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
- run: npm ci --ignore-scripts
- run: node scripts/check-public-live-tap-targets.mjs
live-contact-heading-hierarchy:
runs-on: [self-hosted, linux, x64, vps-verify]
steps:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,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": "node scripts/check-public-live-soft-404.mjs",
"site:check-live": "node scripts/check-public-live-soft-404.mjs && node scripts/check-public-live-tap-targets.mjs",
"site:check-live-contact-heading-hierarchy": "node scripts/test-public-live-contact-heading-hierarchy.mjs",
"claims:check": "node scripts/check-outbound-claim-safety.mjs",
"config:check": "node scripts/check-agency-defaults.mjs",
Expand Down
142 changes: 142 additions & 0 deletions scripts/check-public-live-tap-targets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Guard the LIVE public site against WCAG 2.2 (SC 2.5.8) tap-target drift:
// the stylesheet tinystudio.in actually serves must keep every in-content
// and footer link rule at the 24px minimum.
//
// The static test (test-public-link-targets.mjs) only proves
// public/styles.css in the repo; it cannot catch a stale or misconfigured
// deployment. This check fetches the deployed stylesheet and re-asserts the
// same five link rules the local suite requires (.top-nav a, .plain-list a,
// .product-links a, .rail-item strong a, .footer-links a: block-level box,
// min-height 24px, >= 4px vertical padding), so a deployment that still
// serves pre-fix CSS (the June-20 bundle) fails loudly instead of silently
// re-opening the tap-target backlog item. It runs as part of
// `npm run site:check-live` and from the nightly live-site-check workflow.
//
// It is deliberately NOT part of `npm run test` / `npm run ci`: those
// blocking chains must stay green on repo state alone (see PR #84), while
// the live site is deployed by an external mechanism (Cloudflare Pages).
// Blocking CI on the live site would keep every pull request red whenever
// the deployment is stale.
//
// Failure semantics: a non-2xx HTTP response and a stylesheet that misses
// any required rule both FAIL this check (a deployment serving no rules is
// a failed deployment, not an unknown). Only a genuine network-level
// failure (site unreachable) skips with a notice, so offline machines do
// not go red; the nightly live-site-check workflow still catches the
// same outage via check-public-live-soft-404.mjs.
//
// Escape hatch for machines without network access:
// SKIP_LIVE_CHECKS=1 npm run site:check-live

import { readFileSync } from "node:fs"
import { fileURLToPath } from "node:url"
import { dirname, join } from "node:path"

if (process.env.SKIP_LIVE_CHECKS === "1") {
console.log("check-public-live-tap-targets: SKIP_LIVE_CHECKS=1, skipping live site checks")
process.exit(0)
}

const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..")
const read = (p) => readFileSync(join(ROOT, p), "utf8")

const LIVE_CSS_URL = "https://tinystudio.in/styles.css"
const FETCH_TIMEOUT_MS = 10_000
const FETCH_ATTEMPTS = 2

let failures = 0
let checks = 0
const ok = (cond, msg) => {
checks++
if (cond) console.log(` ok ${msg}`)
else {
failures++
console.error(` FAIL ${msg}`)
}
}

// Same selectors test-public-link-targets.mjs requires of the local sheet.
const TAP_TARGET_SELECTORS = [
".top-nav a",
".plain-list a",
".product-links a",
".rail-item strong a",
".footer-links a"
]

const targetRuleOf = (css, selector) => {
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
const pattern = selector
.split(",")
.map((s) => esc(s.trim()))
.join("\\s*,\\s*")
const match = css.match(new RegExp(`${pattern}\\s*\\{([^}]*)\\}`))
return match ? match[1] : null
Comment on lines +73 to +74

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 Evaluate the effective CSS rule instead of the first match

When a later or more-specific rule overrides one of these selectors—for example, a mobile media query sets .top-nav a { min-height: 0; padding: 0; }String.match still returns the first compliant block, so the nightly check reports success while the rendered tap target is below 24px. Since this detector is intended to catch deployed CSS drift, it should account for the cascade (ideally by checking computed styles at representative viewports) rather than accepting the first textual occurrence.

Useful? React with 👍 / 👎.

}

const assert24pxRule = (selector, rule) => {
ok(rule !== null, `live styles.css has a ${selector} rule`)
if (rule) {
ok(/display:\s*(inline-block|inline-flex|block)/.test(rule), `live ${selector} links are block-level boxes (hit area covers the line box)`)
ok(!/display:\s*inline\s*;/.test(rule), `live ${selector} links are not plain inline boxes`)
ok(/min-height:\s*24px/.test(rule), `live ${selector} links declare min-height: 24px`)

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 Check both target dimensions

Add a width assertion as well as min-height: when a short label or icon link is added under any of these selectors, the existing HTML coverage test still accepts it and this live check remains green even though the rendered target can be narrower than 24px because every required rule has zero horizontal padding and no min-width. The guard therefore does not enforce the stated 24-by-24 target minimum for all content it permits.

Useful? React with 👍 / 👎.

const paddingMatch = rule.match(/padding:\s*([^;]+)/)
ok(paddingMatch !== null, `live ${selector} links declare vertical padding`)
if (paddingMatch) {
const vertical = parseFloat(paddingMatch[1].trim().split(/\s+/)[0])
ok(vertical >= 4, `live ${selector} vertical padding is at least 4px (${vertical}px), so 16px text + padding >= 24px`)
Comment on lines +67 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Evaluate CSS rules after selector grouping and source-order overrides.

Line 73 only matches a selector directly before { and returns its first match. A grouped rule such as .top-nav a, .plain-list a { ... } does not match. A later rule for the same selector can override a compliant earlier rule and still pass this check.

Parse stylesheet rules, collect every matching selector, and evaluate the final effective declarations. Add cases for grouped selectors and later overrides.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-public-live-tap-targets.mjs` around lines 67 - 87, The
targetRuleOf/assert24pxRule validation currently only reads the first direct
selector match, missing grouped selectors and later source-order overrides.
Replace targetRuleOf with stylesheet rule parsing that collects declarations
from every rule containing the requested selector, then evaluate the final
effective declarations after applying source-order overrides; add coverage for
grouped selectors and later overriding rules.

}
}
}

console.log("check-public-live-tap-targets: the deployed tinystudio.in stylesheet keeps every link rule at the WCAG 2.2 24px minimum")

let css = null
try {
for (let attempt = 1; attempt <= FETCH_ATTEMPTS; attempt++) {
try {
const res = await fetch(LIVE_CSS_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) })
if (!res.ok) {
failures++
checks++
console.error(` FAIL ${LIVE_CSS_URL} answered HTTP ${res.status}: the deployed stylesheet is missing or inaccessible, so none of the tap-target rules are being served`)
process.exit(1)
}
css = await res.text()
break
} catch (err) {
if (attempt === FETCH_ATTEMPTS) throw err
await new Promise((r) => setTimeout(r, 1000))
}
}
} catch (err) {
console.log(` ok skipped: ${LIVE_CSS_URL} unreachable (${err?.cause?.code ?? err?.name ?? "network error"}) - no tap-target assertions run`)
}
Comment on lines +112 to +114

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 the stylesheet fetch exhausts its retries

When /styles.css alone times out, resets, or returns a truncated body while the HTML endpoints remain reachable, this catch path reports an ok skipped result and the job exits successfully after the wiring checks. The separate live-soft-404 job in .github/workflows/live-site-check.yml never requests this asset, so the nightly workflow can stay green even though browsers cannot load the stylesheet and the tap-target guarantees are absent. Treat an exhausted stylesheet fetch as a monitoring failure; offline callers already have the explicit SKIP_LIVE_CHECKS=1 escape hatch.

Useful? React with 👍 / 👎.


if (css !== null) {
console.log("A. every link rule in the deployed stylesheet enforces the 24px minimum")
for (const selector of TAP_TARGET_SELECTORS) {
assert24pxRule(selector, targetRuleOf(css, selector))
}
if (failures === 0) {
console.log(" the deployed stylesheet carries every tap-target rule from public/styles.css")
} else {
console.log(" the deployed stylesheet is stale: it misses tap-target rules that public/styles.css already has (see FAIL lines above). Refresh the live deployment from origin/main.")
}
}

console.log("B. wiring")
const pkg = JSON.parse(read("package.json"))
ok(
(pkg.scripts["site:check-live"] ?? "").includes("check-public-live-soft-404.mjs") &&
(pkg.scripts["site:check-live"] ?? "").includes("check-public-live-tap-targets.mjs"),
"npm run site:check-live runs the soft-404 and tap-target live checks"
)
const workflow = read(".github/workflows/live-site-check.yml")
ok(workflow.includes("check-public-live-tap-targets.mjs"), "live-site-check.yml runs the live tap-target check (nightly + manual dispatch)")

console.log(`\n${checks} checks, ${failures} failures`)
if (failures > 0) {
console.error("\nThe deployed stylesheet drifted below the WCAG 2.2 24px tap-target minimum. Re-deploy the public site from origin/main and re-run this check.")
}
process.exit(failures === 0 ? 0 : 1)