From 569a44780558a3f72cfb36a4e77b1caab0e1144a Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Thu, 2 Oct 2025 12:28:12 -0400 Subject: [PATCH 1/8] added basic validation but needs testing --- package-lock.json | 13 +++++- package.json | 3 +- src/helper.ts | 31 +++++++++++++ src/main.ts | 90 +++++++++++++++++++++++-------------- src/validation.ts | 110 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 211 insertions(+), 36 deletions(-) create mode 100644 src/validation.ts diff --git a/package-lock.json b/package-lock.json index e555dadf..94ef0bba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "dependencies": { "@actions/core": "^1.11.1", "@octokit/action": "^7.0.0", - "@rollup/rollup-linux-x64-gnu": "*", - "octokit-plugin-create-pull-request": "^6.0.0" + "octokit-plugin-create-pull-request": "^6.0.0", + "zod": "^4.1.11" }, "devDependencies": { "@eslint/compat": "^1.2.6", @@ -10659,6 +10659,15 @@ "engines": { "node": ">= 14" } + }, + "node_modules/zod": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 17dfd582..ab1ef545 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "dependencies": { "@actions/core": "^1.11.1", "@octokit/action": "^7.0.0", - "octokit-plugin-create-pull-request": "^6.0.0" + "octokit-plugin-create-pull-request": "^6.0.0", + "zod": "^4.1.11" }, "devDependencies": { "@eslint/compat": "^1.2.6", diff --git a/src/helper.ts b/src/helper.ts index b95a3521..cf29ddb8 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -7,6 +7,7 @@ import { exec } from "child_process"; import { promisify } from "util"; import { CodeJSON, BasicRepoInfo } from "./model.js"; +import { validateCodeJSON } from "./validation.js"; const execAsync = promisify(exec); @@ -139,6 +140,36 @@ export async function getBaseBranch(): Promise { } } +//=============================================== +// Validation +//=============================================== +export async function validateOnly(): Promise { + try { + const codeJSON = await readJSON("/github/workspace/code.json"); + + if (!codeJSON) { + core.setFailed("code.json file not found, is empty, or contains invalid JSON syntax..."); + return; + } + + const validationErrors = validateCodeJSON(codeJSON); + + if (validationErrors.length > 0) { + const errorMessage = `code.json validation failed with ${validationErrors.length} error(s):\n\n${validationErrors.map((err, idx) => `${idx + 1}. ${err}`).join('\n')}`; + core.setFailed(errorMessage); + return; + } + + core.info("code.json is valid!"); + core.setOutput("validated", true); + + } catch (error) { + core.setFailed(`validation error: ${error}`); + } +} + +export { validateCodeJSON }; + //=============================================== // Data Handling //=============================================== diff --git a/src/main.ts b/src/main.ts index 34e5564d..fe795caa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -119,42 +119,66 @@ async function getMetaData( } export async function run(): Promise { - const currentCodeJSON = await helpers.readJSON("/github/workspace/code.json"); - const metaData = await getMetaData(currentCodeJSON); - let finalCodeJSON = {} as CodeJSON; - - if (currentCodeJSON) { - finalCodeJSON = { - ...baselineCodeJSON, - ...currentCodeJSON, - ...metaData, - }; - } else { - finalCodeJSON = { - ...baselineCodeJSON, - ...metaData, - }; - } + try { + const eventName = process.env.GITHUB_EVENT_NAME; + core.info(`Event name: ${eventName}`); - const baseBranchName = await helpers.getBaseBranch(); - const skipPR = core.getInput("SKIP_PR", { required: false }) === "true"; - const adminToken = core.getInput("ADMIN_TOKEN", { required: false }); + if (eventName === "pull_request") { + core.info("Detected pull_request event - validating only!"); + await helpers.validateOnly(); + return; + } - if (skipPR) { - if (!adminToken) { - core.warning("SKIP_PR is enabled but ADMIN_TOKEN is not provided."); - core.warning( - "Direct push requires a Personal Access Token with appropriate permissions.", - ); + const currentCodeJSON = await helpers.readJSON("/github/workspace/code.json"); + const metaData = await getMetaData(currentCodeJSON); + let finalCodeJSON = {} as CodeJSON; - core.info("Falling back to pull request creation"); - await helpers.sendPR(finalCodeJSON, baseBranchName); + if (currentCodeJSON) { + finalCodeJSON = { + ...baselineCodeJSON, + ...currentCodeJSON, + ...metaData, + }; + } else { + finalCodeJSON = { + ...baselineCodeJSON, + ...metaData, + }; + } + + core.info("Validating generated code.json before output"); + const validationErrors = helpers.validateCodeJSON(finalCodeJSON); + + if (validationErrors.length > 0) { + const errorMessage = `Generated code.json is invalid:\n\n${validationErrors.map((err, idx) => `${idx + 1}. ${err}`).join('\n')}`; + core.setFailed(errorMessage); + return; + } + + core.info("Generated code.json passed validation!"); + + const baseBranchName = await helpers.getBaseBranch(); + const skipPR = core.getInput("SKIP_PR", { required: false }) === "true"; + const adminToken = core.getInput("ADMIN_TOKEN", { required: false }); + + if (skipPR) { + if (!adminToken) { + core.warning("SKIP_PR is enabled but ADMIN_TOKEN is not provided."); + core.warning( + "Direct push requires a Personal Access Token with appropriate permissions.", + ); + + core.info("Falling back to pull request creation"); + await helpers.sendPR(finalCodeJSON, baseBranchName); + } else { + core.info("Attempting direct push to branch"); + await helpers.pushDirectlyWithFallback(finalCodeJSON, baseBranchName); + } } else { - core.info("Attempting direct push"); - await helpers.pushDirectlyWithFallback(finalCodeJSON, baseBranchName); + core.info("Creating pull request with updated code.json"); + await helpers.sendPR(finalCodeJSON, baseBranchName); } - } else { - core.info("Attempting pull request creation"); - await helpers.sendPR(finalCodeJSON, baseBranchName); + } catch (error) { + core.setFailed(`Action failed: ${error}`); } -} +} \ No newline at end of file diff --git a/src/validation.ts b/src/validation.ts new file mode 100644 index 00000000..026321fa --- /dev/null +++ b/src/validation.ts @@ -0,0 +1,110 @@ +import { z } from "zod"; + +const DateSchema = z.object({ + created: z.string().min(1, "created date is required"), + lastModified: z.string().min(1, "lastModified date is required"), + metaDataLastUpdated: z.string().min(1, "metaDataLastUpdated date is required"), +}); + +const ContactSchema = z.object({ + email: z.email("must be a valid email").min(1, "email is required"), + name: z.string().min(1, "name is required"), +}); + +const LicenseSchema = z.object({ + name: z.string().min(1, "license name is required"), + URL: z.url("license URL must be valid").min(1, "license URL is required"), +}); + +const PermissionsSchema = z.object({ + license: z.array(LicenseSchema).optional(), + licenses: z.array(LicenseSchema).optional(), + usageType: z.union([z.array(z.string()), z.string()]), + exemptionText: z.string(), +}).refine( + (data) => data.license || data.licenses, + { + message: "Either 'license' or 'licenses' array is required", + path: ["license"], + } +); + +const ReuseFrequencySchema = z.object({ + forks: z.number(), + clones: z.number().optional(), +}); + +const RelatedCodeSchema = z.object({ + name: z.string(), + URL: z.url(), + isGovernmentRepo: z.boolean(), +}); + +const ReusedCodeSchema = z.object({ + name: z.string(), + URL: z.url(), +}); + +const PartnerSchema = z.object({ + name: z.string(), + email: z.email(), +}); + +export const CodeJSONSchema = z.object({ + name: z.string().min(1, "name is required"), + version: z.string().optional(), + description: z.string().min(1, "description is required"), + longDescription: z.string(), + status: z.string().min(1, "status is required"), + permissions: PermissionsSchema, + organization: z.string().min(1, "organization is required"), + repositoryURL: z.url("must be a valid URL").min(1, "repositoryURL is required"), + repositoryHost: z.string(), + repositoryVisibility: z.string().min(1, "repositoryVisibility is required"), + homepageURL: z.string().optional(), + downloadURL: z.string().optional(), + disclaimerURL: z.string().optional(), + disclaimerText: z.string().optional(), + vcs: z.string(), + laborHours: z.number(), + reuseFrequency: ReuseFrequencySchema, + platforms: z.array(z.string()), + categories: z.array(z.string()), + softwareType: z.string(), + languages: z.array(z.string()).min(1, "at least one language is required"), + maintenance: z.string(), + contractNumber: z.array(z.string()), + SBOM: z.string(), + relatedCode: z.array(RelatedCodeSchema).optional(), + reusedCode: z.array(ReusedCodeSchema).optional(), + partners: z.array(PartnerSchema).optional(), + date: DateSchema, + tags: z.array(z.string()), + contact: ContactSchema, + feedbackMechanism: z.string().min(1, "feedbackMechanism is required"), + AIUseCaseID: z.string(), + localisation: z.boolean(), + repositoryType: z.string(), + userInput: z.boolean(), + fismaLevel: z.string(), + group: z.string(), + projects: z.array(z.string()), + systems: z.array(z.string()), + subsetInHealthcare: z.array(z.string()), + userType: z.array(z.string()), + maturityModelTier: z.number(), +}); + +export function validateCodeJSON(codeJSON: any): string[] { + const result = CodeJSONSchema.safeParse(codeJSON); + + if (result.success) { + return []; + } + + return result.error.issues.map((err: z.ZodIssue) => { + const path = err.path.join('.'); + const field = path || 'root'; + return `${field}: ${err.message}`; + }); +} \ No newline at end of file From 52fdad9f0cc3136b61af16c68fe80ad742cdbfe1 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Fri, 3 Oct 2025 10:07:29 -0400 Subject: [PATCH 2/8] changed workflow branch for testing --- .github/workflows/updateCodeJSON.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/updateCodeJSON.yml b/.github/workflows/updateCodeJSON.yml index 6cafbc1f..db78bd11 100644 --- a/.github/workflows/updateCodeJSON.yml +++ b/.github/workflows/updateCodeJSON.yml @@ -1,6 +1,10 @@ name: Update Code.json on: workflow_dispatch: + pull_request: + types: [opened, synchronize] + paths: + - 'code.json' permissions: contents: write @@ -18,7 +22,7 @@ jobs: - name: Update code.json id: generator - uses: DSACMS/automated-codejson-generator@main + uses: DSACMS/automated-codejson-generator@sachin/jsonValidationImplementation with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ADMIN_TOKEN: ${{ secrets.ADMIN_PAT }} From 9421230531fe2959aa9af930c16a7b803890923c Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Fri, 3 Oct 2025 10:13:34 -0400 Subject: [PATCH 3/8] changed pr switch --- .github/workflows/updateCodeJSON.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/updateCodeJSON.yml b/.github/workflows/updateCodeJSON.yml index db78bd11..d5c8628a 100644 --- a/.github/workflows/updateCodeJSON.yml +++ b/.github/workflows/updateCodeJSON.yml @@ -27,7 +27,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ADMIN_TOKEN: ${{ secrets.ADMIN_PAT }} BRANCH: "main" - SKIP_PR: "true" + SKIP_PR: "false" - name: Post update information run: | From e0686bd3d8eb71bc8a75030ae44a69e04b92b089 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Fri, 3 Oct 2025 10:36:10 -0400 Subject: [PATCH 4/8] changed to push --- .github/workflows/updateCodeJSON.yml | 10 ++++---- src/helper.ts | 7 +++--- src/main.ts | 13 ++++++---- src/validation.ts | 37 +++++++++++++++------------- 4 files changed, 37 insertions(+), 30 deletions(-) diff --git a/.github/workflows/updateCodeJSON.yml b/.github/workflows/updateCodeJSON.yml index d5c8628a..ca915d4c 100644 --- a/.github/workflows/updateCodeJSON.yml +++ b/.github/workflows/updateCodeJSON.yml @@ -1,10 +1,11 @@ name: Update Code.json on: workflow_dispatch: - pull_request: - types: [opened, synchronize] + push: + branches: + - "code-json-*" paths: - - 'code.json' + - "code.json" permissions: contents: write @@ -25,7 +26,7 @@ jobs: uses: DSACMS/automated-codejson-generator@sachin/jsonValidationImplementation with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ADMIN_TOKEN: ${{ secrets.ADMIN_PAT }} + ADMIN_TOKEN: ${{ secrets.ADMIN_PAT }} BRANCH: "main" SKIP_PR: "false" @@ -37,4 +38,3 @@ jobs: elif [ "${{ steps.generator.outputs.method_used }}" = "pull_request" ]; then echo "Created pull request: ${{ steps.generator.outputs.pr_url }}" fi - diff --git a/src/helper.ts b/src/helper.ts index cf29ddb8..0d6c0320 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -148,21 +148,22 @@ export async function validateOnly(): Promise { const codeJSON = await readJSON("/github/workspace/code.json"); if (!codeJSON) { - core.setFailed("code.json file not found, is empty, or contains invalid JSON syntax..."); + core.setFailed( + "code.json file not found, is empty, or contains invalid JSON syntax...", + ); return; } const validationErrors = validateCodeJSON(codeJSON); if (validationErrors.length > 0) { - const errorMessage = `code.json validation failed with ${validationErrors.length} error(s):\n\n${validationErrors.map((err, idx) => `${idx + 1}. ${err}`).join('\n')}`; + const errorMessage = `code.json validation failed with ${validationErrors.length} error(s):\n\n${validationErrors.map((err, idx) => `${idx + 1}. ${err}`).join("\n")}`; core.setFailed(errorMessage); return; } core.info("code.json is valid!"); core.setOutput("validated", true); - } catch (error) { core.setFailed(`validation error: ${error}`); } diff --git a/src/main.ts b/src/main.ts index fe795caa..b63986a6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -121,15 +121,18 @@ async function getMetaData( export async function run(): Promise { try { const eventName = process.env.GITHUB_EVENT_NAME; + const refName = process.env.GITHUB_REF; core.info(`Event name: ${eventName}`); - if (eventName === "pull_request") { - core.info("Detected pull_request event - validating only!"); + if (eventName === "push" && refName?.startsWith("refs/heads/code-json-")) { + core.info("Detected push to PR branch - validating only!"); await helpers.validateOnly(); return; } - const currentCodeJSON = await helpers.readJSON("/github/workspace/code.json"); + const currentCodeJSON = await helpers.readJSON( + "/github/workspace/code.json", + ); const metaData = await getMetaData(currentCodeJSON); let finalCodeJSON = {} as CodeJSON; @@ -150,7 +153,7 @@ export async function run(): Promise { const validationErrors = helpers.validateCodeJSON(finalCodeJSON); if (validationErrors.length > 0) { - const errorMessage = `Generated code.json is invalid:\n\n${validationErrors.map((err, idx) => `${idx + 1}. ${err}`).join('\n')}`; + const errorMessage = `Generated code.json is invalid:\n\n${validationErrors.map((err, idx) => `${idx + 1}. ${err}`).join("\n")}`; core.setFailed(errorMessage); return; } @@ -181,4 +184,4 @@ export async function run(): Promise { } catch (error) { core.setFailed(`Action failed: ${error}`); } -} \ No newline at end of file +} diff --git a/src/validation.ts b/src/validation.ts index 026321fa..c86fc498 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -3,7 +3,9 @@ import { z } from "zod"; const DateSchema = z.object({ created: z.string().min(1, "created date is required"), lastModified: z.string().min(1, "lastModified date is required"), - metaDataLastUpdated: z.string().min(1, "metaDataLastUpdated date is required"), + metaDataLastUpdated: z + .string() + .min(1, "metaDataLastUpdated date is required"), }); const ContactSchema = z.object({ @@ -16,18 +18,17 @@ const LicenseSchema = z.object({ URL: z.url("license URL must be valid").min(1, "license URL is required"), }); -const PermissionsSchema = z.object({ - license: z.array(LicenseSchema).optional(), - licenses: z.array(LicenseSchema).optional(), - usageType: z.union([z.array(z.string()), z.string()]), - exemptionText: z.string(), -}).refine( - (data) => data.license || data.licenses, - { +const PermissionsSchema = z + .object({ + license: z.array(LicenseSchema).optional(), + licenses: z.array(LicenseSchema).optional(), + usageType: z.union([z.array(z.string()), z.string()]), + exemptionText: z.string(), + }) + .refine((data) => data.license || data.licenses, { message: "Either 'license' or 'licenses' array is required", path: ["license"], - } -); + }); const ReuseFrequencySchema = z.object({ forks: z.number(), @@ -58,7 +59,9 @@ export const CodeJSONSchema = z.object({ status: z.string().min(1, "status is required"), permissions: PermissionsSchema, organization: z.string().min(1, "organization is required"), - repositoryURL: z.url("must be a valid URL").min(1, "repositoryURL is required"), + repositoryURL: z + .url("must be a valid URL") + .min(1, "repositoryURL is required"), repositoryHost: z.string(), repositoryVisibility: z.string().min(1, "repositoryVisibility is required"), homepageURL: z.string().optional(), @@ -97,14 +100,14 @@ export const CodeJSONSchema = z.object({ export function validateCodeJSON(codeJSON: any): string[] { const result = CodeJSONSchema.safeParse(codeJSON); - + if (result.success) { return []; } - + return result.error.issues.map((err: z.ZodIssue) => { - const path = err.path.join('.'); - const field = path || 'root'; + const path = err.path.join("."); + const field = path || "root"; return `${field}: ${err.message}`; }); -} \ No newline at end of file +} From 8f25c151fb039f0774a60972cf4e6fcee378add5 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Fri, 3 Oct 2025 11:10:32 -0400 Subject: [PATCH 5/8] changed back to on PR to include checks --- .github/workflows/updateCodeJSON.yml | 5 ++--- src/main.ts | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/updateCodeJSON.yml b/.github/workflows/updateCodeJSON.yml index ca915d4c..8ceae3d0 100644 --- a/.github/workflows/updateCodeJSON.yml +++ b/.github/workflows/updateCodeJSON.yml @@ -1,9 +1,8 @@ name: Update Code.json on: workflow_dispatch: - push: - branches: - - "code-json-*" + pull_request: + types: [opened, synchronize] paths: - "code.json" diff --git a/src/main.ts b/src/main.ts index b63986a6..b954635e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -121,11 +121,9 @@ async function getMetaData( export async function run(): Promise { try { const eventName = process.env.GITHUB_EVENT_NAME; - const refName = process.env.GITHUB_REF; - core.info(`Event name: ${eventName}`); - if (eventName === "push" && refName?.startsWith("refs/heads/code-json-")) { - core.info("Detected push to PR branch - validating only!"); + if (eventName === "pull_request") { + core.info("Detected pull_request event - validating only!"); await helpers.validateOnly(); return; } @@ -153,7 +151,7 @@ export async function run(): Promise { const validationErrors = helpers.validateCodeJSON(finalCodeJSON); if (validationErrors.length > 0) { - const errorMessage = `Generated code.json is invalid:\n\n${validationErrors.map((err, idx) => `${idx + 1}. ${err}`).join("\n")}`; + const errorMessage = `Generated code.json is invalid:\n${validationErrors.map((err, idx) => `${idx + 1}. ${err}`).join("\n")}`; core.setFailed(errorMessage); return; } From ff7cd1d4a2199f3e332bea36b058ca20a2ab2b82 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Fri, 3 Oct 2025 17:06:13 -0400 Subject: [PATCH 6/8] editied documentation --- CONTRIBUTING.md | 10 ++++++++++ README.md | 47 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c34ae375..3a466d42 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,16 @@ npm run package npm test ``` +## Validation + +The action uses [Zod](https://zod.dev/) for schema validation, automatically validating code.json in two scenarios: + +### 1. Before Generation +Every time the action generates or updates code.json (via schedule or workflow_dispatch), it validates the output before creating a PR or pushing. If validation fails, no changes are made. + +### 2. On PR Edits +When the `pull_request` trigger is configured, the action validates code.json whenever it's edited in a PR. This ensures users cannot accidentally merge invalid JSON. + ### Workflow and Branching We follow the [GitHub Flow Workflow](https://guides.github.com/introduction/flow/): diff --git a/README.md b/README.md index bf1ebdef..c4669d6a 100644 --- a/README.md +++ b/README.md @@ -33,26 +33,33 @@ ADMIN_TOKEN: ```yaml updated: description: "Boolean indicating whether code.json was updated" + pr_url: description: "URL of the created pull request if changes were made via PR" + commit_sha: description: "SHA of the commit if pushed directly to branch" + method_used: description: "Method used for the update: 'direct_push' or 'pull_request'" ``` ## Workflow Examples -### Option 1: Direct Push +### Option 1: Direct Push -This approach tries to push directly to the branch using a Personal Access Token, but falls back to creating a pull request if the direct push fails. +This approach tries to push directly to the branch using a Personal Access Token, but falls back to creating a pull request if the direct push fails. When users need to edit code.json, they should create a PR which will automatically validate their changes. ```yaml -name: Update Code.json (Smart Mode) +name: Update Code.json on: schedule: - cron: 0 0 1 * * # First day of every month workflow_dispatch: + pull_request: + types: [opened, synchronize] + paths: + - 'code.json' permissions: contents: write @@ -89,7 +96,7 @@ jobs: ### Option 2: Pull Request Only -This approach always creates a pull request, ensuring code review for all changes. +This approach always creates a pull request for both automatic generation and validation of manual edits, ensuring code review for all changes. ```yaml name: Update Code.json @@ -97,6 +104,10 @@ on: schedule: - cron: 0 0 1 * * # First day of every month workflow_dispatch: + pull_request: + types: [opened, synchronize] + paths: + - 'code.json' permissions: contents: write @@ -113,13 +124,27 @@ jobs: fetch-depth: 0 - name: Update code.json - uses: DSACMS/automated-codejson-generator@latest + uses: DSACMS/automated-codejson-generator@v1.2.0 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: "main" SKIP_PR: "false" ``` +### How It Works + +**Automatic Generation** +- The action calculates metadata, validates it, and creates a PR or pushes directly +- Validation ensures only valid code.json is created +- Users can then fill in manual fields by editing the PR + +**PR Validation** +- When users edit code.json in a PR, validation runs automatically on every commit +- The PR cannot be merged if validation fails (when branch protection is enabled) +- Error messages help users fix issues quickly + +**Important:** For direct push mode, users should always create PRs when manually editing code.json to ensure validation runs. Direct edits to the main branch will not be validated by this action. + ## Setting Up Personal Access Token (PAT) To use the direct push functionality, you'll need to create a Personal Access Token: @@ -128,14 +153,14 @@ To use the direct push functionality, you'll need to create a Personal Access To 1. **Go to GitHub Settings**: Navigate to your GitHub account settings 2. **Developer Settings**: Click on "Developer settings" in the left sidebar -3. **Personal Access Tokens**: Choose "Tokens (classic)" or "Fine-grained tokens" +3. **Personal Access Tokens**: Choose "Tokens (classic)" 4. **Generate New Token**: Click "Generate new token" 5. **Configure Token**: - - **Name**: Give it a descriptive name like "Code.json Generator" + - **Name**: Give it a name like "code.json Generator" - **Expiration**: Set appropriate expiration (recommend 90 days or 1 year) - **Scopes**: - - For classic tokens: Select `repo` (full repository access) - - For fine-grained tokens: Select `Contents` (write) and `Metadata` (read) + - Select `repo` (full repository access) +6. **Store Token**: Copy and paste your token and store it for the next part ### Adding PAT to Repository @@ -148,8 +173,7 @@ To use the direct push functionality, you'll need to create a Personal Access To 5. **Save**: Click "Add secret" ⚠️ _Please make sure the following are enabled within your Repository Action Settings in order to work properly_ ⚠️ -Screenshot 2025-08-05 at 1 44 36 PM - +Screenshot 2025-08-05 at 1 44 36 PM ## Generation Context @@ -213,6 +237,7 @@ An up-to-date list of core team members can be found in [MAINTAINERS.md](MAINTAI . ├── src/ │ ├── model.ts # TypeScript interfaces for code.json schema +│ ├── validation.ts # Zod schema definitions and validation logic │ ├── main.ts # Main action logic │ ├── helper.ts # Helper functions for GitHub API interactions │ └── index.ts # Action entrypoint From 790d9c39324093c26515105f7d3d532350c23890 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Wed, 8 Oct 2025 14:41:29 -0400 Subject: [PATCH 7/8] implemented requested changes --- CONTRIBUTING.md | 2 ++ README.md | 16 +++++++++------- code.json | 33 ++++++++------------------------- src/helper.ts | 4 ++-- src/main.ts | 8 ++++---- src/model.ts | 4 ++-- src/validation.ts | 32 ++++++++++++++++++++++++-------- 7 files changed, 51 insertions(+), 48 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a466d42..4d68bed7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,9 +43,11 @@ npm test The action uses [Zod](https://zod.dev/) for schema validation, automatically validating code.json in two scenarios: ### 1. Before Generation + Every time the action generates or updates code.json (via schedule or workflow_dispatch), it validates the output before creating a PR or pushing. If validation fails, no changes are made. ### 2. On PR Edits + When the `pull_request` trigger is configured, the action validates code.json whenever it's edited in a PR. This ensures users cannot accidentally merge invalid JSON. ### Workflow and Branching diff --git a/README.md b/README.md index c4669d6a..15022a87 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ method_used: ## Workflow Examples -### Option 1: Direct Push +### Option 1: Direct Push This approach tries to push directly to the branch using a Personal Access Token, but falls back to creating a pull request if the direct push fails. When users need to edit code.json, they should create a PR which will automatically validate their changes. @@ -59,7 +59,7 @@ on: pull_request: types: [opened, synchronize] paths: - - 'code.json' + - "code.json" permissions: contents: write @@ -80,7 +80,7 @@ jobs: uses: DSACMS/automated-codejson-generator@v1.2.0 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ADMIN_TOKEN: ${{ secrets.ADMIN_PAT }} # PAT with admin/push permissions + ADMIN_TOKEN: ${{ secrets.ADMIN_PAT }} # PAT with admin/push permissions BRANCH: "main" SKIP_PR: "true" @@ -107,7 +107,7 @@ on: pull_request: types: [opened, synchronize] paths: - - 'code.json' + - "code.json" permissions: contents: write @@ -128,17 +128,19 @@ jobs: with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: "main" - SKIP_PR: "false" + SKIP_PR: "false" ``` ### How It Works **Automatic Generation** + - The action calculates metadata, validates it, and creates a PR or pushes directly - Validation ensures only valid code.json is created - Users can then fill in manual fields by editing the PR **PR Validation** + - When users edit code.json in a PR, validation runs automatically on every commit - The PR cannot be merged if validation fails (when branch protection is enabled) - Error messages help users fix issues quickly @@ -158,7 +160,7 @@ To use the direct push functionality, you'll need to create a Personal Access To 5. **Configure Token**: - **Name**: Give it a name like "code.json Generator" - **Expiration**: Set appropriate expiration (recommend 90 days or 1 year) - - **Scopes**: + - **Scopes**: - Select `repo` (full repository access) 6. **Store Token**: Copy and paste your token and store it for the next part @@ -197,7 +199,7 @@ The automated code.json generator calculates specific fields by analyzing your r **dateLastModified**: This uses your repository's last update timestamp, reflecting the most recent changes. No configuration needed. -**dateMetaDataLastUpdated**: The generator sets this to the current timestamp each time it runs, providing a record of when the metadata was last refreshed. No configuration needed. +**dateMetadataLastUpdated**: The generator sets this to the current timestamp each time it runs, providing a record of when the metadata was last refreshed. No configuration needed. **feedbackMechanism**: The repository's issues URL in the format of {repositoryURL}/issues. If you already have a code.json file with existing feedback mechanisms, the generator preserves those values. No configuration needed. diff --git a/code.json b/code.json index 6c5a3fa5..470314a7 100644 --- a/code.json +++ b/code.json @@ -28,19 +28,10 @@ "forks": 2, "clones": 0 }, - "platforms": [ - "web", - "linux" - ], - "categories": [ - "developer-tools", - "automation" - ], + "platforms": ["web", "linux"], + "categories": ["developer-tools", "automation"], "softwareType": "standalone/backend", - "languages": [ - "TypeScript", - "JavaScript" - ], + "languages": ["TypeScript", "JavaScript"], "maintenance": "internal", "contractNumber": [], "SBOM": "https://github.com/DSACMS/automated-codejson-generator/network/dependencies", @@ -52,9 +43,7 @@ "lastModified": "2025-09-17T22:40:02Z", "metaDataLastUpdated": "2025-09-26T15:08:24.592Z" }, - "tags": [ - "cmsoss-tier2" - ], + "tags": ["cmsoss-tier2"], "contact": { "email": "opensource@cms.hhs.gov", "name": "CMS Open Source Team" @@ -66,15 +55,9 @@ "userInput": false, "fismaLevel": "Low", "group": "CMS/OA/DSAC", - "projects": [ - "SHARE IT Act" - ], + "projects": ["SHARE IT Act"], "systems": [], - "subsetInHealthcare": [ - "Operational" - ], - "userType": [ - "Government" - ], + "subsetInHealthcare": ["Operational"], + "userType": ["Government"], "maturityModelTier": 3 -} \ No newline at end of file +} diff --git a/src/helper.ts b/src/helper.ts index 0d6c0320..a09d8b39 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -67,7 +67,7 @@ export async function calculateMetaData(): Promise> { date: { created: basicInfo.date.created, lastModified: basicInfo.date.lastModified, - metaDataLastUpdated: basicInfo.date.metaDataLastUpdated, + metadataLastUpdated: basicInfo.date.metadataLastUpdated, }, }; } catch (error) { @@ -100,7 +100,7 @@ async function getBasicInfo(): Promise { date: { created: repoData.data.created_at, lastModified: repoData.data.updated_at, - metaDataLastUpdated: new Date().toISOString(), + metadataLastUpdated: new Date().toISOString(), }, }; } catch (error) { diff --git a/src/main.ts b/src/main.ts index b954635e..41e48858 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,7 @@ const baselineCodeJSON: CodeJSON = { longDescription: "", status: "", permissions: { - license: [ + licenses: [ { name: "", URL: "", @@ -45,7 +45,7 @@ const baselineCodeJSON: CodeJSON = { date: { created: "", lastModified: "", - metaDataLastUpdated: "", + metadataLastUpdated: "", }, tags: [], contact: { @@ -110,8 +110,8 @@ async function getMetaData( date: { created: partialCodeJSON.date?.created ?? "", lastModified: partialCodeJSON.date?.lastModified ?? "", - metaDataLastUpdated: - partialCodeJSON.date?.metaDataLastUpdated ?? new Date().toISOString(), + metadataLastUpdated: + partialCodeJSON.date?.metadataLastUpdated ?? new Date().toISOString(), }, feedbackMechanism, SBOM, diff --git a/src/model.ts b/src/model.ts index 5b3b8912..939d4dea 100644 --- a/src/model.ts +++ b/src/model.ts @@ -49,7 +49,7 @@ export interface ReuseFrequency { } export interface Permissions { - license: License[]; + licenses: License[]; usageType: string[]; exemptionText: string; } @@ -78,7 +78,7 @@ export interface Partner { export interface Date { created: string; lastModified: string; - metaDataLastUpdated: string; + metadataLastUpdated: string; } export interface Contact { diff --git a/src/validation.ts b/src/validation.ts index c86fc498..5add7008 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -3,9 +3,9 @@ import { z } from "zod"; const DateSchema = z.object({ created: z.string().min(1, "created date is required"), lastModified: z.string().min(1, "lastModified date is required"), - metaDataLastUpdated: z + metadataLastUpdated: z .string() - .min(1, "metaDataLastUpdated date is required"), + .min(1, "metadataLastUpdated date is required"), }); const ContactSchema = z.object({ @@ -20,15 +20,31 @@ const LicenseSchema = z.object({ const PermissionsSchema = z .object({ - license: z.array(LicenseSchema).optional(), - licenses: z.array(LicenseSchema).optional(), + licenses: z.array(LicenseSchema).min(1, "at least one license is required"), usageType: z.union([z.array(z.string()), z.string()]), exemptionText: z.string(), }) - .refine((data) => data.license || data.licenses, { - message: "Either 'license' or 'licenses' array is required", - path: ["license"], - }); + .refine( + (data) => { + const usageTypes = Array.isArray(data.usageType) + ? data.usageType + : [data.usageType]; + + const hasExemption = usageTypes.some( + (type) => typeof type === "string" && type.startsWith("exemptBy"), + ); + + if (hasExemption) { + return data.exemptionText && data.exemptionText.trim().length > 0; + } + + return true; + }, + { + message: "exemptionText is required when usageType contains an exemption", + path: ["exemptionText"], + }, + ); const ReuseFrequencySchema = z.object({ forks: z.number(), From e2fc1bdd747a1eee50a364a5d9ef954cb892b9d2 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Wed, 8 Oct 2025 15:16:53 -0400 Subject: [PATCH 8/8] fixed metadata field name --- code.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code.json b/code.json index 470314a7..54197696 100644 --- a/code.json +++ b/code.json @@ -41,7 +41,7 @@ "date": { "created": "2025-02-07T16:29:38Z", "lastModified": "2025-09-17T22:40:02Z", - "metaDataLastUpdated": "2025-09-26T15:08:24.592Z" + "metadataLastUpdated": "2025-09-26T15:08:24.592Z" }, "tags": ["cmsoss-tier2"], "contact": {