Skip to content
Open
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
63 changes: 63 additions & 0 deletions e2e/tests/scripts/eol-crlf-regression.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { test, expect } from "vitest";

import { normalizeEol } from "../../../scripts/lib/eol.ts";

// Regression test for #6192 / #5176: Windows `core.autocrlf=true` checks LF
// files out with CRLF endings. The multi-line template literal in
// `scripts/check-packaged-leaf-boundary.ts` carries LF, so a byte-exact
// `String.prototype.includes` against a CRLF working tree never matches —
// the guard reports a phantom "ci.yml no longer contains the guarded … block"
// violation on a pristine `main`. Apply `normalizeEol` to both sides before
// comparing. Mirrors the precedent set by #5176 for design-system manifests.

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..", "..", "..");

// A reliable multi-line anchor that exists in ci.yml on upstream/main.
// Multi-line is essential: single-line strings trivially `includes()` whether
// the haystack is LF or CRLF — the #6192 bug specifically reproduces on
// multi-line template literals where the embedded \n cannot match a
// working-tree \r\n.
const CI_YML_ANCHOR = `if [ "\${{ needs.scopes.outputs.tools_dev_tests_required }}" = "true" ]; then
pnpm --filter @open-design/tools-dev test
fi`;

const ciLF = readFileSync(path.join(repoRoot, ".github/workflows/ci.yml"), "utf8");
const ciCRLF = ciLF.replace(/\n/g, "\r\n");

test("normalizeEol maps CRLF → LF", () => {
expect(normalizeEol("a\r\nb\r\nc")).toBe("a\nb\nc");
});

test("normalizeEol is a no-op on pure LF", () => {
const pureLF = "a\nb\nc";
expect(normalizeEol(pureLF)).toBe(pureLF);
});

test("normalizeEol is idempotent", () => {
const once = normalizeEol("a\r\nb\r\nc\r\n");
expect(normalizeEol(once)).toBe(once);
});

test("normalizeEol handles mixed CRLF/LF input", () => {
expect(normalizeEol("a\r\nb\nc\r\nd")).toBe("a\nb\nc\nd");
});

test("LF ci.yml contains the key tools-dev test command (#6192 sanity)", () => {
expect(ciLF.includes(CI_YML_ANCHOR)).toBe(true);
});

test("raw includes() against CRLF ci.yml fails (the bug #6192)", () => {
// On Windows the guard reads ci.yml with CRLF endings and then calls
// `workflow.includes(requiredWorkspaceUnitBlock)` — both literal strings
// use LF, so the comparison always returns false even on pristine main.
expect(ciCRLF.includes(CI_YML_ANCHOR)).toBe(false);
});

test("normalizeEol on both sides fixes the CRLF comparison (#6192 fix)", () => {
expect(normalizeEol(ciCRLF).includes(normalizeEol(CI_YML_ANCHOR))).toBe(true);
});
7 changes: 1 addition & 6 deletions scripts/check-design-system-manifests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,13 @@ import {
type DerivedDesignTokenBinding,
type DerivedDesignTokenReport,
} from "../packages/contracts/src/design-systems/derived-token-outputs.ts";
import { normalizeEol } from "./lib/eol.ts";

const repoRoot = path.resolve(import.meta.dirname, "..");
const designSystemsRoot = path.join(repoRoot, "design-systems");
const craftRoot = path.join(repoRoot, "craft");
const SKIPPED_DIRECTORIES = new Set(["_schema"]);

/** Normalize CRLF→LF so byte-exact generated-file comparisons are EOL-agnostic
* (Windows core.autocrlf=true checks generated LF files out as CRLF). See #5175. */
function normalizeEol(text: string): string {
return text.replace(/\r\n/g, "\n");
}

function toRepositoryPath(filePath: string): string {
return path.relative(repoRoot, filePath).split(path.sep).join("/");
}
Expand Down
7 changes: 6 additions & 1 deletion scripts/check-packaged-leaf-boundary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
evaluateScopeOutputs,
SCOPE_EFFECTS,
} from "./scopes.ts";
import { normalizeEol } from "./lib/eol.ts";

const repoRoot = path.resolve(import.meta.dirname, "..");
const checkedRoots = ["apps", "packages", "tools", "e2e"] as const;
Expand Down Expand Up @@ -249,7 +250,11 @@ export async function checkPackagedLeafBoundary(): Promise<boolean> {

const workflow = await readFile(path.join(repoRoot, ".github/workflows/ci.yml"), "utf8");
const errors = scopeBoundaryErrors();
if (!workflow.includes(requiredWorkspaceUnitBlock)) {
// Normalize EOL on both sides: Windows `core.autocrlf=true` checks the LF
// workflow file out with CRLF endings, and the multi-line template literal
// above carries LF. A byte-exact `includes` would otherwise never match on
// a Windows working tree even on a pristine main. See #6192 / #5176.
if (!normalizeEol(workflow).includes(normalizeEol(requiredWorkspaceUnitBlock))) {
errors.push("ci.yml no longer contains the guarded tools-dev, packaged unit, and focused E2E command block");
}

Expand Down
16 changes: 16 additions & 0 deletions scripts/lib/eol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* ─────────────────────────────────────────────────────────────────────────
* scripts/lib/eol.ts
*
* Shared helpers for byte-exact text comparisons that must ignore CRLF/LF
* drift. Windows `core.autocrlf=true` checks LF files out with CRLF endings,
* so any `String.prototype.includes` / strict-equality against a generated
* or template-literal block fails for line-ending reasons, not content
* reasons. Apply `normalizeEol` to BOTH sides before comparing.
*
* See: #5175 / #5176 (design-system manifests), #6192 (packaged-leaf
* boundary) for prior art using the same helper.
* ─────────────────────────────────────────────────────────────────── */

export function normalizeEol(text: string): string {
return text.replace(/\r\n/g, "\n");
}
Loading