Skip to content

Commit

Permalink
ci: add node engine check (#7574)
Browse files Browse the repository at this point in the history
* add issue bot for prs

* Update CHANGELOG.md

* Update issue-bot.yml

* replace node 15 with node 16

* Update CHANGELOG.md

* use node 16 as default node version

* ignore node 15 in ci self-check

* bumped madge for node deprecation DEP0148

* ci: add node engine check

* lint

* bump node engine

* Update ci.yml

* revert unnecessary changes

* Update CHANGELOG.md

* Update ci.yml
  • Loading branch information
mtrezza authored Sep 14, 2021
1 parent 3e4d1ec commit e9e3be1
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 79 deletions.
14 changes: 8 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Cache Node.js modules
Expand All @@ -29,8 +29,10 @@ jobs:
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
- name: Install dependencies
run: npm ci
- name: CI Self-Check
- name: CI Environments Check
run: npm run ci:check
- name: CI Node Engine Check
run: npm run ci:checkNodeEngine
check-changelog:
name: Changelog
timeout-minutes: 5
Expand All @@ -45,7 +47,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Cache Node.js modules
Expand All @@ -65,7 +67,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Cache Node.js modules
Expand Down Expand Up @@ -159,7 +161,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.NODE_VERSION }}
- name: Cache Node.js modules
Expand Down Expand Up @@ -219,7 +221,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.NODE_VERSION }}
- name: Cache Node.js modules
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ ___
- Remove support for Node 10 which has reached its End-of-Life date (Manuel Trezza) [#7314](https://github.com/parse-community/parse-server/pull/7314)
- Remove S3 Files Adapter from Parse Server, instead install separately as `@parse/s3-files-adapter` (Manuel Trezza) [#7324](https://github.com/parse-community/parse-server/pull/7324)
- Remove Session field `restricted`; the field was a code artifact from a feature that never existed in Open Source Parse Server; if you have been using this field for custom purposes, consider that for new Parse Server installations the field does not exist anymore in the schema, and for existing installations the field default value `false` will not be set anymore when creating a new session (Manuel Trezza) [#7543](https://github.com/parse-community/parse-server/pull/7543)
- ci: add node engine version check (Manuel Trezza) [#7574](https://github.com/parse-community/parse-server/pull/7574)

### Notable Changes
- Added Parse Server Security Check to report weak security settings (Manuel Trezza, dblythy) [#7247](https://github.com/parse-community/parse-server/issues/7247)
Expand Down
File renamed without changes.
File renamed without changes.
190 changes: 190 additions & 0 deletions ci/nodeEngineCheck.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
const core = require('@actions/core');
const semver = require('semver');
const fs = require('fs').promises;
const path = require('path');

/**
* This checks whether any package dependency requires a minimum node engine
* version higher than the host package.
*/
class NodeEngineCheck {

/**
* The constructor.
* @param {Object} config The config.
* @param {String} config.nodeModulesPath The path to the node_modules directory.
* @param {String} config.packageJsonPath The path to the parent package.json file.
*/
constructor(config) {
const {
nodeModulesPath,
packageJsonPath,
} = config;

// Ensure required params are set
if ([
nodeModulesPath,
packageJsonPath,
].includes(undefined)) {
throw 'invalid configuration';
}

this.nodeModulesPath = nodeModulesPath;
this.packageJsonPath = packageJsonPath;
}

/**
* Returns an array of `package.json` files under the given path and subdirectories.
* @param {String} [basePath] The base path for recursive directory search.
*/
async getPackageFiles(basePath = this.nodeModulesPath) {
try {
// Declare file list
const files = []

// Get files
const dirents = await fs.readdir(basePath, { withFileTypes: true });
const validFiles = dirents.filter(d => d.name.toLowerCase() == 'package.json').map(d => path.join(basePath, d.name));
files.push(...validFiles);

// For each directory entry
for (const dirent of dirents) {
if (dirent.isDirectory()) {
const subFiles = await this.getPackageFiles(path.join(basePath, dirent.name));
files.push(...subFiles);
}
}
return files;
} catch (e) {
throw `Failed to get package.json files in ${this.nodeModulesPath} with error: ${e}`;
}
}

/**
* Extracts and returns the node engine versions of the given package.json
* files.
* @param {String[]} files The package.json files.
* @param {Boolean} clean Is true if packages with undefined node versions
* should be removed from the results.
* @returns {Object[]} A list of results.
*/
async getNodeVersion({ files, clean = false }) {

// Declare response
let response = [];

// For each file
for (const file of files) {

// Get node version
const contentString = await fs.readFile(file, 'utf-8');
const contentJson = JSON.parse(contentString);
const version = ((contentJson || {}).engines || {}).node;

// Add response
response.push({
file: file,
nodeVersion: version
});
}

// If results should be cleaned by removing undefined node versions
if (clean) {
response = response.filter(r => r.nodeVersion !== undefined);
}
return response;
}

/**
* Returns the highest semver definition that satisfies all versions
* in the given list.
* @param {String[]} versions The list of semver version ranges.
* @param {String} baseVersion The base version of which higher versions should be
* determined; as a version (1.2.3), not a range (>=1.2.3).
* @returns {String} The highest semver version.
*/
getHigherVersions({ versions, baseVersion }) {
// Add min satisfying node versions
const minVersions = versions.map(v => {
v.nodeMinVersion = semver.minVersion(v.nodeVersion)
return v;
});

// Sort by min version
const sortedMinVersions = minVersions.sort((v1, v2) => semver.compare(v1.nodeMinVersion, v2.nodeMinVersion));

// Filter by higher versions
const higherVersions = sortedMinVersions.filter(v => semver.gt(v.nodeMinVersion, baseVersion));
// console.log(`getHigherVersions: ${JSON.stringify(higherVersions)}`);
return higherVersions;
}

/**
* Returns the node version of the parent package.
* @return {Object} The parent package info.
*/
async getParentVersion() {
// Get parent package.json version
const version = await this.getNodeVersion({ files: [ this.packageJsonPath ], clean: true });
// console.log(`getParentVersion: ${JSON.stringify(version)}`);
return version[0];
}
}

async function check() {
// Define paths
const nodeModulesPath = path.join(__dirname, '../node_modules');
const packageJsonPath = path.join(__dirname, '../package.json');

// Create check
const check = new NodeEngineCheck({
nodeModulesPath,
packageJsonPath,
});

// Get package node version of parent package
const parentVersion = await check.getParentVersion();

// If parent node version could not be determined
if (parentVersion === undefined) {
core.setFailed(`Failed to determine node engine version of parent package at ${this.packageJsonPath}`);
return;
}

// Determine parent min version
const parentMinVersion = semver.minVersion(parentVersion.nodeVersion);

// Get package.json files
const files = await check.getPackageFiles();
core.info(`Checking the minimum node version requirement of ${files.length} dependencies`);

// Get node versions
const versions = await check.getNodeVersion({ files, clean: true });

// Get are dependencies that require a higher node version than the parent package
const higherVersions = check.getHigherVersions({ versions, baseVersion: parentMinVersion });

// Get highest version
const highestVersion = higherVersions.map(v => v.nodeMinVersion).pop();

// If there are higher versions
if (higherVersions.length > 0) {
console.log(`\nThere are ${higherVersions.length} dependencies that require a higher node engine version than the parent package (${parentVersion.nodeVersion}):`);

// For each dependency
for (const higherVersion of higherVersions) {

// Get package name
const _package = higherVersion.file.split('node_modules/').pop().replace('/package.json', '');
console.log(`- ${_package} requires at least node ${higherVersion.nodeMinVersion} (${higherVersion.nodeVersion})`);
}
console.log('');
core.setFailed(`❌ Upgrade the node engine version in package.json to at least '${highestVersion}' to satisfy the dependencies.`);
console.log('');
return;
}

console.log(`✅ All dependencies satisfy the node version requirement of the parent package (${parentVersion.nodeVersion}).`);
}

check();
Loading

0 comments on commit e9e3be1

Please sign in to comment.