diff --git a/.github/actions/check-codescanning-config/action.yml b/.github/actions/check-codescanning-config/action.yml index 0c65c3a41..cac79312e 100644 --- a/.github/actions/check-codescanning-config/action.yml +++ b/.github/actions/check-codescanning-config/action.yml @@ -54,16 +54,15 @@ runs: env: CODEQL_ACTION_TEST_MODE: 'true' - - name: Install dependencies - shell: bash - run: npm install --location=global ts-node js-yaml - - name: Check config - working-directory: ${{ github.action_path }} shell: bash env: EXPECTED_CONFIG_FILE_CONTENTS: '${{ inputs.expected-config-file-contents }}' - run: ts-node ./index.ts "$RUNNER_TEMP/user-config.yaml" "$EXPECTED_CONFIG_FILE_CONTENTS" + run: | + npx tsx ../action/pr-checks/check-cs-config.ts \ + --file "$RUNNER_TEMP/user-config.yaml" \ + --expected-contents "$EXPECTED_CONFIG_FILE_CONTENTS" + - name: Clean up shell: bash if: always() diff --git a/.github/actions/check-codescanning-config/index.ts b/.github/actions/check-codescanning-config/index.ts deleted file mode 100644 index ea99ca365..000000000 --- a/.github/actions/check-codescanning-config/index.ts +++ /dev/null @@ -1,49 +0,0 @@ - -import * as core from '@actions/core' -import * as yaml from 'js-yaml' -import * as fs from 'fs' -import * as assert from 'assert' - -const actualConfig = loadActualConfig() - -function sortConfigArrays(config) { - for (const key of Object.keys(config)) { - const value = config[key]; - if (key === 'queries' && Array.isArray(value)) { - config[key] = value.sort(); - } - } - return config; -} - -const rawExpectedConfig = process.argv[3].trim() -if (!rawExpectedConfig) { - core.setFailed('No expected configuration provided') -} else { - core.startGroup('Expected generated user config') - core.info(yaml.dump(JSON.parse(rawExpectedConfig))) - core.endGroup() -} - -const expectedConfig = rawExpectedConfig ? JSON.parse(rawExpectedConfig) : undefined; - -assert.deepStrictEqual( - sortConfigArrays(actualConfig), - sortConfigArrays(expectedConfig), - 'Expected configuration does not match actual configuration' -); - - -function loadActualConfig() { - if (!fs.existsSync(process.argv[2])) { - core.info('No configuration file found') - return undefined - } else { - const rawActualConfig = fs.readFileSync(process.argv[2], 'utf8') - core.startGroup('Actual generated user config') - core.info(rawActualConfig) - core.endGroup() - - return yaml.load(rawActualConfig) - } -} diff --git a/pr-checks/check-cs-config.test.ts b/pr-checks/check-cs-config.test.ts new file mode 100644 index 000000000..c2b470a73 --- /dev/null +++ b/pr-checks/check-cs-config.test.ts @@ -0,0 +1,68 @@ +/** + * Tests for `check-cs-config.ts`. + */ + +import * as assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import type { UserConfig } from "../src/config/db-config"; + +import { checkConfiguration } from "./check-cs-config"; + +describe("checkConfiguration", async () => { + await it("passes when actual and expected configs match", () => { + const actual: UserConfig = { name: "test-config", paths: ["src"] }; + const expected = JSON.stringify(actual); + assert.doesNotThrow(() => checkConfiguration(actual, expected)); + }); + + await it("passes when queries arrays match after sorting", () => { + const actual: UserConfig = { paths: ["b", "a", "c"] }; + const expected = JSON.stringify(actual); + assert.doesNotThrow(() => checkConfiguration(actual, expected)); + }); + + await it("throws when actual config does not match expected", () => { + const actual: UserConfig = { name: "actual-name" }; + const expected = JSON.stringify({ + name: "expected-name", + } satisfies UserConfig); + assert.throws(() => checkConfiguration(actual, expected), { + message: /Expected configuration does not match actual configuration/, + }); + }); + + await it("throws when expected contents are empty", () => { + assert.throws(() => checkConfiguration({}, ""), { + message: /No expected configuration provided/, + }); + }); + + await it("throws when expected contents are only whitespace", () => { + assert.throws(() => checkConfiguration({}, " "), { + message: /No expected configuration provided/, + }); + }); + + await it("passes with complex config", () => { + const actual: UserConfig = { + name: "complex", + "disable-default-queries": true, + paths: ["src", "lib"], + "paths-ignore": ["test"], + "threat-models": ["remote"], + }; + const expected = JSON.stringify(actual); + assert.doesNotThrow(() => checkConfiguration(actual, expected)); + }); + + await it("trims whitespace from expected contents before parsing", () => { + const actual: UserConfig = { name: "trimmed" }; + const expected = ` ${JSON.stringify(actual)} `; + assert.doesNotThrow(() => checkConfiguration(actual, expected)); + }); + + await it("passes when both configs are empty objects", () => { + assert.doesNotThrow(() => checkConfiguration({}, "{}")); + }); +}); diff --git a/pr-checks/check-cs-config.ts b/pr-checks/check-cs-config.ts new file mode 100755 index 000000000..f520fc403 --- /dev/null +++ b/pr-checks/check-cs-config.ts @@ -0,0 +1,100 @@ +#!/usr/bin/env npx tsx + +/** + * Checks the code scanning configuration file generated by the + * action to ensure it contains the expected contents + */ + +import * as assert from "node:assert"; +import * as fs from "node:fs"; +import { parseArgs } from "node:util"; + +import * as core from "@actions/core"; +import * as yaml from "yaml"; + +import type { UserConfig } from "../src/config/db-config"; + +import { getErrorMessage } from "./util"; + +function sortConfigArrays(config: UserConfig) { + for (const key of Object.keys(config)) { + const value = config[key]; + if (key === "queries" && Array.isArray(value)) { + config[key] = value.sort(); + } + } + return config; +} + +function loadActualConfig(configPath: string) { + if (!fs.existsSync(configPath)) { + throw new Error("No configuration file found"); + } else { + const rawActualConfig = fs.readFileSync(configPath, "utf8"); + core.startGroup("Actual generated user config"); + core.info(rawActualConfig); + core.endGroup(); + + return yaml.parse(rawActualConfig) as UserConfig; + } +} + +export function checkConfiguration( + actualConfig: UserConfig, + expectedContents: string, +) { + const rawExpectedConfig = expectedContents.trim(); + if (!rawExpectedConfig) { + throw new Error("No expected configuration provided"); + } + + const expectedConfig = JSON.parse(rawExpectedConfig) as UserConfig; + + core.startGroup("Expected generated user config"); + core.info(yaml.stringify(expectedConfig)); + core.endGroup(); + + assert.deepStrictEqual( + sortConfigArrays(actualConfig), + sortConfigArrays(expectedConfig), + "Expected configuration does not match actual configuration", + ); +} + +function main() { + const { values } = parseArgs({ + options: { + // The path of the configuration file to check. + file: { + type: "string", + }, + // The expected contents of the file. + "expected-contents": { + type: "string", + }, + }, + strict: true, + }); + + if (values.file === undefined) { + throw new Error("The '--file' input is required."); + } + if (values["expected-contents"] === undefined) { + throw new Error("The '--expected-contents' input is required."); + } + + const actualConfig = loadActualConfig(values.file); + + try { + checkConfiguration(actualConfig, values["expected-contents"]); + } catch (err) { + core.error(getErrorMessage(err)); + return -1; + } + + return 0; +} + +if (require.main === module) { + process.exit(main()); +}