From e7efdaf1faf1115d780b36b9df7505b152bab3c4 Mon Sep 17 00:00:00 2001 From: haseebmalik18 Date: Tue, 30 Jun 2026 14:56:10 -0400 Subject: [PATCH 1/3] populate reusedCode with detected USWDS and CMS Design System dependencies --- src/__tests__/unit/helper.test.ts | 216 +++++++++++++++++++++++++++++- src/gov-dependencies.ts | 59 ++++++++ src/helper.ts | 85 ++++++++++++ src/main.ts | 9 +- 4 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 src/gov-dependencies.ts diff --git a/src/__tests__/unit/helper.test.ts b/src/__tests__/unit/helper.test.ts index 368607f3..04af7bbe 100644 --- a/src/__tests__/unit/helper.test.ts +++ b/src/__tests__/unit/helper.test.ts @@ -1,8 +1,29 @@ import { describe, it, expect, jest, beforeEach } from "@jest/globals"; -import { createHelpers } from "../../helper.js"; +import { + createHelpers, + parsePackageJSON, + parseRequirementsTxt, + mergeReusedCode, +} from "../../helper.js"; +import { + GOV_DEPENDENCIES, + USWDS, + USWDS_COMPILE, + CMS_DESIGN_SYSTEM, + CMS_DS_HEALTHCARE_GOV, +} from "../../gov-dependencies.js"; import { createMockDeps, createMockOctokit } from "../fixtures/mock-deps.js"; import { Dependencies } from "../../types/Dependencies.js"; +// returns a readFile mock that serves content by filepath and rejects otherwise +function readFileFrom(files: Record) { + return jest.fn((filepath: string) => + filepath in files + ? Promise.resolve(files[filepath]) + : Promise.reject(new Error("ENOENT")), + ); +} + describe("createHelpers - calculateMetaData", () => { let deps: Dependencies; @@ -203,4 +224,197 @@ describe("createHelpers - validateOnly", () => { expect(deps.setFailed).not.toHaveBeenCalled(); expect(deps.log.info).toHaveBeenCalledWith("code.json is valid!"); }); +}); + +describe("parsePackageJSON", () => { + it("collects dependencies and devDependencies", () => { + const content = JSON.stringify({ + dependencies: { uswds: "^3.0.0", react: "^18.0.0" }, + devDependencies: { jest: "^29.0.0" }, + }); + expect(parsePackageJSON(content)).toEqual(["uswds", "react", "jest"]); + }); + + it("handles missing dependency sections", () => { + expect(parsePackageJSON(JSON.stringify({ name: "x" }))).toEqual([]); + }); + + it("returns empty array for invalid JSON", () => { + expect(parsePackageJSON("not json {{{")).toEqual([]); + }); +}); + +describe("parseRequirementsTxt", () => { + it("strips version specifiers, extras, markers and comments", () => { + const content = [ + "uswds==3.0.0", + "requests>=2.0 # http client", + "django[argon2]~=4.2", + 'pytz; python_version < "3.9"', + "# a comment line", + "", + "-r other-requirements.txt", + "--hash=sha256:abc", + ].join("\n"); + + expect(parseRequirementsTxt(content)).toEqual([ + "uswds", + "requests", + "django", + "pytz", + ]); + }); +}); + +describe("createHelpers - detectReusedCode", () => { + it("matches a known gov dependency from package.json", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ + "/github/workspace/package.json": JSON.stringify({ + dependencies: { "@uswds/uswds": "^3.0.0", react: "^18.0.0" }, + }), + }), + }); + + expect(await createHelpers(deps).detectReusedCode()).toEqual([USWDS]); + }); + + it("matches a known gov dependency from requirements.txt", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ + "/github/workspace/requirements.txt": "uswds==3.0.0\nrequests==2.0", + }), + }); + + expect(await createHelpers(deps).detectReusedCode()).toEqual([USWDS]); + }); + + it("matches multiple distinct gov dependencies in one manifest", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ + "/github/workspace/package.json": JSON.stringify({ + dependencies: { + "@uswds/uswds": "^3.0.0", + "@cmsgov/design-system": "^14.0.0", + react: "^18.0.0", + }, + }), + }), + }); + + expect(await createHelpers(deps).detectReusedCode()).toEqual([ + USWDS, + CMS_DESIGN_SYSTEM, + ]); + }); + + it("lists @uswds/compile as a separate entry from @uswds/uswds", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ + "/github/workspace/package.json": JSON.stringify({ + dependencies: { "@uswds/uswds": "^3.0.0" }, + devDependencies: { "@uswds/compile": "^1.0.0" }, + }), + }), + }); + + expect(await createHelpers(deps).detectReusedCode()).toEqual([ + USWDS, + USWDS_COMPILE, + ]); + }); + + it("lists each CMS dependency individually", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ + "/github/workspace/package.json": JSON.stringify({ + dependencies: { "@cmsgov/design-system": "^14.0.0" }, + devDependencies: { "@cmsgov/ds-healthcare-gov": "^18.0.0" }, + }), + }), + }); + + expect(await createHelpers(deps).detectReusedCode()).toEqual([ + CMS_DESIGN_SYSTEM, + CMS_DS_HEALTHCARE_GOV, + ]); + }); + + it("dedupes the same dependency found across both manifests", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ + "/github/workspace/package.json": JSON.stringify({ + dependencies: { "@uswds/uswds": "^3.0.0" }, + }), + "/github/workspace/requirements.txt": "uswds==3.0.0", + }), + }); + + expect(await createHelpers(deps).detectReusedCode()).toEqual([USWDS]); + }); + + it("returns empty when no manifests are present", async () => { + const deps = createMockDeps(); + expect(await createHelpers(deps).detectReusedCode()).toEqual([]); + }); + + it("returns empty when no known gov dependencies are found", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ + "/github/workspace/package.json": JSON.stringify({ + dependencies: { react: "^18.0.0" }, + }), + }), + }); + + expect(await createHelpers(deps).detectReusedCode()).toEqual([]); + }); + + it("ignores dependency names that collide with Object prototype members", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ + "/github/workspace/package.json": JSON.stringify({ + dependencies: { constructor: "1.0.0", valueOf: "1.0.0" }, + }), + }), + }); + + expect(await createHelpers(deps).detectReusedCode()).toEqual([]); + }); +}); + +describe("mergeReusedCode", () => { + it("appends detected entries to existing ones", () => { + const existing = [{ name: "Other Gov Tool", URL: "https://example.gov" }]; + expect(mergeReusedCode(existing, [USWDS])).toEqual([...existing, USWDS]); + }); + + it("does not duplicate an entry already present by URL", () => { + const existing = [{ name: "USWDS (manual)", URL: USWDS.URL }]; + expect(mergeReusedCode(existing, [USWDS])).toEqual(existing); + }); + + it("does not duplicate an entry already present by name", () => { + const existing = [{ name: USWDS.name, URL: "https://old.example" }]; + expect(mergeReusedCode(existing, [USWDS])).toEqual(existing); + }); + + it("returns existing unchanged when nothing is detected", () => { + const existing = [{ name: "Gov Tool", URL: "https://example.gov" }]; + expect(mergeReusedCode(existing, [])).toEqual(existing); + }); + + it("tolerates a non-array existing value", () => { + expect(mergeReusedCode(undefined as any, [USWDS])).toEqual([USWDS]); + }); +}); + +describe("GOV_DEPENDENCIES integrity", () => { + const entries = Object.entries(GOV_DEPENDENCIES); + + it.each(entries)("%s has a lowercase key and a valid entry", (key, entry) => { + expect(key).toBe(key.toLowerCase()); + expect(entry.name.trim()).not.toBe(""); + expect(entry.URL).toMatch(/^https:\/\//); + }); }); \ No newline at end of file diff --git a/src/gov-dependencies.ts b/src/gov-dependencies.ts new file mode 100644 index 00000000..1547d7a2 --- /dev/null +++ b/src/gov-dependencies.ts @@ -0,0 +1,59 @@ +export interface ReusedCodeEntry { + name: string; + URL: string; +} + +// each dependency gets its own entry with the repository URL where the source code is hosted +export const USWDS: ReusedCodeEntry = { + name: "U.S. Web Design System (USWDS)", + URL: "https://github.com/uswds/uswds", +}; + +export const USWDS_COMPILE: ReusedCodeEntry = { + name: "USWDS Compile", + URL: "https://github.com/uswds/uswds-compile", +}; + +export const CMS_DESIGN_SYSTEM: ReusedCodeEntry = { + name: "CMS Design System", + URL: "https://github.com/CMSgov/design-system", +}; + +export const CMS_DS_HEALTHCARE_GOV: ReusedCodeEntry = { + name: "CMS Design System - HealthCare.gov", + URL: "https://github.com/CMSgov/design-system/tree/main/packages/ds-healthcare-gov", +}; + +export const CMS_DS_MEDICARE_GOV: ReusedCodeEntry = { + name: "CMS Design System - Medicare.gov", + URL: "https://github.com/CMSgov/design-system/tree/main/packages/ds-medicare-gov", +}; + +export const CMS_DS_CMS_GOV: ReusedCodeEntry = { + name: "CMS Design System - CMS.gov", + URL: "https://github.com/CMSgov/design-system/tree/main/packages/ds-cms-gov", +}; + +// keys are lowercased package names; each maps to its own unique entry +export const GOV_DEPENDENCIES: Record = { + uswds: USWDS, + "@uswds/uswds": USWDS, + "@uswds/compile": USWDS_COMPILE, + "@cmsgov/design-system": CMS_DESIGN_SYSTEM, + "@cmsgov/ds-healthcare-gov": CMS_DS_HEALTHCARE_GOV, + "@cmsgov/ds-medicare-gov": CMS_DS_MEDICARE_GOV, + "@cmsgov/ds-cms-gov": CMS_DS_CMS_GOV, +}; + +// npm names are lowercase and Python names are case-insensitive, so lowercasing suffices to match +export function normalizePackageName(name: string): string { + return name.trim().toLowerCase(); +} + +export function lookupGovDependency(name: string): ReusedCodeEntry | undefined { + // hasOwn guard so names like "constructor"/"__proto__" don't match inherited members + const key = normalizePackageName(name); + return Object.hasOwn(GOV_DEPENDENCIES, key) + ? GOV_DEPENDENCIES[key] + : undefined; +} diff --git a/src/helper.ts b/src/helper.ts index 0bd5ccab..5e04df6b 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -2,6 +2,7 @@ import { CodeJSON } from "./types/CodeJSONSchema.js"; import { BasicRepoInfo } from "./types/BasicRepoInfo.js"; import { validateCodeJSON } from "./zod-validation.js"; import { Dependencies } from "./types/Dependencies.js"; +import { ReusedCodeEntry, lookupGovDependency } from "./gov-dependencies.js"; const HOURS_PER_MONTH = 730.001; @@ -90,6 +91,39 @@ export function createHelpers(deps: Dependencies) { } } + //=============================================== + // Reused Code + //=============================================== + async function detectReusedCode(): Promise { + const [packageJSON, requirements] = await Promise.all([ + readManifest("/github/workspace/package.json"), + readManifest("/github/workspace/requirements.txt"), + ]); + + const names: string[] = []; + if (packageJSON) names.push(...parsePackageJSON(packageJSON)); + if (requirements) names.push(...parseRequirementsTxt(requirements)); + + const entries: ReusedCodeEntry[] = []; + const seen = new Set(); + for (const name of names) { + const entry = lookupGovDependency(name); + if (entry && !seen.has(entry.URL)) { + seen.add(entry.URL); + entries.push(entry); + } + } + return entries; + } + + async function readManifest(filepath: string): Promise { + try { + return await deps.readFile(filepath); + } catch { + return null; + } + } + async function getBaseBranch(): Promise { if (deps.branch) { return deps.branch; @@ -267,6 +301,7 @@ export function createHelpers(deps: Dependencies) { return { calculateMetaData, + detectReusedCode, getBaseBranch, validateOnly, validateCodeJSON, @@ -279,6 +314,56 @@ export function createHelpers(deps: Dependencies) { // export the type for convenience export type Helpers = ReturnType; +export function parsePackageJSON(content: string): string[] { + try { + const pkg = JSON.parse(content); + return [ + ...Object.keys(pkg.dependencies ?? {}), + ...Object.keys(pkg.devDependencies ?? {}), + ]; + } catch { + return []; + } +} + +// strips version specifiers, extras, markers and comments, leaving the bare package name +export function parseRequirementsTxt(content: string): string[] { + const names: string[] = []; + for (const rawLine of content.split(/\r?\n/)) { + const line = rawLine.split("#")[0].trim(); + if (!line || line.startsWith("-")) continue; + const match = line.match(/^[A-Za-z0-9._-]+/); + if (match) names.push(match[0]); + } + return names; +} + +// keeps existing entries (manual edits) and appends detected ones, de-duped by name and URL +export function mergeReusedCode( + existing: Array<{ name?: string; URL?: string }>, + detected: ReusedCodeEntry[], +): Array<{ name?: string; URL?: string }> { + const base = Array.isArray(existing) ? existing : []; + const merged = [...base]; + const seenURLs = new Set( + base.map((e) => e.URL?.toLowerCase()).filter(Boolean), + ); + const seenNames = new Set( + base.map((e) => e.name?.toLowerCase()).filter(Boolean), + ); + + for (const entry of detected) { + const url = entry.URL.toLowerCase(); + const name = entry.name.toLowerCase(); + if (seenURLs.has(url) || seenNames.has(name)) continue; + merged.push(entry); + seenURLs.add(url); + seenNames.add(name); + } + + return merged; +} + function bodyOfPR(): string { return ` ## Welcome to the Federal Open Source Community! diff --git a/src/main.ts b/src/main.ts index 0d0eeebf..15ef0b58 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { CodeJSON } from "./types/CodeJSONSchema.js"; import { Dependencies } from "./types/Dependencies.js"; -import { createHelpers, Helpers } from "./helper.js"; +import { createHelpers, Helpers, mergeReusedCode } from "./helper.js"; import { createProductionDeps } from "./create-deps.js"; const baselineCodeJSON: Partial = { @@ -136,6 +136,12 @@ async function getMetaData( tags?.push("archived"); } + // detect government-made dependencies and merge with any existing reusedCode + const reusedCode = mergeReusedCode( + existingCodeJSON?.reusedCode ?? [], + await helpers.detectReusedCode(), + ); + return { name: partialCodeJSON.name, description: description, @@ -158,6 +164,7 @@ async function getMetaData( feedbackMechanism, SBOM, contractNumber, + reusedCode, }; } From 406dea2332efe0d529f5b96b1432116ff64a0d0f Mon Sep 17 00:00:00 2001 From: haseebmalik18 Date: Wed, 8 Jul 2026 11:37:24 -0400 Subject: [PATCH 2/3] populate reusedCode with the upstream repo when the repository is a fork --- src/__tests__/fixtures/mock-deps.ts | 2 ++ src/__tests__/unit/helper.test.ts | 54 +++++++++++++++++++++++++++++ src/__tests__/unit/main.test.ts | 38 +++++++++++++++++++- src/helper.ts | 21 +++++++++++ src/main.ts | 14 +++++--- src/types/Dependencies.ts | 5 +++ 6 files changed, 128 insertions(+), 6 deletions(-) diff --git a/src/__tests__/fixtures/mock-deps.ts b/src/__tests__/fixtures/mock-deps.ts index 316622a9..54aeb143 100644 --- a/src/__tests__/fixtures/mock-deps.ts +++ b/src/__tests__/fixtures/mock-deps.ts @@ -28,6 +28,8 @@ export function createMockOctokit(overrides: Partial> created_at: "2024-01-01T00:00:00Z", updated_at: "2024-06-01T00:00:00Z", default_branch: "main", + fork: false, + parent: null, }, }), listLanguages: overrides.rest?.repos?.listLanguages as OctokitClient["rest"]["repos"]["listLanguages"] ?? diff --git a/src/__tests__/unit/helper.test.ts b/src/__tests__/unit/helper.test.ts index 04af7bbe..ff0e81ab 100644 --- a/src/__tests__/unit/helper.test.ts +++ b/src/__tests__/unit/helper.test.ts @@ -383,6 +383,60 @@ describe("createHelpers - detectReusedCode", () => { }); }); +describe("createHelpers - detectForkParent", () => { + function mockRepoGet(data: Record): Dependencies { + return createMockDeps({ + octokit: createMockOctokit({ + rest: { + repos: { + get: jest.fn().mockResolvedValue({ data }), + }, + }, + }), + }); + } + + it("returns the upstream parent when the repo is a fork", async () => { + const deps = mockRepoGet({ + fork: true, + parent: { + full_name: "upstream-owner/upstream-repo", + html_url: "https://github.com/upstream-owner/upstream-repo", + }, + }); + + expect(await createHelpers(deps).detectForkParent()).toEqual({ + name: "upstream-owner/upstream-repo", + URL: "https://github.com/upstream-owner/upstream-repo", + }); + }); + + it("returns null when the repo is not a fork", async () => { + const deps = mockRepoGet({ fork: false, parent: null }); + expect(await createHelpers(deps).detectForkParent()).toBeNull(); + }); + + it("returns null when fork is true but parent is missing", async () => { + const deps = mockRepoGet({ fork: true }); + expect(await createHelpers(deps).detectForkParent()).toBeNull(); + }); + + it("returns null and logs when the API call fails", async () => { + const deps = createMockDeps({ + octokit: createMockOctokit({ + rest: { + repos: { + get: jest.fn().mockRejectedValue(new Error("API down")), + }, + }, + }), + }); + + expect(await createHelpers(deps).detectForkParent()).toBeNull(); + expect(deps.log.error).toHaveBeenCalled(); + }); +}); + describe("mergeReusedCode", () => { it("appends detected entries to existing ones", () => { const existing = [{ name: "Other Gov Tool", URL: "https://example.gov" }]; diff --git a/src/__tests__/unit/main.test.ts b/src/__tests__/unit/main.test.ts index 39a5b186..4204b8e6 100644 --- a/src/__tests__/unit/main.test.ts +++ b/src/__tests__/unit/main.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, jest, beforeEach, afterEach } from "@jest/globals"; import { runWithDeps, filterValidFields, getMetaData } from "../../main.js"; import { createHelpers } from "../../helper.js"; -import { createMockDeps } from "../fixtures/mock-deps.js"; +import { createMockDeps, createMockOctokit } from "../fixtures/mock-deps.js"; import validCodeJSON from "../fixtures/test-code.json"; describe("filterValidFields", () => { @@ -57,6 +57,42 @@ describe("getMetaData", () => { expect(result.contractNumber).toEqual(["LEGACY-001"]); }); + + it("adds the fork upstream to reusedCode", async () => { + const forkOctokit = createMockOctokit({ + rest: { + repos: { + get: jest.fn().mockResolvedValue({ + data: { + name: "test-repo", + description: "A forked repository", + html_url: "https://github.com/test-owner/test-repo", + private: false, + forks_count: 0, + topics: [], + created_at: "2024-01-01T00:00:00Z", + updated_at: "2024-06-01T00:00:00Z", + default_branch: "main", + fork: true, + parent: { + full_name: "upstream-owner/upstream-repo", + html_url: "https://github.com/upstream-owner/upstream-repo", + }, + }, + }), + }, + }, + }); + const deps = createMockDeps({ octokit: forkOctokit }); + const helpers = createHelpers(deps); + + const result = await getMetaData(helpers, deps, null); + + expect(result.reusedCode).toContainEqual({ + name: "upstream-owner/upstream-repo", + URL: "https://github.com/upstream-owner/upstream-repo", + }); + }); }); describe("runWithDeps", () => { diff --git a/src/helper.ts b/src/helper.ts index 5e04df6b..fbb244af 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -124,6 +124,26 @@ export function createHelpers(deps: Dependencies) { } } + //=============================================== + // Fork Upstream + //=============================================== + async function detectForkParent(): Promise { + try { + const repoData = await octokit.rest.repos.get({ owner, repo }); + const { fork, parent } = repoData.data; + + if (!fork || !parent) return null; + + return { + name: parent.full_name, + URL: parent.html_url, + }; + } catch (error) { + log.error(`Failed to detect fork parent: ${error}`); + return null; + } + } + async function getBaseBranch(): Promise { if (deps.branch) { return deps.branch; @@ -302,6 +322,7 @@ export function createHelpers(deps: Dependencies) { return { calculateMetaData, detectReusedCode, + detectForkParent, getBaseBranch, validateOnly, validateCodeJSON, diff --git a/src/main.ts b/src/main.ts index 15ef0b58..c0422639 100644 --- a/src/main.ts +++ b/src/main.ts @@ -136,11 +136,15 @@ async function getMetaData( tags?.push("archived"); } - // detect government-made dependencies and merge with any existing reusedCode - const reusedCode = mergeReusedCode( - existingCodeJSON?.reusedCode ?? [], - await helpers.detectReusedCode(), - ); + // detect the fork upstream and government-made dependencies, then merge with any existing reusedCode + const [forkParent, detectedDeps] = await Promise.all([ + helpers.detectForkParent(), + helpers.detectReusedCode(), + ]); + const reusedCode = mergeReusedCode(existingCodeJSON?.reusedCode ?? [], [ + ...(forkParent ? [forkParent] : []), + ...detectedDeps, + ]); return { name: partialCodeJSON.name, diff --git a/src/types/Dependencies.ts b/src/types/Dependencies.ts index c00f470d..f734c764 100644 --- a/src/types/Dependencies.ts +++ b/src/types/Dependencies.ts @@ -71,6 +71,11 @@ export interface RepoResponse { created_at: string; updated_at: string; default_branch: string; + fork?: boolean; + parent?: { + full_name: string; + html_url: string; + } | null; }; } From 1c4ac04d1ec62b56f9d814a6a6c64644a6822f41 Mon Sep 17 00:00:00 2001 From: haseebmalik18 Date: Mon, 13 Jul 2026 15:45:10 -0400 Subject: [PATCH 3/3] Revert unrelated formatting changes --- .github/CODEOWNERS.md | 2 +- .github/workflows/test.yml | 2 +- COMMUNITY.md | 18 ++-- CONTRIBUTING.md | 1 + code.json | 2 +- src/__tests__/fixtures/test-code.json | 2 +- src/__tests__/unit/zod-validation.test.ts | 41 +++------ src/create-deps.ts | 2 +- src/scripts/generate-schema.ts | 101 +++++++++++----------- src/scripts/get-latest-schema.ts | 12 ++- src/types/BasicRepoInfo.ts | 2 +- src/zod-validation.ts | 5 +- 12 files changed, 83 insertions(+), 107 deletions(-) diff --git a/.github/CODEOWNERS.md b/.github/CODEOWNERS.md index 66b376e8..817d38d8 100644 --- a/.github/CODEOWNERS.md +++ b/.github/CODEOWNERS.md @@ -7,4 +7,4 @@ ## Repository Domains -/src/types - [@natalialuzuriaga](https://github.com/natalialuzuriaga) +/src/types - [@natalialuzuriaga](https://github.com/natalialuzuriaga) \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 23e8a69b..d28594f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 - cache: "npm" + cache: 'npm' - name: Install dependencies run: npm ci diff --git a/COMMUNITY.md b/COMMUNITY.md index 23813a15..e6c0aaf7 100644 --- a/COMMUNITY.md +++ b/COMMUNITY.md @@ -8,13 +8,13 @@ automated-codejson-generator is supported by a dedicated team of individuals ful Roles to include, but not limited to: Project Owner, Technical Lead, Developers/Contributors, Community Manager, Security Team, Policy Advisor, Contracting Officer's Representative, Compliance Officer, Procurement Officer --> -| Role | Name | Affiliation | -| :---------------- | :---------------- | :---------- | -| Open Source Lead | Remy DeCausemaker | DSAC | -| Software Engineer | Sachin Panayil | DSAC | -| Software Engineer | Natalia Luzuriaga | DSAC | -| Software Engineer | Isaac Milarsky | DSAC | -| Software Engineer | Dinne Kopelevich | DSAC | +| Role | Name | Affiliation | +| :---------------- | :------------------ | :---------- | +| Open Source Lead | Remy DeCausemaker | DSAC | +| Software Engineer | Sachin Panayil | DSAC | +| Software Engineer | Natalia Luzuriaga | DSAC | +| Software Engineer | Isaac Milarsky | DSAC | +| Software Engineer | Dinne Kopelevich | DSAC | See [CODEOWNERS.md](.github/CODEOWNERS.md) for a list of those responsible for the code and documentation in this repository. @@ -58,7 +58,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for more details on the release process. -![]() +![](https://img.shields.io/github/contributors/DSACMS/automated-codejson-generator?style=flat-square&label=Contributor%20Count(incl.%20bots)) @@ -141,4 +141,4 @@ When participating in Code.json Auto Generator open source community conversatio ### Acknowledgements -The Community Guidelines sections were originally forked from the [United States Digital Service](https://usds.gov) [Justice40](https://thejustice40.com) open source [repository](https://github.com/usds/justice40-tool), and we would like to acknowledge and thank the community for their contributions. +The Community Guidelines sections were originally forked from the [United States Digital Service](https://usds.gov) [Justice40](https://thejustice40.com) open source [repository](https://github.com/usds/justice40-tool), and we would like to acknowledge and thank the community for their contributions. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 730fe967..23f9a906 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,6 +69,7 @@ We follow a **GitHub Flow–inspired workflow** with a protected `main` branch a - This triggers automatic versioning, tagging, and GitHub Release creation 10. Delete your feature branch after merge + ### Testing Conventions - Tests are written using Jest and can be found in the `__tests__` directory diff --git a/code.json b/code.json index a8ee5775..134c9ce9 100644 --- a/code.json +++ b/code.json @@ -68,4 +68,4 @@ "subsetInHealthcare": ["operational"], "userType": ["government"], "maturityModelTier": 3 -} +} \ No newline at end of file diff --git a/src/__tests__/fixtures/test-code.json b/src/__tests__/fixtures/test-code.json index 6770887e..134f4d14 100644 --- a/src/__tests__/fixtures/test-code.json +++ b/src/__tests__/fixtures/test-code.json @@ -53,4 +53,4 @@ "subsetInHealthcare": ["operational"], "userType": ["government"], "maturityModelTier": 1 -} +} \ No newline at end of file diff --git a/src/__tests__/unit/zod-validation.test.ts b/src/__tests__/unit/zod-validation.test.ts index 20e84663..1d14c690 100644 --- a/src/__tests__/unit/zod-validation.test.ts +++ b/src/__tests__/unit/zod-validation.test.ts @@ -8,16 +8,13 @@ const createCodeJSON = (overrides: Record = {}) => ({ }); // ============================================================================= -// FIXTURE VALIDATION +// FIXTURE VALIDATION // ============================================================================= describe("CodeJSONSchema - fixture validation", () => { it("accepts the valid test fixture", () => { const result = CodeJSONSchema.safeParse(validCodeJSON); if (!result.success) { - console.error( - "Validation errors:", - JSON.stringify(result.error, null, 2), - ); + console.error("Validation errors:", JSON.stringify(result.error, null, 2)); } expect(result.success).toBe(true); }); @@ -28,15 +25,13 @@ describe("CodeJSONSchema - fixture validation", () => { if (result.success) { expect(result.data.name).toBe("automated-codejson-generator"); expect(result.data.status).toBe("Production"); - expect(result.data.organization).toBe( - "Centers for Medicare & Medicaid Services", - ); + expect(result.data.organization).toBe("Centers for Medicare & Medicaid Services"); } }); }); // ============================================================================= -// REQUIRED FIELDS +// REQUIRED FIELDS // ============================================================================= describe("CodeJSONSchema - required fields", () => { it("rejects missing name", () => { @@ -303,15 +298,7 @@ describe("CodeJSONSchema - maturityModelTier enum", () => { // PLATFORMS ENUM ARRAY // ============================================================================= describe("CodeJSONSchema - platforms enum array", () => { - const validPlatforms = [ - "web", - "windows", - "mac", - "linux", - "ios", - "android", - "other", - ]; + const validPlatforms = ["web", "windows", "mac", "linux", "ios", "android", "other"]; it("accepts all valid platform values", () => { const input = createCodeJSON({ platforms: validPlatforms }); @@ -357,9 +344,7 @@ describe("CodeJSONSchema - subsetInHealthcare enum array", () => { }); it("rejects duplicate subsets", () => { - const input = createCodeJSON({ - subsetInHealthcare: ["medicare", "medicare"], - }); + const input = createCodeJSON({ subsetInHealthcare: ["medicare", "medicare"] }); const result = CodeJSONSchema.safeParse(input); expect(result.success).toBe(false); }); @@ -495,16 +480,12 @@ describe("CodeJSONSchema - usageType enum", () => { }); // Test each exemption type requires exemptionText - const exemptionTypes = validUsageTypes.filter((t) => - t.startsWith("exemptBy"), - ); + const exemptionTypes = validUsageTypes.filter((t) => t.startsWith("exemptBy")); exemptionTypes.forEach((exemption) => { it(`requires exemptionText for ${exemption}`, () => { const input = createCodeJSON({ permissions: { - licenses: [ - { name: "MIT", URL: "https://opensource.org/licenses/MIT" }, - ], + licenses: [{ name: "MIT", URL: "https://opensource.org/licenses/MIT" }], usageType: [exemption], exemptionText: null, }, @@ -516,9 +497,7 @@ describe("CodeJSONSchema - usageType enum", () => { it(`accepts ${exemption} with valid exemptionText`, () => { const input = createCodeJSON({ permissions: { - licenses: [ - { name: "MIT", URL: "https://opensource.org/licenses/MIT" }, - ], + licenses: [{ name: "MIT", URL: "https://opensource.org/licenses/MIT" }], usageType: [exemption], exemptionText: "Valid exemption justification text", }, @@ -1366,4 +1345,4 @@ describe("CodeJSONSchema - unique array validation", () => { const result = CodeJSONSchema.safeParse(input); expect(result.success).toBe(false); }); -}); +}); \ No newline at end of file diff --git a/src/create-deps.ts b/src/create-deps.ts index 1d0a5d14..81b1f5c8 100644 --- a/src/create-deps.ts +++ b/src/create-deps.ts @@ -63,4 +63,4 @@ export function createProductionDeps(): Dependencies { setOutput: core.setOutput, setFailed: core.setFailed, }; -} +} \ No newline at end of file diff --git a/src/scripts/generate-schema.ts b/src/scripts/generate-schema.ts index 696491ec..f3021c23 100644 --- a/src/scripts/generate-schema.ts +++ b/src/scripts/generate-schema.ts @@ -4,25 +4,24 @@ import prettier from "prettier"; import { JsonSchema, jsonSchemaToZod } from "json-schema-to-zod"; import { getLatestSchemaVersion } from "./get-latest-schema.js"; -const SCHEMA_BASE_URL = - "https://raw.githubusercontent.com/DSACMS/gov-codejson/refs/heads/main/schemas/cms"; +const SCHEMA_BASE_URL = "https://raw.githubusercontent.com/DSACMS/gov-codejson/refs/heads/main/schemas/cms"; const filePath = "src/types/CodeJSONSchema.ts"; function allowEmptyUrls(zodCode: string): string { - // allow empty strings for all URL fields while still validating non-empty values - return zodCode.replace(/\.url\(\)/g, '.url().or(z.literal(""))'); -} + // allow empty strings for all URL fields while still validating non-empty values + return zodCode.replace(/\.url\(\)/g, '.url().or(z.literal(""))'); + } function fixUniqueArrays(zodCode: string): string { - // replace .unique() with .refine() pattern since Zod doesn't have .unique() for arrays but converter adds it in there - return zodCode.replace( - /\.unique\(\)/g, - ".refine((items) => new Set(items).size === items.length, { message: 'Array must contain unique values' })", - ); + // replace .unique() with .refine() pattern since Zod doesn't have .unique() for arrays but converter adds it in there + return zodCode.replace( + /\.unique\(\)/g, + ".refine((items) => new Set(items).size === items.length, { message: 'Array must contain unique values' })" + ); } function addAdditionalRefinements(): string { - const permissionRefinement = ` + const permissionRefinement = ` .refine( (data) => { const usageTypes = data.permissions?.usageType ?? []; @@ -46,46 +45,44 @@ function addAdditionalRefinements(): string { ); `; - const refinements = permissionRefinement; - return refinements; + const refinements = permissionRefinement + return refinements } async function formatFile(filePath: string) { - const absolutePath = path.resolve(filePath); - const source = fs.readFileSync(absolutePath, "utf8"); - const config = await prettier.resolveConfig(absolutePath); + const absolutePath = path.resolve(filePath); + const source = fs.readFileSync(absolutePath, "utf8"); + const config = await prettier.resolveConfig(absolutePath); - const formatted = await prettier.format(source, { - ...config, - filepath: absolutePath, - }); + const formatted = await prettier.format(source, { + ...config, + filepath: absolutePath, + }); - fs.writeFileSync(absolutePath, formatted); + fs.writeFileSync(absolutePath, formatted); } async function generateSchema() { - const schemaVersion = await getLatestSchemaVersion(); - console.log(`Latest schema version: ${schemaVersion}`); + const schemaVersion = await getLatestSchemaVersion(); + console.log(`Latest schema version: ${schemaVersion}`); - const schemaURL = `${SCHEMA_BASE_URL}/schema-${schemaVersion}.json`; - console.log(`Fetching JSON schema from GitHub...`); - const response = await fetch(schemaURL); + const schemaURL = `${SCHEMA_BASE_URL}/schema-${schemaVersion}.json`; + console.log(`Fetching JSON schema from GitHub...`); + const response = await fetch(schemaURL); + + if (!response.ok) { + throw new Error(`Failed to fetch schema: ${response.status} ${response.statusText}`); + } - if (!response.ok) { - throw new Error( - `Failed to fetch schema: ${response.status} ${response.statusText}`, - ); - } + const jsonSchema = (await response.json()) as JsonSchema; - const jsonSchema = (await response.json()) as JsonSchema; + console.log(`Converting JSON schema to Zod...`); + let zodSourceCode = jsonSchemaToZod(jsonSchema); - console.log(`Converting JSON schema to Zod...`); - let zodSourceCode = jsonSchemaToZod(jsonSchema); + zodSourceCode = fixUniqueArrays(zodSourceCode); + zodSourceCode = allowEmptyUrls(zodSourceCode) - zodSourceCode = fixUniqueArrays(zodSourceCode); - zodSourceCode = allowEmptyUrls(zodSourceCode); - - const fileContent = ` + const fileContent = ` // DO NOT EDIT - AUTOMATICALLY GENERATED FILE!!! // Schema Version: ${schemaVersion} @@ -96,22 +93,22 @@ async function generateSchema() { export type CodeJSON = z.infer; `; - fs.writeFileSync(filePath, fileContent); - console.log(`File written to ${filePath}...`); + fs.writeFileSync(filePath, fileContent); + console.log(`File written to ${filePath}...`); - await formatFile(filePath); - console.log(`Schema generation complete!`); + await formatFile(filePath); + console.log(`Schema generation complete!`); } try { - await generateSchema(); + await generateSchema(); } catch (error) { - console.error(`Schema generation failed!`); - - if (error instanceof Error) { - console.error(`Error: ${error.message}`); - } else { - console.error(`Unknown error:`, error); - } - process.exit(1); -} + console.error(`Schema generation failed!`); + + if (error instanceof Error) { + console.error(`Error: ${error.message}`); + } else { + console.error(`Unknown error:`, error); + } + process.exit(1); +} \ No newline at end of file diff --git a/src/scripts/get-latest-schema.ts b/src/scripts/get-latest-schema.ts index 571ada4a..b1d7ded8 100644 --- a/src/scripts/get-latest-schema.ts +++ b/src/scripts/get-latest-schema.ts @@ -25,22 +25,20 @@ export async function getLatestSchemaVersion(): Promise { throw new Error("No schema versions found"); } - const versionNumber = versions.sort((a, b) => - b.localeCompare(a, undefined, { numeric: true }), - )[0]; - // console.log(versionNumber) - return versionNumber; + const versionNumber = versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))[0]; +// console.log(versionNumber) + return versionNumber } // try { // await getLatestSchemaVersion(); // } catch (error) { // console.error(`GET operation failed!`); - + // if (error instanceof Error) { // console.error(`Error: ${error.message}`); // } else { // console.error(`Unknown error:`, error); // } // process.exit(1); -// } +// } \ No newline at end of file diff --git a/src/types/BasicRepoInfo.ts b/src/types/BasicRepoInfo.ts index 627eb9a8..61a7ffd2 100644 --- a/src/types/BasicRepoInfo.ts +++ b/src/types/BasicRepoInfo.ts @@ -12,7 +12,7 @@ export interface BasicRepoInfo { interface Date { created: string; lastModified: string; - metadataLastUpdated: string; + metadataLastUpdated: string } type RepositoryVisibility = "public" | "private" | undefined; diff --git a/src/zod-validation.ts b/src/zod-validation.ts index 969c234b..75a7017d 100644 --- a/src/zod-validation.ts +++ b/src/zod-validation.ts @@ -4,7 +4,7 @@ import { CodeJSONSchema } from "./types/CodeJSONSchema.js"; z.config({ customError: createErrorMap({ - displayInvalidFormatDetails: true, + displayInvalidFormatDetails: true }), }); @@ -15,5 +15,6 @@ export function validateCodeJSON(codeJSON: unknown): string[] { return []; } - return [z.prettifyError(result.error)]; + return [z.prettifyError(result.error)] } +