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
31 changes: 30 additions & 1 deletion .github/actions/static-analysis/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,46 @@ 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)"
node ${{ github.action_path }}/scan_github_actions.js
shell: bash
working-directory: ${{ github.workspace }}
env:
IGNORED_ACCOUNTS: ${{ inputs.ignored-accounts }}
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
160 changes: 160 additions & 0 deletions .github/actions/static-analysis/layer_dependency_analysis.js
Original file line number Diff line number Diff line change
@@ -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 <layer-package-json> <domains>');
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();
4 changes: 4 additions & 0 deletions .github/actions/static-analysis/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "static-analysis",
"version": "1.0.0"
}
46 changes: 35 additions & 11 deletions .github/actions/static-analysis/scan_github_actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down