diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a5cf3bf41..09554b1f1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,8 +105,13 @@ jobs: env: BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before }} HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha || github.sha }} + # Boolean dispatch inputs become the strings "true"/"false". Off-dispatch + # this context is empty, so the quoted comparison below is false — the + # same as interpolating a blank ${{ }} into the script, without putting + # GitHub context into a shell. + REFRESH_LIGHTHOUSE_BASELINE: ${{ github.event.inputs.refresh_lighthouse_baseline }} run: | - if [ "${{ github.event.inputs.refresh_lighthouse_baseline }}" = "true" ]; then + if [ "$REFRESH_LIGHTHOUSE_BASELINE" = "true" ]; then # A baseline refresh is a focused measurement operation, not a # synthetic full repository change. Keep the trusted workflow/perf # contracts without starting unrelated build, UI, DB or container jobs. diff --git a/src/app/caring-contacts/layout.tsx b/src/app/caring-contacts/layout.tsx index 909642d434..ec707f07d0 100644 --- a/src/app/caring-contacts/layout.tsx +++ b/src/app/caring-contacts/layout.tsx @@ -1,12 +1,15 @@ import type { Metadata } from "next"; import type { ReactNode } from "react"; +import { PRIVATE_APP_ROBOTS_METADATA } from "@/lib/crawler-policy"; + // Listed in the live tools catalogue by the owner's decision of 19 August 2026, but never // indexed: this workspace holds invented patients only and must not appear in a search result -// where its synthetic nature is not visible. +// where its synthetic nature is not visible. Use the shared private-app robots object so +// this route does not emit a narrower child override beside the root noindex extras. export const metadata: Metadata = { title: "Caring Contacts - Clinical KB", - robots: { index: false, follow: false }, + robots: PRIVATE_APP_ROBOTS_METADATA, }; export default function CaringContactsLayout({ children }: { children: ReactNode }) { diff --git a/tests/ci-cache-safety.test.ts b/tests/ci-cache-safety.test.ts index af89169520..072a79eb76 100644 --- a/tests/ci-cache-safety.test.ts +++ b/tests/ci-cache-safety.test.ts @@ -656,6 +656,23 @@ describe("Lighthouse budget routing", () => { expect(workflow).toMatch(/workflow_dispatch:\n\s+inputs:\n\s+refresh_lighthouse_baseline:/); }); + it("does not interpolate the lighthouse refresh dispatch input into a run script", () => { + // github.event.inputs is untrusted in `run:` (shell injection). Bind it through + // env and quote the variable. Job-level `if:` expressions may still read the + // input context directly — those are not a shell. + const classifyStep = sourceSegment(workflow, "name: Classify changed files", "sync-pr-policy-body:", { + label: "CI change-scope classify step", + }); + const runScript = classifyStep.split(/\n\s+run:\s*\|\n/)[1] ?? ""; + expect(runScript, "could not read the classify step run script").not.toBe(""); + expect(classifyStep).toMatch( + /REFRESH_LIGHTHOUSE_BASELINE:\s*\$\{\{\s*github\.event\.inputs\.refresh_lighthouse_baseline\s*\}\}/, + ); + expect(runScript).toContain('"$REFRESH_LIGHTHOUSE_BASELINE"'); + expect(runScript).not.toContain("github.event.inputs.refresh_lighthouse_baseline"); + expect(runScript).not.toMatch(/\$\{\{[\s\S]*?\}\}/); + }); + it("pairs promotion to pr-required with merge_group coverage", () => { // The budget skips merge_group ONLY because it is advisory and outside // pr-required, where it could add ~7 minutes of merge latency without ever diff --git a/tests/crawler-policy.test.ts b/tests/crawler-policy.test.ts index ab28a4fb59..51e0c873a0 100644 --- a/tests/crawler-policy.test.ts +++ b/tests/crawler-policy.test.ts @@ -1,5 +1,6 @@ import { expect, it } from "vitest"; +import { metadata as caringContactsMetadata } from "../src/app/caring-contacts/layout"; import robots from "../src/app/robots"; import { PRIVATE_APP_ROBOTS_METADATA } from "../src/lib/crawler-policy"; @@ -18,3 +19,7 @@ it("serves restrictive search metadata through crawlable routes", () => { }, }); }); + +it("keeps Caring Contacts on the shared private-app robots object", () => { + expect(caringContactsMetadata.robots).toEqual(PRIVATE_APP_ROBOTS_METADATA); +});