Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6b99625
feat: 🎸 scan all workflow and action yml files
robaone-redshelf Apr 4, 2025
414485a
feat: 🎸 add layer dependency analysis javascript file
robaone-redshelf Apr 4, 2025
18a2023
feat: 🎸 print to github warnings
robaone-redshelf Apr 4, 2025
d4f18c2
feat: 🎸 parse and scan package.json files
robaone-redshelf Apr 4, 2025
1e1a49a
feat: 🎸 ignore projects that are '.'
robaone-redshelf Apr 4, 2025
67ba36f
feat: 🎸 use fils reader function
robaone-redshelf Apr 4, 2025
f98bade
feat: 🎸 import fs
robaone-redshelf Apr 4, 2025
dc8cb85
feat: 🎸 print the test plan
robaone-redshelf Apr 4, 2025
b161aa2
chore: 🤖 create main function and print debugging
robaone-redshelf Apr 4, 2025
4a7c45b
feat: 🎸 remove defaults
robaone-redshelf Apr 4, 2025
ed4d24f
feat: 🎸 open full path
robaone-redshelf Apr 4, 2025
ce005f5
fix: 🐛 remove file read
robaone-redshelf Apr 4, 2025
f5c85bd
fix: 🐛 call filter on the include property
robaone-redshelf Apr 4, 2025
2df12e1
fix: 🐛 call map on the include
robaone-redshelf Apr 4, 2025
78da695
chore: 🤖 add package.json
robaone-redshelf Apr 4, 2025
38b90b5
chore: 🤖 use require
robaone-redshelf Apr 4, 2025
59db839
feat: 🎸 only compare when they both exist
robaone-redshelf Apr 4, 2025
cb74dae
fix: 🐛 add debugging
robaone-redshelf Apr 4, 2025
1eed541
fix: 🐛 add more debugging
robaone-redshelf Apr 4, 2025
7eac74e
fix: 🐛 parse the json
robaone-redshelf Apr 4, 2025
3b602cc
fix: 🐛 put quotes around warning message
robaone-redshelf Apr 4, 2025
858431c
fix: 🐛 format report on one line
robaone-redshelf Apr 4, 2025
50e9b06
feat: 🎸 success
robaone-redshelf Apr 4, 2025
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