Skip to content
Merged
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"selfhost:env-reference": "node scripts/gen-selfhost-env-reference.mjs",
"selfhost:env-reference:check": "node scripts/gen-selfhost-env-reference.mjs --check",
"selfhost:validate-observability": "node scripts/validate-observability-configs.mjs",
"selfhost:config-lint": "tsx scripts/gittensory-config-lint.ts",
"cf-typegen": "wrangler types && perl -pi -e 's/[[:blank:]]+$//' worker-configuration.d.ts",
"cf-typegen:check": "wrangler types --check",
"db:migrate:local": "wrangler d1 migrations apply gittensory --local",
Expand Down
46 changes: 46 additions & 0 deletions scripts/gittensory-config-lint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env tsx
// Wires up the previously-unwired src/selfhost/config-lint.ts validator (#2906): a self-hoster (or the
// maintainer, dogfooding on JSONbored/gittensory) can now actually run it against a real .gittensory.yml or
// private-config file and get actionable feedback, instead of the validator existing only in its own test suite.
import { existsSync, readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { lintManifestText, type SelfHostConfigLintResult } from "../src/selfhost/config-lint";

function usage(): string {
return `Usage: npm run selfhost:config-lint -- [path]

Validates a Gittensory focus manifest (.gittensory.yml, a per-repo/global self-host
private-config file, or any equivalent YAML/JSON file with the same shape) and reports
unrecognized top-level fields and parser warnings, without echoing any of the file's values.

Options:
path Manifest file to lint. Defaults to ".gittensory.yml" in the current directory.`;
}

export function formatLintReport(path: string, result: SelfHostConfigLintResult): string {
const lines = [`${path}: ${result.summary}`];
if (result.recognizedFields.length > 0) lines.push(` recognized fields: ${result.recognizedFields.join(", ")}`);
for (const warning of result.warnings) lines.push(` - ${warning}`);
return lines.join("\n");
}

/* v8 ignore start -- CLI entrypoint (file I/O + process.exit); formatLintReport above carries the tested logic. */
function main(): void {
const args = process.argv.slice(2);
if (args.includes("--help") || args.includes("-h")) {
console.log(usage());
return;
}
const path = args[0] ?? ".gittensory.yml";
if (!existsSync(path)) {
console.error(`gittensory-config-lint: no such file: ${path}\n\n${usage()}`);
process.exit(1);
}
const text = readFileSync(path, "utf8");
const result = lintManifestText(text);
console.log(formatLintReport(path, result));
if (!result.ok) process.exit(1);
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main();
/* v8 ignore stop */
36 changes: 36 additions & 0 deletions test/unit/gittensory-config-lint-script.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { formatLintReport } from "../../scripts/gittensory-config-lint";
import { lintManifestText } from "../../src/selfhost/config-lint";

describe("formatLintReport (#2906)", () => {
it("reports a valid manifest's summary and recognized fields, no warnings", () => {
const result = lintManifestText("wantedPaths:\n - src/\n");
expect(formatLintReport(".gittensory.yml", result)).toBe(
".gittensory.yml: Manifest parsed 1 recognized field.\n recognized fields: wantedPaths",
);
});

it("reports warnings without a recognized-fields line when none are recognized", () => {
const result = lintManifestText("unknownSecretKey: super-secret-value\n");
expect(formatLintReport(".gittensory.yml", result)).toBe(
[
".gittensory.yml: Manifest has 2 warnings.",
" - Manifest contained no recognized focus fields; falling back to deterministic signals.",
" - Manifest contains unknown top-level field: unknownSecretKey.",
].join("\n"),
);
// Never echoes the raw supplied value into the report (#2906 dogfoods config-lint's own secret-redaction).
expect(formatLintReport(".gittensory.yml", result)).not.toContain("super-secret-value");
});

it("reports both recognized fields and warnings together for a partially-valid manifest", () => {
const result = lintManifestText("wantedPaths: [src/]\nunknownSecretKey: super-secret-value\n");
expect(formatLintReport("private-config.yml", result)).toBe(
[
"private-config.yml: Manifest has 1 warning.",
" recognized fields: wantedPaths",
" - Manifest contains unknown top-level field: unknownSecretKey.",
].join("\n"),
);
});
});
Loading