Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lint command #31

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions packages/@instana-integration/go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Below are the dashboards that are currently supported by this integration packag
|----------------------------|-----------------------|
| Go Runtime Metrics | Instana custom dashboard that displays runtime metrics for Go application |

## Go Runtime Metrics
## Metrics

### Semantic Conventions for Go Runtime Metrics
### Semantic Conventions

The Go runtime metrics are obtained by OpenTelemetry auto-instrumentation:

Expand Down
2 changes: 1 addition & 1 deletion packages/@instana-integration/go/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@instana-integration/go",
"version": "1.0.3",
"version": "1.0.4",
"description": "The integration package is used to support Go monitoring. Once you import this package into your Instana environment, you will be able to monitor Go runtime and the applications on various aspects by checking the dashboards, alerts, etc. included in this integration package.",
"author": "IBM",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions packages/@instana-integration/nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Below are the dashboards that are currently supported by this integration packag
|----------------------------|-----------------------|
| Node.js Runtime Metrics | Instana custom dashboard that displays runtime metrics for Node.js application |

## Node.js Runtime Metrics
## Metrics

### Semantic Conventions for Node.js Runtime Metrics
### Semantic Conventions

The Node.js runtime metrics are obtained by OpenTelemetry auto-instrumentation:

Expand Down
2 changes: 1 addition & 1 deletion packages/@instana-integration/nodejs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@instana-integration/nodejs",
"version": "1.0.1",
"version": "1.0.2",
"description": "The integration package is used to support Node.js monitoring. Once you import this package into your Instana environment, you will be able to monitor Node.js runtime and the applications on various aspects by checking the dashboards, alerts, etc. included in this integration package.",
"author": "IBM",
"license": "MIT",
Expand Down
36 changes: 34 additions & 2 deletions tools/integration/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions tools/integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"axios": "^1.7.2",
"glob": "^10.4.1",
"handlebars": "^4.7.8",
"semver": "^7.7.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"winston": "^3.13.0",
Expand All @@ -54,6 +55,7 @@
"devDependencies": {
"@types/glob": "^8.1.0",
"@types/node": "^20.14.2",
"@types/semver": "^7.5.8",
"@types/winston": "^2.4.4",
"@types/yargs": "^17.0.32"
}
Expand Down
209 changes: 208 additions & 1 deletion tools/integration/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import fs from 'fs';
import path from 'path';
import axios from 'axios';
import axios, { AxiosError } from 'axios';
import https from 'https';
import yargs from 'yargs';
import { globSync } from 'glob';
Expand All @@ -11,6 +11,7 @@ import logger from './logger'; // Import the logger
import { exec, spawn } from 'child_process';
import { promisify } from 'util';
import { input, checkbox, password, Separator } from '@inquirer/prompts';
import semver from 'semver';

const execAsync = promisify(exec);

Expand Down Expand Up @@ -107,6 +108,22 @@ const readPackageJson = (filePath: string) => {
}
};

// Helper function to check and read README.md
const readReadmeFile = (directoryPath: string) : string | null => {
const readmeFilePath = path.join(directoryPath, 'README.md');
try {
if(fs.existsSync(readmeFilePath)){
return fs.readFileSync(readmeFilePath, 'utf8');
} else {
logger.error('README.md is missing in the directory: ${directoryPath}');
return null;
}
} catch (error){
logger.error('Failed to read README.md.');
return null;
}
};

async function isUserLoggedIn() {
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
const npmrcPath = path.join(homeDir, '.npmrc');
Expand Down Expand Up @@ -271,11 +288,201 @@ yargs
demandOption: true
});
}, handlePublish)
.command('lint', 'provides linting for package', (yargs) => {
return yargs
.option('path', {
alias: 'p',
describe: 'The path to the package',
type: 'string',
demandOption: false
})
.option('strict-mode', {
alias: 's',
describe: 'Restricts the validations',
type: 'boolean',
demandOption: false
})
.option('debug', {
alias: 'd',
describe: 'Enable debug mode',
type: 'boolean',
default: false
});
}, handleLint)
.demandCommand(1, 'You need at least one command before moving on')
.help()
.alias('help', 'h')
.argv;

// Function to handle lint logic
async function handleLint(argv: any) {
const currentDirectory = process.cwd();
const errors: string[] = [];
const warnings: string[] = [];
const successMessages: string[] = [];

const readmeContent = readReadmeFile(currentDirectory);
const packageData = readPackageJson(currentDirectory);
const dashboardsPath = path.join(currentDirectory, 'dashboards');

// Check README file
if (readmeContent) {
try {
validateReadmeContent(readmeContent, packageData.name, errors, warnings, successMessages);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
errors.push(errorMessage);
}
} else {
errors.push('README.md is missing or empty.');
}
try {
const strictMode = argv['strict-mode'];
await validatePackageJson(packageData, errors, warnings, successMessages, strictMode);
validateDashboardFiles(dashboardsPath, errors, warnings, successMessages);
} catch (error) {
errors.push(`Linting failed: ${error}`);
}

// Check if debug is enabled
const isDebug = argv.debug;

if (isDebug) {
successMessages.forEach((message) => {
logger.info(message);
});
if (warnings.length > 0) {
logger.warn(`Warnings encountered during linting:`);
warnings.forEach((warning) => {
logger.warn(warning);
});
}
if (errors.length > 0) {
logger.error(`Linting failed with the following errors:`);
errors.forEach((error) => {
logger.error(error);
});
}
}

if (errors.length > 0) {
logger.error(`Linting failed.`);
process.exit(-1);
} else {
if (!isDebug) {
logger.info('Linting completed successfully.');
}
process.exit(0);
}
}

// Helper function to validate `package.json`
async function validatePackageJson(packageData: any, errors: string[], warnings: string[], successMessages: string[], strictMode: boolean): Promise<void> {
// Validate `name`
const namePattern = /^@instana-integration\/[a-zA-Z0-9-_]+$/;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rule is only a must if the package is officially maintained by IBM. User defined package does not need to follow it. We can report it as a warning instead of error. Or we can make it configurable (error vs. warning), e.g.: when run lint against observability-as-code repo, we should enable it as error, since the repo is used to host official package.

if (!namePattern.test(packageData.name)) {
const warningMessage = `Warning: Package name "${packageData.name}" does not align with the IBM package naming convention.`;
if(strictMode) {
errors.push(warningMessage);
} else {
warnings.push(warningMessage);
}
} else {
successMessages.push('Field "name" is valid.');
}

// Validate `version`
const versionPattern = /^\d+\.\d+\.\d+$/;
if (!versionPattern.test(packageData.version)) {
errors.push(`Invalid version "${packageData.version}". The version must follow the format "x.x.x".`);
} else {
successMessages.push('Field "version" format is valid.');
}

// Fetch the currently published version from npm and compare
try {
const response = await axios.get(`https://registry.npmjs.org/${packageData.name}`);
const publishedVersion = response.data['dist-tags']?.latest;

if (semver.eq(packageData.version, publishedVersion)) {
errors.push(`Package version "${packageData.version}" is the same as the currently published version "${publishedVersion}". It must be greater than the currently published version.`);
} else if (semver.lt(packageData.version, publishedVersion)) {
errors.push(`Invalid version "${packageData.version}". It must be greater than the currently published version "${publishedVersion}".`);
} else {
successMessages.push('Package version is valid and greater than the currently published version.');
}
} catch (error) {
errors.push(error instanceof Error ? error.message : String(error));
}

// Check for required fields and description
const requiredFields = ['name', 'version', 'author', 'license', 'description'];
for (const field of requiredFields) {
if (!packageData[field]) {
if (field === 'description') {
warnings.push('Warning: The "description" field is missing. Adding a description is recommended.');
} else {
errors.push(`Missing required field "${field}" in package.json.`);
}
} else {
successMessages.push(`Field "${field}" is present.`);
}
}
}

// Helper function to validate dashboard files
function validateDashboardFiles(dashboardsPath: string, errors: string[], warnings: string[], successMessages: string[]): void {
const files = fs.readdirSync(dashboardsPath);
const jsonFiles = files.filter(file => file.endsWith('.json'));

if (jsonFiles.length === 0) {
warnings.push('No JSON files found in the dashboards folder.');
return;
}

jsonFiles.forEach(file => {
const filePath = path.join(dashboardsPath, file);
try {
const fileContent = fs.readFileSync(filePath, 'utf-8');
const dashboard = JSON.parse(fileContent);
const globalAccessRule = {
accessType: 'READ_WRITE',
relationType: 'GLOBAL',
relatedId: ''
};
const globalAccessRuleExists = dashboard.accessRules?.some(
(rule: AccessRule) =>
rule.accessType === globalAccessRule.accessType &&
rule.relationType === globalAccessRule.relationType
);
if (!globalAccessRuleExists) {
errors.push(`Global access rule is missing in the dashboard file: ${file}.`);
} else {
successMessages.push(`Global access rule is correctly defined in the dashboard file: ${file}.`);
}
} catch (error) {
errors.push(`Error validating file ${file}: ${error instanceof Error ? error.message : String(error)}`);
}
});
}

// Helper function to validate README content
function validateReadmeContent(readmeContent: string, packageName: string, errors: string[], warnings: string[], successMessages: string[]): void {
const requiredSections = [
packageName,
'Dashboards',
'Metrics',
'Semantic Conventions'
];
const missingSections = requiredSections.filter(section => !readmeContent.includes(section));

if (missingSections.length > 0) {
errors.push(`README.md is missing required sections: ${missingSections.join(', ')}`);
} else {
successMessages.push('README.md contains all required sections.');
}
}

// Function to handle download logic
async function handleDownload(argv: any) {
const { package: packageName, location } = argv;
Expand Down