diff --git a/.github/actions/static-analysis/action.yml b/.github/actions/static-analysis/action.yml index b9dbe50..b57ac62 100644 --- a/.github/actions/static-analysis/action.yml +++ b/.github/actions/static-analysis/action.yml @@ -7,12 +7,23 @@ inputs: required: false default: "actions,VirdocsSoftware" + layer-package-json: + description: "Path to the layer package.json file" + required: true + + domains: + description: "Comma-separated list of domains to analyze" + required: true + runs: using: "composite" steps: - name: Checkout Code uses: actions/checkout@v4 + continue-on-error: true + - name: Scan workflow yml files + id: scan-workflows run: | echo "Running the following script: ${{ github.action_path }}/scan_github_actions.js" echo "With the current working directory: $(pwd)" @@ -20,4 +31,22 @@ runs: shell: bash working-directory: ${{ github.workspace }} env: - IGNORED_ACCOUNTS: ${{ inputs.ignored-accounts }} \ No newline at end of file + IGNORED_ACCOUNTS: ${{ inputs.ignored-accounts }} + continue-on-error: true + + - name: Run layer dependency analysis + id: layer-dependency-analysis + run: | + echo "Running layer dependency analysis script" + node ${{ github.action_path }}/layer_dependency_analysis.js "${{ inputs.layer-package-json }}" '${{ inputs.domains }}' + shell: bash + working-directory: ${{ github.workspace }} + continue-on-error: true + + - name: Check results + run: | + if [ "${{ steps.scan-workflows.outcome }}" != "success" ] || [ "${{ steps.layer-dependency-analysis.outcome }}" != "success" ]; then + echo "One or more steps failed" + exit 1 + fi + shell: bash \ No newline at end of file diff --git a/.github/actions/static-analysis/layer_dependency_analysis.js b/.github/actions/static-analysis/layer_dependency_analysis.js new file mode 100644 index 0000000..580bea9 --- /dev/null +++ b/.github/actions/static-analysis/layer_dependency_analysis.js @@ -0,0 +1,160 @@ +const fs = require('fs'); + +class PackageJsonDependencyComparator { + /** + * Compares dependencies between two package.json files + * @param {Object} packageJson1 - First package.json object + * @param {Object} packageJson2 - Second package.json object + * @returns {Object} Report containing mismatched dependencies + */ + compareDependencies(packageJson1, packageJson2) { + const report = { + mismatches: [], + missingInFirst: [], + missingInSecond: [] + }; + console.log('Comparing dependencies:', packageJson1, packageJson2); + + // Get all dependencies from both files + const deps1 = this._getAllDependencies(packageJson1); + const deps2 = this._getAllDependencies(packageJson2); + console.log('Dependencies:', deps1, deps2); + + // Compare dependencies + for (const [dep, version1] of Object.entries(deps1)) { + const version2 = deps2[dep]; + console.log('Comparing dependency:', dep, version1, version2); + + if (version2 === undefined) { + continue; // we don't care about missing dependencies + } else if (version1 !== version2) { + report.mismatches.push({ + dependency: dep, + version1, + version2 + }); + } + } + + // Find dependencies that exist only in second file + for (const [dep] of Object.entries(deps2)) { + if (deps1[dep] === undefined) { + continue; // we don't care about missing dependencies + } + } + + return report; + } + + /** + * Extracts all dependencies from a package.json object + * @param {Object} packageJson - package.json object + * @returns {Object} Combined dependencies object + */ + _getAllDependencies(packageJson) { + return { + ...packageJson.dependencies || {}, + ...packageJson.devDependencies || {}, + ...packageJson.peerDependencies || {}, + ...packageJson.optionalDependencies || {} + }; + } + + /** + * Formats the comparison report into a readable string + * @param {Object} report - Comparison report + * @returns {string} Formatted report + */ + formatReport(report) { + let parts = []; + + if (report.mismatches.length > 0) { + const mismatchItems = report.mismatches.map(({ dependency, version1, version2 }) => + `${dependency}:${version1}vs${version2}` + ).join(', '); + parts.push(`Mismatched against layer: [${mismatchItems}]`); + } + + if (report.missingInFirst.length > 0) { + const missingFirstItems = report.missingInFirst.map(({ dependency, version }) => + `${dependency}:${version}` + ).join(', '); + parts.push(`Missing in First: [${missingFirstItems}]`); + } + + if (report.missingInSecond.length > 0) { + const missingSecondItems = report.missingInSecond.map(({ dependency, version }) => + `${dependency}:${version}` + ).join(', '); + parts.push(`Missing in Second: [${missingSecondItems}]`); + } + + return parts.length > 0 ? parts.join(' | ') : 'No differences found between package.json files.'; + } +} + +class LayerDependencyAnalysis { + constructor(comparator) { + this.comparator = comparator; + } + + run(layerPackageJson, domainPackageJsons) { + console.log('Running layer dependency analysis'); + + const reports = domainPackageJsons.map(domainPackageJson => { + return { + project: domainPackageJson.project, + report: this.comparator.compareDependencies(layerPackageJson, domainPackageJson.packageJson) + }; + }); + + const reportsWithWarnings = reports.filter(report => report.report.mismatches.length > 0); + + if (reportsWithWarnings.length > 0) { + console.log('Reports with mismatched dependencies:'); + reportsWithWarnings.forEach(report => { + // output warning to github actions + console.log(`::warning file=${report.project}/package.json::${this.comparator.formatReport(report.report)}`); + }); + } else { + console.log('No mismatched dependencies found'); + } + } +} + +function main() { + if (process.argv.length < 4) { + console.error('Usage: node layer_dependency_analysis.js '); + process.exit(1); + } + + console.log('Layer package.json:', process.argv[2]); + console.log('Domains:', process.argv[3]); + console.log('Current working directory:', process.cwd()); + + // Example usage: + const comparator = new PackageJsonDependencyComparator(); + + const layerDependencyAnalysis = new LayerDependencyAnalysis(comparator); + + console.log('Reading layer package.json:', process.cwd() + '/' + process.argv[2]); + const layerPackageJson = JSON.parse(fs.readFileSync(process.cwd() + '/' + process.argv[2], 'utf8')); + const domains = JSON.parse(process.argv[3]); // {"include": [{"project": "domain1"}, {"project": "domain2"}]} + + const domainPackageJsons = domains.include.filter(domain => domain.project != '.').map(domain => { + return { + project: domain.project, + packageJson: JSON.parse(fs.readFileSync(process.cwd() + '/domains/' + domain.project + '/package.json', 'utf8')) + }; + }); + + // print the test plan + console.log('Test plan:'); + console.log('Layer package.json:', layerPackageJson); + console.log('Domains:', domains); + console.log('Domain package.json:', JSON.stringify(domainPackageJsons, null, 2)); + + layerDependencyAnalysis.run(layerPackageJson, domainPackageJsons); +} + +main(); \ No newline at end of file diff --git a/.github/actions/static-analysis/package.json b/.github/actions/static-analysis/package.json new file mode 100644 index 0000000..cc0704d --- /dev/null +++ b/.github/actions/static-analysis/package.json @@ -0,0 +1,4 @@ +{ + "name": "static-analysis", + "version": "1.0.0" +} diff --git a/.github/actions/static-analysis/scan_github_actions.js b/.github/actions/static-analysis/scan_github_actions.js index 6d298e6..34bcb5d 100644 --- a/.github/actions/static-analysis/scan_github_actions.js +++ b/.github/actions/static-analysis/scan_github_actions.js @@ -44,21 +44,45 @@ class StaticAnalysis { run() { const workflowDir = this.dataProvider.path.join(this.process.cwd(), '.github', 'workflows'); - if (!this.dataProvider.fileExists(workflowDir)) { - console.error(`Error: Directory ${workflowDir} does not exist.`); - this.process.exit(1); - } - - const yamlFiles = this.findYamlFiles(workflowDir); + const domainsDir = this.dataProvider.path.join(this.process.cwd(), 'domains'); let allWarnings = []; - yamlFiles.forEach(filePath => { - const warnings = this.scanFile(filePath); - allWarnings = allWarnings.concat(warnings); - }); + // Scan .github/workflows directory if it exists + if (this.dataProvider.fileExists(workflowDir)) { + const yamlFiles = this.findYamlFiles(workflowDir); + yamlFiles.forEach(filePath => { + const warnings = this.scanFile(filePath); + allWarnings = allWarnings.concat(warnings); + }); + } + + // Scan domains/*/.github/**/*.yml files if domains directory exists + if (this.dataProvider.fileExists(domainsDir)) { + const domains = this.dataProvider.readDirectory(domainsDir); + domains.forEach(domain => { + const domainPath = this.dataProvider.path.join(domainsDir, domain); + const domainGithubPath = this.dataProvider.path.join(domainPath, '.github'); + + if (this.dataProvider.fileExists(domainGithubPath)) { + const yamlFiles = this.findYamlFiles(domainGithubPath); + yamlFiles.forEach(filePath => { + const warnings = this.scanFile(filePath); + allWarnings = allWarnings.concat(warnings); + }); + } + }); + } if (allWarnings.length > 0) { - allWarnings.forEach(warning => console.warn(warning)); + allWarnings.forEach(warning => { + // Extract file path and line number if available + const fileMatch = warning.match(/In file (.*?),/); + const filePath = fileMatch ? fileMatch[1] : ''; + const relativePath = filePath ? this.dataProvider.path.relative(this.process.cwd(), filePath) : ''; + + // Output warning using GitHub's Workflow Commands + console.log(`::warning file=${relativePath}::${warning}`); + }); console.log('To fix these issues, refer to the solution in the following Jira ticket: https://virdocs.atlassian.net/browse/RD-2964'); this.process.exit(0); // TODO: Exit with non-zero code if warnings are found } else { diff --git a/package.json b/package.json index 7cee9c9..37cdc8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "github-actions", - "version": "2.12.0", + "version": "2.13.0", "description": "Used to store GitHub actions for use across the enterprise", "scripts": { "test": "./tooling/scripts/run_tests.sh",