Skip to content

fix(public): land the tinystudio.in release lane so the live site can leave the June-20 bundle - #81

Merged
nish3451 merged 3 commits into
mainfrom
fix/lane1-social-share-deploy-path
Aug 11, 2026
Merged

fix(public): land the tinystudio.in release lane so the live site can leave the June-20 bundle#81
nish3451 merged 3 commits into
mainfrom
fix/lane1-social-share-deploy-path

Conversation

@nish3451

@nish3451 nish3451 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Why

Dogfood item 82771ab0cb63 (social share image incomplete on /promptly/privacy) remains open because the live site still serves the 2026-06-20 bundle (07acd07): the repo-side og:image/twitter:image fix merged via #26/#27 months ago, but the Cloudflare Pages GitHub integration for tiny-studio-3f5 has not deployed since. npm test proves the source is fixed (test-public-social-preview 137/137); curl https://tinystudio.in/promptly/privacy/ proves the live page still has zero og:image/twitter:image meta tags.

What this PR does

Lands the release lane that repairs the deploy path (cherry-picked from fix/lane1-public-release-lane, which was never opened as a PR):

  • scripts/prepare-public-deploy-bundle.mjs — filtered deploy bundle from public/ (snoozed managed-service buyer path from feat(public): route founders to reviewed Website Correction #10/feat(measurement): add Website Correction conversion signal #11 removed; every other merged fix preserved), fail-closed in both directions
  • scripts/test-public-deploy-bundle.mjs — 62-check regression guard, wired into npm test/npm run ci
  • scripts/publish-public-site.mjs — prepare → wrangler pages deploy to tiny-studio-3f5 → live verification; passes source commit for dashboard provenance
  • scripts/check-public-live-deploy.mjs — live proof for the deploy-path accept
  • .github/workflows/deploy-public-site.yml — runs on push to main (and workflow_dispatch), fail-closed with a printed provisioning guide when CLOUDFLARE_API_TOKEN/CLOUDFLARE_ACCOUNT_ID secrets are absent

Credential step (Nish-only, ~2 minutes, documented in the workflow and script)

The fleet Workers token (fleet-console/cf.env) does not have Cloudflare Pages:Edit — verified live: GET /accounts/f670a698e17bf160c8e4679823e68916/pages/projects → 403 Authentication error. Until provisioned, the workflow fails closed on every main push, which is the correct behavior: the stale deploy must not pass silently.

  1. Cloudflare dashboard → Profile → API Tokens → Create Token → "Cloudflare Pages: Edit" template, scoped to account f670a698e17bf160c8e4679823e68916
  2. gh secret set CLOUDFLARE_API_TOKEN -R nish3451/tinystudio-in
  3. gh secret set CLOUDFLARE_ACCOUNT_ID -R nish3451/tinystudio-in -b f670a698e17bf160c8e4679823e68916
  4. Merge this PR (or rerun the workflow) → live site deploys the fixed bundle → dogfood item closes

Verification

  • npm test exit 0 (includes test-public-social-preview 137/137 and test-public-deploy-bundle 62/62)
  • node scripts/publish-public-site.mjs --prepare-only produces a valid bundle
  • git diff --check clean
  • Live state confirming the gap: curl -s https://tinystudio.in/promptly/privacy/ | grep -c og:image → 0

Summary by CodeRabbit

  • New Features

    • Added automated publishing for the public site, including manual and main-branch deployments.
    • Added commands to prepare, validate, and publish deployment bundles.
    • Added live-site checks for key pages, structured data, branding, and 404 behavior.
  • Bug Fixes

    • Prevented snoozed managed-service content from appearing in public deployments.
    • Added validation to ensure required public-site content remains intact.
  • Tests

    • Added deployment-bundle integration checks to standard test and CI workflows.

nish3451 and others added 2 commits August 11, 2026 16:01
…loy path repair)

The live site has served the 2026-06-20 bundle (07acd07) since June 20 while
17+ public PRs merged to main; the Cloudflare Pages git connection for
tiny-studio-3f5 never shows checks/statuses on commits and no deploy
workflow or secrets exist in the repo. The fleet Workers token lacks
Cloudflare Pages:Edit, so no automation on this box can publish today.

Add an in-repo release lane that works the moment a Pages-scoped token is
provisioned (documented fail-closed message):
- scripts/prepare-public-deploy-bundle.mjs: filtered bundle (public/ minus
  the snoozed-by-Nish managed-service buyer path from PRs #10/#11; every
  other merged fix preserved), fail-closed in both directions
- scripts/test-public-deploy-bundle.mjs: regression guard, wired into
  npm test/ci (62 checks)
- scripts/publish-public-site.mjs: prepare -> wrangler pages deploy to
  tiny-studio-3f5 -> live verification
- scripts/check-public-live-deploy.mjs: live proof for the deploy-path
  accept (H2-after-H1 /promptly/support/ #18/#20, JSON-LD /contact/ #19,
  real 404s #34, homepage portfolio-only #29 + no buyer path)
- .github/workflows/deploy-public-site.yml: vps-verify lane on push to main
- wrangler 4.120.0 devDependency

verify: npm test (603 checks, 0 failures); node scripts/prepare-public-deploy-bundle.mjs; git diff --check
wrangler pages deploy accepts --commit-hash/--commit-message/--commit-dirty
for dashboard provenance. The bundle already records source_commit in
deploy-manifest.json; pass it through so the Pages dashboard links the
deployment to the exact tinystudio-in commit that produced the bundle.

verify: node --check scripts/publish-public-site.mjs; node scripts/test-public-deploy-bundle.mjs (62 checks, 0 failures)

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a filtered public-site bundle pipeline. It validates content, publishes through Cloudflare Pages, verifies the live site, and runs through GitHub Actions on main pushes or manual dispatch.

Changes

Public site deployment

Layer / File(s) Summary
Filtered bundle preparation
scripts/prepare-public-deploy-bundle.mjs
Copies public/, removes managed-service buyer-path content, validates required markers and files, and writes deploy-manifest.json.
Bundle integration validation
package.json, scripts/test-public-deploy-bundle.mjs
Adds bundle validation to ci and test, plus an integration test for filtering, file completeness, page structure, and command wiring.
Live deployment checks
scripts/check-public-live-deploy.mjs
Checks live page headings, contact-page JSON-LD, 404 behavior, homepage branding, and forbidden buyer-path content.
Cloudflare release workflow
scripts/publish-public-site.mjs, .github/workflows/deploy-public-site.yml
Prepares and deploys bundles with Wrangler, runs live checks, and configures automated deployment on main pushes or manual dispatch.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant PublishPublicSite
  participant BundlePreparation
  participant CloudflarePages
  participant LiveChecker
  GitHubActions->>PublishPublicSite: start public-site release
  PublishPublicSite->>BundlePreparation: create and validate bundle
  PublishPublicSite->>CloudflarePages: deploy bundle with Wrangler
  PublishPublicSite->>LiveChecker: verify tinystudio.in
  LiveChecker-->>PublishPublicSite: return check status
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the new public release lane and its purpose of updating the stale live-site bundle.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/lane1-social-share-deploy-path

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 531569a171

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 20

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Run Wrangler on its required Node version

This workflow installs Node 20 before running the newly pinned Wrangler 4.120.0, but package-lock.json declares that exact Wrangler version with engines.node: ">=22.0.0" (lines 1458–1480). The production deployment therefore runs on an unsupported runtime and can fail during installation or CLI startup before publishing; configure setup-node for Node 22 or newer.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (5)
scripts/test-public-deploy-bundle.mjs (2)

106-109: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert the manifest contents, not only its presence.

The test checks that deploy-manifest.json exists. It does not check the fields that the publish lane depends on. Assert filter_version === SNOOZE_FILTER_VERSION and that source_commit is a 40-character hex string, so a "unknown" commit or a stale filter version fails the test instead of shipping.

♻️ Proposed addition
 for (const rel of requiredFiles) {
   ok(existsSync(join(bundleDir, rel)), `${rel} exists in the bundle`)
 }
+
+const manifest = JSON.parse(readFileSync(join(bundleDir, "deploy-manifest.json"), "utf8"))
+ok(manifest.filter_version === SNOOZE_FILTER_VERSION, "manifest records the current filter version")
+ok(/^[0-9a-f]{40}$/.test(manifest.source_commit), `manifest records a real source commit (got ${manifest.source_commit})`)

Add the import:

 import {
   FORBIDDEN_MARKERS,
   NEUTRAL_PROOFS,
+  SNOOZE_FILTER_VERSION,
   preparePublicDeployBundle,
 } from "./prepare-public-deploy-bundle.mjs"
🤖 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/test-public-deploy-bundle.mjs` around lines 106 - 109, Extend the
deploy bundle validation around requiredFiles to read and parse
deploy-manifest.json, then assert that filter_version equals
SNOOZE_FILTER_VERSION and source_commit matches a 40-character hexadecimal
string. Keep the existing presence checks and add the necessary manifest-reading
import.

136-139: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the temp directory even when a check throws.

rmSync runs only on the success path. Any exception between Line 61 and Line 135 leaks the temp directory. Register the cleanup with process.on("exit", ...) right after mkdtempSync, so it runs on every exit path.

🤖 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/test-public-deploy-bundle.mjs` around lines 136 - 139, Register an
exit handler immediately after the temporary directory is created by mkdtempSync
in the test script, and move the rmSync(bundleDir, { recursive: true, force:
true }) cleanup into that handler. Remove the success-path-only cleanup so the
temporary directory is removed for both normal completion and exceptions.
scripts/prepare-public-deploy-bundle.mjs (2)

310-317: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

file_count counts directories, and omits the manifest.

fs.readdir(output, { recursive: true }) returns directory entries as well as files. file_count therefore reports more than the file total. The manifest is written after the count, so it is never included. The CLI prints this value at Line 341 as files. Filter on file entries if the number is meant to be a file count.

♻️ Proposed fix
-    file_count: (await fs.readdir(output, { recursive: true })).length,
+    file_count: (await fs.readdir(output, { recursive: true, withFileTypes: true })).filter((e) =>
+      e.isFile()
+    ).length,
🤖 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/prepare-public-deploy-bundle.mjs` around lines 310 - 317, Update the
manifest construction around `file_count` to count only regular file entries
returned by `fs.readdir`, excluding directories and other non-file entries.
Preserve the existing manifest-writing flow, while ensuring the CLI’s `files`
value reflects the intended file total and does not include the manifest written
afterward.

321-337: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Absolute --source and --output values break path resolution.

Line 335 always calls join(ROOT, args.source). If a user passes an absolute source path, join appends it to ROOT and the read fails. --output is passed unjoined at Line 336, so the two flags resolve differently. Use resolve(ROOT, value) for both so absolute and relative inputs behave the same.

♻️ Proposed fix
   const args = parseArgs(process.argv)
-  const outputDir = args.output || join(tmpdir(), `tinystudio-deploy-bundle-${Date.now()}`)
+  const outputDir = args.output
+    ? resolve(ROOT, args.output)
+    : join(tmpdir(), `tinystudio-deploy-bundle-${Date.now()}`)
   try {
     const out = await preparePublicDeployBundle({
-      sourceDir: join(ROOT, args.source),
+      sourceDir: resolve(ROOT, args.source),
       outputDir,
     })
🤖 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/prepare-public-deploy-bundle.mjs` around lines 321 - 337, Update the
argument handling around parseArgs and the preparePublicDeployBundle call so
both args.source and args.output are normalized with resolve(ROOT, value).
Preserve absolute inputs unchanged while resolving relative inputs against ROOT,
and pass the resolved paths consistently as sourceDir and outputDir.
package.json (1)

88-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Collapse the duplicated ci and test command chains.

Lines 88 and 90 hold the same command string. Every new check must be added twice, and the two lists will drift. Line 89 already delegates check to npm test. Do the same for ci.

♻️ Proposed fix
-    "ci": "node scripts/test-service-engine.mjs && ... && node scripts/test-public-deploy-bundle.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check",
+    "ci": "npm test",
🤖 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 `@package.json` around lines 88 - 90, Update the package scripts so the ci
script delegates to npm test, matching the existing check script, instead of
duplicating the full command chain. Leave the test script as the single source
of truth for the listed checks.
🤖 Prompt for all review comments with 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.

Inline comments:
In @.github/workflows/deploy-public-site.yml:
- Around line 20-24: Update the workflow triggers around workflow_dispatch so
manually selected branches cannot execute production deployment commands with
CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID; remove workflow_dispatch, or
require a protected production environment restricted to main while preserving
push deployments from main.

In `@scripts/check-public-live-deploy.mjs`:
- Around line 52-99: Update the live-check entrypoint to read the expected
source_commit from deploy-manifest.json and pass it into the checker, then fetch
the live deployment manifest and compare its commit before running the page
assertions in the try block. Fail the check on a missing or mismatched live
manifest so prior releases cannot satisfy the existing checks.
- Around line 38-40: Update the get function to pass AbortSignal.timeout(30_000)
in the fetch options for every live request, and catch timeout failures so the
resulting error message includes the requested path before propagating it.

In `@scripts/prepare-public-deploy-bundle.mjs`:
- Around line 179-186: Update the article-block validation around the end scan
in the deployment bundle processing flow so it checks that end remains within
lines before accessing lines[end]. Preserve the existing “article block not
balanced” error path when no closing </article> marker is found, avoiding an
out-of-bounds access.
- Around line 289-294: Guard the resolved output path in
preparePublicDeployBundle before fs.rm: reject output paths that equal ROOT or
are located within ROOT, including equivalent normalized paths, and fail before
deleting anything. Preserve normal behavior for safe output directories.
- Around line 255-268: Update assertForbiddenAbsent to recursively walk every
file under bundleDir instead of iterating only REQUIRED_FILES, inspect text/HTML
files while skipping binary files, and apply all FORBIDDEN_MARKERS to their
contents. Build paths using a Node-compatible directory-entry fallback rather
than relying exclusively on Dirent.parentPath, while preserving the existing
failure messages and return behavior.

In `@scripts/publish-public-site.mjs`:
- Around line 132-136: Update the --deploy flow around deployWithWrangler to
validate the supplied bundle using an exported validator from
preparePublicDeployBundle before uploading. Reuse the preparer’s filtering and
neutral-proof validation logic, while retaining the existing bundle argument and
deploy-manifest checks.
- Around line 73-75: Update the CLOUDFLARE_ACCOUNT_ID validation in the publish
script to reject any value that differs from PAGES_ACCOUNT_ID, not just missing
or empty values. Perform this check before Wrangler runs and retain the existing
error-handling context for invalid account configuration.

In `@scripts/test-public-deploy-bundle.mjs`:
- Around line 58-73: Update the bundle preparation catch block around
preparePublicDeployBundle to stop execution immediately after recording the
failure, before iterating over bundleFiles or reading files from bundleDir.
Ensure the failure path does not continue with an empty or incomplete bundle
directory.
- Line 128: Update the assertion in the public deploy bundle test to detect the
script tag name regardless of attributes, replacing the exact "<script>"
substring check while preserving the expectation that the measurement-marker
script is absent.

---

Nitpick comments:
In `@package.json`:
- Around line 88-90: Update the package scripts so the ci script delegates to
npm test, matching the existing check script, instead of duplicating the full
command chain. Leave the test script as the single source of truth for the
listed checks.

In `@scripts/prepare-public-deploy-bundle.mjs`:
- Around line 310-317: Update the manifest construction around `file_count` to
count only regular file entries returned by `fs.readdir`, excluding directories
and other non-file entries. Preserve the existing manifest-writing flow, while
ensuring the CLI’s `files` value reflects the intended file total and does not
include the manifest written afterward.
- Around line 321-337: Update the argument handling around parseArgs and the
preparePublicDeployBundle call so both args.source and args.output are
normalized with resolve(ROOT, value). Preserve absolute inputs unchanged while
resolving relative inputs against ROOT, and pass the resolved paths consistently
as sourceDir and outputDir.

In `@scripts/test-public-deploy-bundle.mjs`:
- Around line 106-109: Extend the deploy bundle validation around requiredFiles
to read and parse deploy-manifest.json, then assert that filter_version equals
SNOOZE_FILTER_VERSION and source_commit matches a 40-character hexadecimal
string. Keep the existing presence checks and add the necessary manifest-reading
import.
- Around line 136-139: Register an exit handler immediately after the temporary
directory is created by mkdtempSync in the test script, and move the
rmSync(bundleDir, { recursive: true, force: true }) cleanup into that handler.
Remove the success-path-only cleanup so the temporary directory is removed for
both normal completion and exceptions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e0d4c2b-5f18-40b8-887f-adcfe2efbd5a

📥 Commits

Reviewing files that changed from the base of the PR and between 52d4da4 and 531569a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • .github/workflows/deploy-public-site.yml
  • package.json
  • scripts/check-public-live-deploy.mjs
  • scripts/prepare-public-deploy-bundle.mjs
  • scripts/publish-public-site.mjs
  • scripts/test-public-deploy-bundle.mjs

Comment on lines +20 to +24
on:
push:
branches:
- main
workflow_dispatch:

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 | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Can GitHub Actions workflow_dispatch run a workflow from a selected branch, and does the run use that ref's workflow definition?

💡 Result:

Yes, GitHub Actions workflow_dispatch can run a workflow from a selected branch, and it uses the workflow definition from that specific ref (branch or tag) [1][2]. Key Technical Details: 1. Registration Requirement: For a workflow to be discoverable and triggerable via the GitHub UI "Run workflow" button, the workflow file must exist on the repository's default branch [1][2][3]. If the file does not exist on the default branch, the workflow will not appear in the Actions UI, and it cannot be triggered manually through the browser [2][3]. 2. Executing Non-Default Branches: Once a workflow is registered (by having the file present on the default branch), you can use the GitHub UI to select and run that workflow from any other branch or tag [1][4]. The run will execute using the specific workflow file definition found on that selected ref [1][2]. 3. Alternative Methods: If a workflow file is only present on a feature branch (and not on the default branch), it is not registered in the GitHub Actions UI [2][5]. However, you may still be able to trigger it using the GitHub CLI (gh) or REST API if the workflow has been triggered at least once previously or by using other triggers (like push or pull_request) to temporarily "register" the file [6][2]. Once the workflow is known to the system, you can use the gh workflow run --ref command to execute the definition located on that branch [1][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/deploy-public-site.yml
printf '%s\n' '--- related workflow and environment references ---'
rg -n --hidden -S 'deploy-public-site|vps-verify|CLOUDFLARE|environment:|workflow_dispatch|ref:' .github README.md 2>/dev/null || true
printf '%s\n' '--- tracked workflow files ---'
git ls-files .github/workflows

Repository: nish3451/tinystudio-in

Length of output: 3805


Do not expose production credentials to workflow_dispatch runs.

workflow_dispatch can run the workflow definition from a selected branch. That branch controls the checkout and the commands that run with CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID. Remove workflow_dispatch, or attach the secrets to a protected production environment that allows deployments only from main.

🤖 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/deploy-public-site.yml around lines 20 - 24, Update the
workflow triggers around workflow_dispatch so manually selected branches cannot
execute production deployment commands with CLOUDFLARE_API_TOKEN and
CLOUDFLARE_ACCOUNT_ID; remove workflow_dispatch, or require a protected
production environment restricted to main while preserving push deployments from
main.

Comment on lines +38 to +40
const get = async (path, { redirect = "manual" } = {}) => {
const res = await fetch(`${BASE}${path}`, { redirect })
return { status: res.status, body: await res.text() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Node.js 20 support AbortSignal.timeout() as the signal passed to global fetch()?

💡 Result:

Yes, Node.js 20 supports using AbortSignal.timeout as the signal passed to the global fetch function [1][2][3]. AbortSignal.timeout was added to Node.js in versions v17.3.0 and v16.14.0 [1][4]. Since Node.js 20 includes this method globally, you can pass it directly to the signal option of the global fetch function to automatically abort requests after a specified duration [1][3]. This is a standard and recommended practice for managing timeouts in Node.js applications [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

file="scripts/check-public-live-deploy.mjs"
printf '%s\n' '--- target file ---'
cat -n "$file"

printf '%s\n' '--- runtime and project baseline references ---'
rg -n --hidden -S 'engines|node-version|NODE_VERSION|Node 20|node20|AbortSignal\.timeout|check-public-live-deploy|fetch\(' \
  package.json .nvmrc .node-version .github scripts 2>/dev/null || true

Repository: nish3451/tinystudio-in

Length of output: 5732


Set a deadline for each live request.

Pass AbortSignal.timeout(30_000) to fetch(). Include the request path in timeout error messages.

🤖 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-deploy.mjs` around lines 38 - 40, Update the get
function to pass AbortSignal.timeout(30_000) in the fetch options for every live
request, and catch timeout failures so the resulting error message includes the
requested path before propagating it.

Comment on lines +52 to +99
console.log("check-public-live-deploy: live tinystudio.in must match neutral merged fixes")

if (process.env.SKIP_LIVE_CHECKS === "1") {
console.log(" SKIP_LIVE_CHECKS=1 - live checks skipped")
process.exit(0)
}

try {
console.log("A. /promptly/support/ renders the fixed heading hierarchy (PRs #18/#20)")
{
const { status, body } = await get("/promptly/support/")
ok(status === 200, `/promptly/support/ returns 200 (got ${status})`)
ok(h2AfterFirstH1(body), "H2 follows the first H1 before any H3")
}

console.log("B. /contact/ carries structured data (PR #19)")
{
const { status, body } = await get("/contact/")
ok(status === 200, `/contact/ returns 200 (got ${status})`)
ok(body.includes("application/ld+json"), "page contains application/ld+json")
}

console.log("C. unknown URLs return a real 404 (PR #34)")
{
const probe = `/definitely-missing-live-deploy-${Date.now()}`
const { status, body } = await get(probe)
ok(status === 404, `unknown URL returns 404 (got ${status})`)
ok(!body.includes("<title>Tiny Studio | Promptly, Drishti, and 0509"), "the 404 body is not the homepage")
}

console.log("D. homepage is portfolio-only (snooze honored, #29 live)")
{
const { status, body } = await get("/")
ok(status === 200, `homepage returns 200 (got ${status})`)
ok(body.includes("<title>Tiny Studio | Promptly, Drishti, and 0509"), "portfolio title is live")
ok(body.includes('"alternateName"'), "brand-disambiguation JSON-LD is live (PR #29)")
ok(body.includes("not affiliated"), "non-affiliation copy is live (PR #29)")
for (const marker of BUYER_PATH_MARKERS) {
ok(!body.includes(marker), `homepage has no ${marker}`)
}
}
} catch (error) {
failures++
console.error(` FAIL live request error: ${error.message}`)
}

console.log(`\n${checks} checks, ${failures} failures`)
process.exit(failures === 0 ? 0 : 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Bind live checks to the uploaded bundle.

The checker receives neither a deployment ID nor the prepared bundle source_commit. A previous release with the same page properties can pass every check, so the release result does not prove that this upload serves tinystudio.in. Pass the expected commit from deploy-manifest.json, then fetch and compare the live manifest before the page assertions.

🤖 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-deploy.mjs` around lines 52 - 99, Update the
live-check entrypoint to read the expected source_commit from
deploy-manifest.json and pass it into the checker, then fetch the live
deployment manifest and compare its commit before running the page assertions in
the try block. Fail the check on a missing or mismatched live manifest so prior
releases cannot satisfy the existing checks.

Comment on lines +179 to +186
let start = markerIdx
while (start > 0 && !lines[start].includes("<article")) start--
let end = markerIdx
while (end < lines.length && !lines[end].includes("</article>")) end++
if (!lines[start].includes("<article") || !lines[end].includes("</article>")) {
errors.push(`article block not balanced around ${op.marker} in ${op.file}`)
continue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the </article> scan against running off the end of the file.

If no line contains </article>, the loop at Line 182 exits with end === lines.length. Line 183 then evaluates lines[end].includes("</article>") on undefined and throws a TypeError. The filter reports a stack trace instead of the intended "article block not balanced" error, so the fail-closed message is lost.

🐛 Proposed fix for the unbounded scan
       let start = markerIdx
       while (start > 0 && !lines[start].includes("<article")) start--
       let end = markerIdx
       while (end < lines.length && !lines[end].includes("</article>")) end++
-      if (!lines[start].includes("<article") || !lines[end].includes("</article>")) {
+      if (
+        end >= lines.length ||
+        !lines[start].includes("<article") ||
+        !lines[end].includes("</article>")
+      ) {
         errors.push(`article block not balanced around ${op.marker} in ${op.file}`)
         continue
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let start = markerIdx
while (start > 0 && !lines[start].includes("<article")) start--
let end = markerIdx
while (end < lines.length && !lines[end].includes("</article>")) end++
if (!lines[start].includes("<article") || !lines[end].includes("</article>")) {
errors.push(`article block not balanced around ${op.marker} in ${op.file}`)
continue
}
let start = markerIdx
while (start > 0 && !lines[start].includes("<article")) start--
let end = markerIdx
while (end < lines.length && !lines[end].includes("</article>")) end++
if (
end >= lines.length ||
!lines[start].includes("<article") ||
!lines[end].includes("</article>")
) {
errors.push(`article block not balanced around ${op.marker} in ${op.file}`)
continue
}
🤖 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/prepare-public-deploy-bundle.mjs` around lines 179 - 186, Update the
article-block validation around the end scan in the deployment bundle processing
flow so it checks that end remains within lines before accessing lines[end].
Preserve the existing “article block not balanced” error path when no closing
</article> marker is found, avoiding an out-of-bounds access.

Comment on lines +255 to +268
const assertForbiddenAbsent = (bundleDir) => {
const failures = []
for (const rel of REQUIRED_FILES) {
const p = join(bundleDir, rel)
if (!existsSync(p)) continue
const html = readFileSync(p, "utf8")
for (const marker of FORBIDDEN_MARKERS) {
if (marker.test(html)) {
failures.push(`${rel} still contains ${marker.label}`)
}
}
}
return failures
}

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Node.js fs.readdir recursive withFileTypes Dirent parentPath vs path availability Node 20

💡 Result:

In Node.js 20, the fs.Dirent object property path is deprecated (via DEP0178) and has been replaced by parentPath to clarify that it contains the path to the parent directory of the entry, not the full path of the entry itself [1][2][3]. Key points regarding these properties in Node.js 20: 1. Deprecation: The dirent.path property is deprecated [1][4]. You should migrate to dirent.parentPath in all new projects and existing codebases [1][3]. 2. Availability: dirent.parentPath was introduced to the fs.Dirent class in Node.js 18.20.0 and Node.js 20.12.0 [5]. If you are using an older version of Node.js 20 (e.g., prior to 20.12.0), parentPath will not be available [6]. 3. Semantic Difference: The name parentPath was chosen because the previous name, path, was ambiguous and often incorrectly interpreted as the absolute path to the file [1][7][3]. The value of parentPath is the path to the directory containing the file [1][3]. 4. Constructing Full Paths: To obtain the full path of an entry, you should combine dirent.parentPath and dirent.name using the path.join() utility [1][3]: const fullPath = path.join(dirent.parentPath, dirent.name); When using fs.readdir with { recursive: true, withFileTypes: true }, each Dirent object returned will contain this parentPath property, correctly reflecting the directory structure being traversed [1][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- package/runtime declarations ---'
rg -n --glob 'package.json' --glob '.nvmrc' --glob '.node-version' --glob 'Dockerfile*' '"engines"|"node"|20\.' . 2>/dev/null | head -80
printf '%s\n' '--- script structure and relevant symbols ---'
ast-grep outline scripts/prepare-public-deploy-bundle.mjs --match 'assertForbiddenAbsent' --view expanded 2>/dev/null || true
rg -n -A12 -B8 'REQUIRED_FILES|FORBIDDEN_MARKERS|assertForbiddenAbsent|rm\(output|--source|--output|file_count|article-block|section-block' scripts/prepare-public-deploy-bundle.mjs
printf '%s\n' '--- bundle file references ---'
rg -n 'public/|REQUIRED_FILES|deploy-manifest|forbidden|fail-closed' scripts/prepare-public-deploy-bundle.mjs README.md package.json 2>/dev/null | head -160
printf '%s\n' '--- tracked public files (sample) ---'
git ls-files 'public/**' | head -120

Repository: nish3451/tinystudio-in

Length of output: 12975


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re

root = Path("public")
required = {
    "index.html", "404.html", "styles.css", "favicon.svg",
    "apple-touch-icon.svg", "robots.txt", "sitemap.xml", "llms.txt",
    "_headers", "contact/index.html", "support/index.html",
    "privacy/index.html",
}
files = [p for p in root.rglob("*") if p.is_file()]
markers = [
    ("The Website Correction", lambda s: "Website Correction" in s),
    ("website-correction ids/hrefs", lambda s: "website-correction" in s),
    ("data-measure-source", lambda s: "data-measure-source" in s),
    ("managed service phrase", lambda s: re.search(r"managed\s+service", s, re.I)),
]
print("unlisted text files with forbidden markers:")
for p in files:
    rel = p.relative_to(root).as_posix()
    if rel in required:
        continue
    try:
        text = p.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        continue
    hits = [name for name, test in markers if test(text)]
    if hits:
        print(rel, hits)
print("binary files among REQUIRED_FILES:")
for rel in sorted(required):
    p = root / rel
    if p.exists():
        try:
            p.read_text(encoding="utf-8")
        except UnicodeDecodeError:
            print(rel)
print("all unlisted files:", len([p for p in files if p.relative_to(root).as_posix() not in required]))
PY
printf '%s\n' '--- runtime ---'
node --version 2>/dev/null || true
node - <<'JS'
const fs = require("node:fs");
(async () => {
  const entries = await fs.promises.readdir("public", { recursive: true, withFileTypes: true });
  const sample = entries.find((entry) => entry.name === "index.html");
  console.log(JSON.stringify({
    hasParentPath: sample && Object.prototype.hasOwnProperty.call(sample, "parentPath"),
    parentPath: sample && sample.parentPath,
    hasPath: sample && Object.prototype.hasOwnProperty.call(sample, "path"),
    path: sample && sample.path,
    count: entries.length,
  }));
})().catch((error) => {
  console.error(error.message);
  process.exitCode = 1;
});
JS

Repository: nish3451/tinystudio-in

Length of output: 359


Scan all bundle text files for forbidden markers.

assertForbiddenAbsent checks only REQUIRED_FILES. An unlisted HTML or text file can bypass the fail-closed post-filter contract. Walk the bundle recursively and inspect text files only. Use Dirent.parentPath only when Node.js 20.12.0+ is required; otherwise provide a compatible fallback.

🤖 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/prepare-public-deploy-bundle.mjs` around lines 255 - 268, Update
assertForbiddenAbsent to recursively walk every file under bundleDir instead of
iterating only REQUIRED_FILES, inspect text/HTML files while skipping binary
files, and apply all FORBIDDEN_MARKERS to their contents. Build paths using a
Node-compatible directory-entry fallback rather than relying exclusively on
Dirent.parentPath, while preserving the existing failure messages and return
behavior.

Comment on lines +289 to +294
export const preparePublicDeployBundle = async ({ sourceDir, outputDir }) => {
const source = resolve(sourceDir)
const output = resolve(outputDir)
await fs.rm(output, { recursive: true, force: true })
await fs.mkdir(output, { recursive: true })
await fs.cp(source, output, { recursive: true })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a guard before the recursive delete of output.

Line 292 removes the resolved output directory recursively with force: true. The CLI accepts any --output value. A call such as --output . or --output .. deletes the working tree without confirmation. Reject an output path that equals or contains ROOT, or require the directory to be empty or non-existent.

🛡️ Proposed guard
 export const preparePublicDeployBundle = async ({ sourceDir, outputDir }) => {
   const source = resolve(sourceDir)
   const output = resolve(outputDir)
+  const rootRel = relative(output, ROOT)
+  if (output === ROOT || (rootRel !== "" && !rootRel.startsWith("..") && !isAbsolute(rootRel))) {
+    throw new Error(`refusing to wipe ${output}: output must not contain the repository root`)
+  }
+  if (output === source) {
+    throw new Error(`refusing to wipe ${output}: output must differ from source`)
+  }
   await fs.rm(output, { recursive: true, force: true })
🤖 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/prepare-public-deploy-bundle.mjs` around lines 289 - 294, Guard the
resolved output path in preparePublicDeployBundle before fs.rm: reject output
paths that equal ROOT or are located within ROOT, including equivalent
normalized paths, and fail before deleting anything. Preserve normal behavior
for safe output directories.

Comment on lines +73 to +75
if (!process.env.CLOUDFLARE_ACCOUNT_ID) {
throw new Error(`Missing CLOUDFLARE_ACCOUNT_ID (set it to ${PAGES_ACCOUNT_ID}).${PROVISION_MESSAGE}`)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject an account ID that does not match the release contract.

The current check accepts every non-empty CLOUDFLARE_ACCOUNT_ID. Reject a value other than PAGES_ACCOUNT_ID before Wrangler runs. This prevents a credential configuration error from uploading to another account.

Proposed fix
-  if (!process.env.CLOUDFLARE_ACCOUNT_ID) {
-    throw new Error(`Missing CLOUDFLARE_ACCOUNT_ID (set it to ${PAGES_ACCOUNT_ID}).${PROVISION_MESSAGE}`)
+  if (process.env.CLOUDFLARE_ACCOUNT_ID !== PAGES_ACCOUNT_ID) {
+    throw new Error(`CLOUDFLARE_ACCOUNT_ID must be ${PAGES_ACCOUNT_ID}.${PROVISION_MESSAGE}`)
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!process.env.CLOUDFLARE_ACCOUNT_ID) {
throw new Error(`Missing CLOUDFLARE_ACCOUNT_ID (set it to ${PAGES_ACCOUNT_ID}).${PROVISION_MESSAGE}`)
}
if (process.env.CLOUDFLARE_ACCOUNT_ID !== PAGES_ACCOUNT_ID) {
throw new Error(`CLOUDFLARE_ACCOUNT_ID must be ${PAGES_ACCOUNT_ID}.${PROVISION_MESSAGE}`)
}
🤖 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/publish-public-site.mjs` around lines 73 - 75, Update the
CLOUDFLARE_ACCOUNT_ID validation in the publish script to reject any value that
differs from PAGES_ACCOUNT_ID, not just missing or empty values. Perform this
check before Wrangler runs and retain the existing error-handling context for
invalid account configuration.

Comment on lines +132 to +136
if (!args.bundle) throw new Error("--deploy requires --bundle <dir>")
if (!existsSync(join(args.bundle, "deploy-manifest.json"))) {
throw new Error(`not a deploy bundle (missing deploy-manifest.json): ${args.bundle}`)
}
await deployWithWrangler(args.bundle)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the supplied bundle before upload.

--deploy accepts any directory that contains a file named deploy-manifest.json. It does not run the filtering and neutral-proof validation that preparePublicDeployBundle() performs. Export a bundle validator from the preparer and call it here before deployWithWrangler().

🤖 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/publish-public-site.mjs` around lines 132 - 136, Update the --deploy
flow around deployWithWrangler to validate the supplied bundle using an exported
validator from preparePublicDeployBundle before uploading. Reuse the preparer’s
filtering and neutral-proof validation logic, while retaining the existing
bundle argument and deploy-manifest checks.

Comment on lines +58 to +73
let bundleDir = ""
try {
bundleDir = mkdtempSync(join(tmpdir(), "tinystudio-bundle-test-"))
await preparePublicDeployBundle({ sourceDir: join(ROOT, "public"), outputDir: bundleDir })
ok(true, "bundle prepared in a temp directory")
} catch (error) {
ok(false, `bundle preparation failed: ${error.message}`)
}

const bundleFiles = ["index.html", "contact/index.html"]
for (const rel of bundleFiles) {
const html = readFileSync(join(bundleDir, rel), "utf8")
for (const marker of FORBIDDEN_MARKERS) {
ok(!marker.test(html), `${rel} has no ${marker.label}`)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Stop the run when bundle preparation fails.

The catch block at Line 63 records the failure and execution continues. Line 69 then calls readFileSync(join(bundleDir, rel)) and throws an uncaught exception. The summary at Line 138 never prints, and the temp directory at Line 136 is never removed.

If mkdtempSync itself fails, bundleDir stays "". join("", "index.html") resolves relative to the current working directory, so the test reads an unrelated file or throws a confusing ENOENT.

Exit immediately after the failed preparation.

🐛 Proposed fix
 let bundleDir = ""
 try {
   bundleDir = mkdtempSync(join(tmpdir(), "tinystudio-bundle-test-"))
   await preparePublicDeployBundle({ sourceDir: join(ROOT, "public"), outputDir: bundleDir })
   ok(true, "bundle prepared in a temp directory")
 } catch (error) {
   ok(false, `bundle preparation failed: ${error.message}`)
+  if (bundleDir) rmSync(bundleDir, { recursive: true, force: true })
+  console.log(`\n${checks} checks, ${failures} failures`)
+  process.exit(1)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let bundleDir = ""
try {
bundleDir = mkdtempSync(join(tmpdir(), "tinystudio-bundle-test-"))
await preparePublicDeployBundle({ sourceDir: join(ROOT, "public"), outputDir: bundleDir })
ok(true, "bundle prepared in a temp directory")
} catch (error) {
ok(false, `bundle preparation failed: ${error.message}`)
}
const bundleFiles = ["index.html", "contact/index.html"]
for (const rel of bundleFiles) {
const html = readFileSync(join(bundleDir, rel), "utf8")
for (const marker of FORBIDDEN_MARKERS) {
ok(!marker.test(html), `${rel} has no ${marker.label}`)
}
}
let bundleDir = ""
try {
bundleDir = mkdtempSync(join(tmpdir(), "tinystudio-bundle-test-"))
await preparePublicDeployBundle({ sourceDir: join(ROOT, "public"), outputDir: bundleDir })
ok(true, "bundle prepared in a temp directory")
} catch (error) {
ok(false, `bundle preparation failed: ${error.message}`)
if (bundleDir) rmSync(bundleDir, { recursive: true, force: true })
console.log(`\n${checks} checks, ${failures} failures`)
process.exit(1)
}
const bundleFiles = ["index.html", "contact/index.html"]
for (const rel of bundleFiles) {
const html = readFileSync(join(bundleDir, rel), "utf8")
for (const marker of FORBIDDEN_MARKERS) {
ok(!marker.test(html), `${rel} has no ${marker.label}`)
}
}
🤖 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/test-public-deploy-bundle.mjs` around lines 58 - 73, Update the
bundle preparation catch block around preparePublicDeployBundle to stop
execution immediately after recording the failure, before iterating over
bundleFiles or reading files from bundleDir. Ensure the failure path does not
continue with an empty or incomplete bundle directory.

ok(filteredContact.includes("Primary inbox"), "neutral support card remains")
ok(filteredContact.includes("App-specific public pages"), "neutral apps card remains")
ok(filteredContact.includes("mailto:support@tinystudio.in"), "studio inbox mailto remains")
ok(!filteredContact.includes("<script>"), "the measurement-marker script is gone with the application section")

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 | 🟡 Minor | ⚡ Quick win

The <script> assertion matches only the attribute-free tag.

includes("<script>") does not match <script type="application/ld+json"> or <script defer>. If the measurement script returns with any attribute, this check still passes. Match the tag name instead.

🐛 Proposed fix
-ok(!filteredContact.includes("<script>"), "the measurement-marker script is gone with the application section")
+ok(
+  !/<script\b(?![^>]*application\/ld\+json)/i.test(filteredContact),
+  "the measurement-marker script is gone with the application section"
+)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ok(!filteredContact.includes("<script>"), "the measurement-marker script is gone with the application section")
ok(
!/<script\b(?![^>]*application\/ld\+json)/i.test(filteredContact),
"the measurement-marker script is gone with the application section"
)
🤖 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/test-public-deploy-bundle.mjs` at line 128, Update the assertion in
the public deploy bundle test to detect the script tag name regardless of
attributes, replacing the exact "<script>" substring check while preserving the
expectation that the measurement-marker script is absent.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 27bb4249de

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +257 to +260
for (const rel of REQUIRED_FILES) {
const p = join(bundleDir, rel)
if (!existsSync(p)) continue
const html = readFileSync(p, "utf8")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Scan every copied page for snoozed content

When a future public route or other text asset contains managed-service buyer-path copy, fs.cp includes it in the deployment, but this check examines only the hard-coded REQUIRED_FILES list. I reproduced this by adding public/new-offer/index.html containing “The Website Correction managed service”; bundle preparation exited successfully and copied the forbidden page, despite reporting that the snoozed content was removed. Recursively scan all copied text files so new routes cannot bypass the production snooze.

Useful? React with 👍 / 👎.

Comment on lines +257 to +260
for (const rel of REQUIRED_FILES) {
const p = join(bundleDir, rel)
if (!existsSync(p)) continue
const html = readFileSync(p, "utf8")

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 Reject bundles that omit required files

When site:publish is run directly from a checkout missing a required page or asset, this branch silently skips that file instead of failing, even though REQUIRED_FILES is documented as the set that must exist in every bundle. The full publish command does not run the repository gate first, so it can deploy an incomplete site; add a failure for a missing required path rather than continuing.

Useful? React with 👍 / 👎.

@nish3451
nish3451 merged commit 45ef63b into main Aug 11, 2026
3 checks passed
nish3451 added a commit that referenced this pull request Aug 12, 2026
…very main merge while the Pages token is missing (#111)

The deploy lane has been failing every main push since PR #81 landed:
without CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID the publish step
throws after a full npm ci + check + prepare cycle (~6 minutes), leaving
the live site permanently stale on the June-20 bundle.

PR #85 attempted to gate the publish step on the secrets, but its
step-level `if: ${{ secrets... != '' }}` conditions are invalid - the
secrets context is not available in step-level if conditions (context
availability table), so the workflow failed validation and never ran.

Hoist both secrets into the job-level env (where the secrets context is
allowed) and gate every expensive step with `if: env.X != ''`:
- Dormant (no secrets): seconds-long green run printing the exact
  one-time provisioning steps as a warning annotation.
- Active (both secrets set): unchanged fail-closed publish + live
  verification on every main merge; the moment the token is provisioned
  the lane deploys without any code change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant