diff --git a/package-lock.json b/package-lock.json index c2d14ed..91c8544 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "commander": "^15.0.0", "dotenv": "^17.3.1", "ink": "^7.0.1", - "js-yaml": "^4.3.1", + "js-yaml": "^5.4.0", "jsonc-parser": "^3.3.1", "react": "^19.2.4", "smol-toml": "^1.6.0", @@ -3092,6 +3092,29 @@ } } }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/cosmiconfig/node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -5019,9 +5042,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.0.tgz", + "integrity": "sha512-jE7vUJIebKzYQI5xu4co5CRBDlDEYnHrdzsxs4O2giCz4v2SbVMYKpmt1D9L38OKQAeCWmrOTRiCV93u0UkaJA==", "funding": [ { "type": "github", @@ -5037,7 +5060,7 @@ "argparse": "^2.0.1" }, "bin": { - "js-yaml": "bin/js-yaml.js" + "js-yaml": "bin/js-yaml.mjs" } }, "node_modules/json-buffer": { diff --git a/package.json b/package.json index b9076e6..b983533 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "commander": "^15.0.0", "dotenv": "^17.3.1", "ink": "^7.0.1", - "js-yaml": "^4.3.1", + "js-yaml": "^5.4.0", "jsonc-parser": "^3.3.1", "react": "^19.2.4", "smol-toml": "^1.6.0", diff --git a/src/layer1-discovery/config-parser.ts b/src/layer1-discovery/config-parser.ts index 48687fd..a7f51c8 100644 --- a/src/layer1-discovery/config-parser.ts +++ b/src/layer1-discovery/config-parser.ts @@ -1,6 +1,6 @@ import { readFileSync } from "node:fs"; import dotenv from "dotenv"; -import yaml from "js-yaml"; +import * as yaml from "js-yaml"; import { parse as parseJsonc, printParseErrorCode, type ParseError } from "jsonc-parser"; import { parse as parseToml } from "smol-toml"; import type { DiscoveryFormat } from "../types/discovery.js"; @@ -51,6 +51,12 @@ export function parseConfigContent(content: string, format: DiscoveryFormat): Pa } if (format === "yaml") { + // js-yaml 5 throws on an empty document where 4 returned undefined. + // An empty config file is an ordinary thing to find on a machine and + // says nothing is configured — not a parse failure to report. + if (content.trim().length === 0) { + return { ok: true, data: undefined }; + } return { ok: true, data: yaml.load(content) as unknown }; } diff --git a/tests/layer1/config-parser.test.ts b/tests/layer1/config-parser.test.ts index 48137a7..eb15cec 100644 --- a/tests/layer1/config-parser.test.ts +++ b/tests/layer1/config-parser.test.ts @@ -90,3 +90,52 @@ describe("task 09 config parser", () => { } }); }); + +// js-yaml 5 changed three things that reach this parser: it dropped the +// default export, it stopped resolving `<<` merge keys, and it throws on an +// empty document where 4 returned undefined. These pin the two that are +// observable here, so a future bump cannot quietly change them back. +describe("YAML parsing behaviour this scanner depends on", () => { + it("treats an empty file as nothing configured, not as a parse failure", () => { + const dir = createTempDir(); + const path = join(dir, "empty.yaml"); + writeFileSync(path, ""); + + const result = parseConfigFile(path, "yaml"); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.data).toBeUndefined(); + }); + + it("treats a whitespace-only file the same way", () => { + const dir = createTempDir(); + const path = join(dir, "blank.yaml"); + writeFileSync(path, "\n \n\n"); + + const result = parseConfigFile(path, "yaml"); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.data).toBeUndefined(); + }); + + // Merge keys are a YAML 1.1 feature that js-yaml 5 no longer resolves, and + // GitHub Actions rejects anchors in workflows anyway. Asserted so the + // behaviour is recorded rather than discovered: a detector reading these + // sees the literal "<<" key, not the merged result. + it("surfaces a merge key literally rather than resolving it", () => { + const dir = createTempDir(); + const path = join(dir, "merge.yaml"); + writeFileSync(path, "base: &b\n permissions: write-all\njob:\n <<: *b\n name: build\n"); + + const result = parseConfigFile(path, "yaml"); + + expect(result.ok).toBe(true); + if (!result.ok) return; + const data = result.data as { job: Record }; + expect(data.job.name).toBe("build"); + expect(data.job.permissions).toBeUndefined(); + expect(data.job["<<"]).toEqual({ permissions: "write-all" }); + }); +});