Skip to content

Commit

Permalink
feat: add duplicate keys rule (#179)
Browse files Browse the repository at this point in the history
* feat: add duplicate keys rule

* test names

* update README.md

* fix main

* test

* chore: comitting generated dist

* fix

* chore: comitting generated dist

---------

Co-authored-by: danadajian <[email protected]>
  • Loading branch information
danadajian and danadajian authored May 25, 2023
1 parent ebc901b commit b1720d4
Show file tree
Hide file tree
Showing 13 changed files with 217 additions and 25 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,5 @@ jobs:
rules: |
ranges
tags
resolutions
keys
54 changes: 46 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

A Github Action for validating package.json conventions.

## Usage
## Rules

Use the `rules` input to specify one or more rules you would like to check for your `package.json`.

Expand All @@ -16,7 +16,7 @@ The following usage would allow `"my-package": "1.2.3"` but prevent `"my-package
```yaml
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3

- uses: ExpediaGroup/package-json-validator@v1
with:
Expand All @@ -27,7 +27,7 @@ You can also specify `allowed-ranges`. The following would allow `"my-package":
```yaml
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- uses: ExpediaGroup/package-json-validator@v1
with:
Expand All @@ -43,7 +43,7 @@ The following usage would allow `"my-package": "1.2.3"` but prevent `"my-package
```yaml
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- uses: ExpediaGroup/package-json-validator@v1
with:
Expand All @@ -55,19 +55,57 @@ The following usage would allow `"my-package": "1.2.3-canary.456.0"` but prevent
```yaml
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- uses: ExpediaGroup/package-json-validator@v1
with:
rules: tags
allowed-tags: canary
```

### Resolutions
The "resolutions" rule validates that your package.json does not contain the `resolutions` option.

```yaml
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: ExpediaGroup/package-json-validator@v1
with:
rules: resolutions
```

### Keys
The "keys" rule validates that your package.json does not contain duplicate dependency keys.

```yaml
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: ExpediaGroup/package-json-validator@v1
with:
rules: keys
```

Example invalid package.json this will prevent:
```json
{
"dependencies": {
"some-dependency": "1.0.0",
"some-dependency": "2.0.0"
}
}
```

## Other Inputs

Specify `dependency-types` to denote which type of package.json dependencies you wish to validate. Valid options include `dependencies`, `devDependencies`, `peerDependencies`, and `optionalDependencies`. Defaults to `dependencies`.
```yaml
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- uses: ExpediaGroup/package-json-validator@v1
with:
Expand All @@ -79,7 +117,7 @@ Specify `ignore-packages` to skip validation entirely for certain packages. Opti
```yaml
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- uses: ExpediaGroup/package-json-validator@v1
with:
Expand All @@ -92,7 +130,7 @@ You may also enforce multiple rules (and pass additional inputs) like this:
```yaml
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- uses: ExpediaGroup/package-json-validator@v1
with:
Expand Down
78 changes: 72 additions & 6 deletions dist/index.js

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

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

19 changes: 13 additions & 6 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,46 @@ import { readFileSync } from 'fs';
import { validateVersionRanges } from './rules/ranges';
import { validateVersionTags } from './rules/tags';
import { validateResolutions } from './rules/resolutions';
import { validateKeys } from './rules/keys';

type GithubError = {
status: number;
message: string;
};

const pathToPackageJson = './package.json';

export const RULES_MAP: {
[key: string]: {
method: Function;
extraInputName?: string;
extraInput?: string;
};
} = {
ranges: {
method: validateVersionRanges,
extraInputName: 'allowed-ranges'
extraInput: 'allowed-ranges'
},
tags: {
method: validateVersionTags,
extraInputName: 'allowed-tags'
extraInput: 'allowed-tags'
},
resolutions: {
method: validateResolutions
},
keys: {
method: validateKeys,
extraInput: pathToPackageJson
}
};

export const run = () => {
try {
const packageJson: PackageJson = JSON.parse(readFileSync('./package.json').toString());
const packageJson: PackageJson = JSON.parse(readFileSync(pathToPackageJson).toString());

const rules = core.getMultilineInput('rules', { required: true });
rules.forEach(rule => {
const { method, extraInputName } = RULES_MAP[rule];
method(packageJson, extraInputName);
const { method, extraInput } = RULES_MAP[rule];
method(packageJson, extraInput);
});
} catch (error) {
core.setFailed((error as GithubError).message);
Expand Down
28 changes: 28 additions & 0 deletions src/rules/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
Copyright 2021 Expedia, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import * as core from '@actions/core';
import { readFileSync } from 'fs';
import { PackageJson } from 'type-fest';
import { getDependencies } from '../utils/get-dependencies';

export const validateKeys = (packageJson: PackageJson, packageJsonPath: string) => {
const dependencies = getDependencies(packageJson);
Object.keys(dependencies).forEach(dependency => {
const stringifiedPackageJson = readFileSync(packageJsonPath).toString();
const regexMatches = stringifiedPackageJson.match(new RegExp(dependency, 'g'));
if (regexMatches && regexMatches.length > 1) {
core.setFailed(`Duplicate keys found in package.json: ${regexMatches}`);
}
});
};
2 changes: 1 addition & 1 deletion src/rules/resolutions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { PackageJson } from 'type-fest';
export const validateResolutions = (packageJson: PackageJson) => {
if (packageJson.resolutions) {
core.setFailed(
'Resolutions may not be set. Please investigate the root cause your dependency issues!'
'Resolutions may not be set. Please investigate the root cause of your dependency issues!'
);
}
};
9 changes: 9 additions & 0 deletions test/fixtures/deduped-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"dependencies": {
"some-dependency": "1.0.0",
"some-other-dependency": "2.0.0"
},
"devDependencies": {
"some-dev-dependency": "2.0.0"
}
}
6 changes: 6 additions & 0 deletions test/fixtures/duped-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"dependencies": {
"some-dependency": "1.0.0",
"some-dependency": "2.0.0"
}
}
9 changes: 9 additions & 0 deletions test/fixtures/duped-package2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"dependencies": {
"some-dependency": "1.0.0",
"some-other-dependency": "2.0.0"
},
"devDependencies": {
"some-dependency": "2.0.0"
}
}
4 changes: 2 additions & 2 deletions test/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ describe('main', () => {
})

it('should call correct methods', () => {
expect(validateVersionRanges).toHaveBeenCalledWith(packageJson, RULES_MAP.ranges.extraInputName)
expect(validateVersionTags).toHaveBeenCalledWith(packageJson, RULES_MAP.tags.extraInputName)
expect(validateVersionRanges).toHaveBeenCalledWith(packageJson, RULES_MAP.ranges.extraInput)
expect(validateVersionTags).toHaveBeenCalledWith(packageJson, RULES_MAP.tags.extraInput)
expect(validateResolutions).toHaveBeenCalledWith(packageJson, undefined)
});
})
Expand Down
26 changes: 26 additions & 0 deletions test/rules/keys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {validateKeys} from '../../src/rules/keys';
import * as core from '@actions/core';
import { getMultilineInput } from '@actions/core';
import dupedPackageJson from '../fixtures/duped-package.json';
import dupedPackageJson2 from '../fixtures/duped-package2.json';
import dedupedPackageJson from '../fixtures/deduped-package.json';

jest.mock('@actions/core');
(getMultilineInput as jest.Mock).mockReturnValue(['dependencies', 'devDependencies']);

describe('keys', () => {
it('should fail when package.json contains duplicate keys in dependencies', () => {
validateKeys(dupedPackageJson, 'test/fixtures/duped-package.json');
expect(core.setFailed).toHaveBeenCalled();
});

it('should fail when package.json contains duplicate keys across dependencies and devDependencies', () => {
validateKeys(dupedPackageJson2, 'test/fixtures/duped-package2.json');
expect(core.setFailed).toHaveBeenCalled();
});

it('should not fail when package.json contains no duplicate keys', () => {
validateKeys(dedupedPackageJson, 'test/fixtures/deduped-package.json');
expect(core.setFailed).not.toHaveBeenCalled();
});
});
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"sourceMap": true,
"moduleResolution": "node"
"moduleResolution": "node",
"resolveJsonModule": true
},
"exclude": [
"node_modules"
Expand Down

0 comments on commit b1720d4

Please sign in to comment.