Skip to content

Commit

Permalink
feat(upgrade-verify): check-issues executor
Browse files Browse the repository at this point in the history
  • Loading branch information
ziacik committed Oct 21, 2023
1 parent 948faa3 commit 9ee1512
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 12 deletions.
3 changes: 2 additions & 1 deletion packages/upgrade-verify/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.0.0] - 2023-10-16
## [1.0.0] - 2023-10-21

## Added

- `removeHashes` option added to the `verify-build` executor, with a default of `true`, which removes hashes from file names, making the comparisons more deterministic.
- `check-issues` executor added.

## [0.2.0] - 2023-10-15

Expand Down
66 changes: 55 additions & 11 deletions packages/upgrade-verify/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
# Nx Upgrade Verify Plugin

This plugin provides functionality to verify the build of a project after NX upgrade by comparing dist file statistics and detecting any significant differences.

On each run, the executor builds the project for each configuration from the build target. At the first run, the executor generates stats in the `.stats` directory of the project from the built files.

At every subsequent run, the executor compares the current saved stats with the new ones, writes out difference percentages to the output, and then updates the stats.

The stats can be committed to the repository for future use.

If the percentage differences cross a threshold of 10%, the executor will report a failure.
This plugin provides functionality to verify various aspects of a workspace after NX upgrade.

## Installation

Expand All @@ -20,7 +12,21 @@ npm install -D @ziacik/upgrade-verify

## Usage

Once the plugin is installed, you can use it as a custom executor in your project's configuration. Here's an example configuration:
Once the plugin is installed, you can use its executors in your project's configuration.

### verify-build executor

This executor provides functionality to verify the build of a project after NX upgrade by comparing dist file statistics and detecting any significant differences.

On each run, the executor builds the project for each configuration from the build target. At the first run, the executor generates stats in the `.stats` directory of the project from the built files.

At every subsequent run, the executor compares the current saved stats with the new ones, writes out difference percentages to the output, and then updates the stats.

The stats can be committed to the repository for future use.

If the percentage differences cross a threshold of 10%, the executor will report a failure.

#### Example configuration

```json
{
Expand All @@ -39,12 +45,50 @@ Once the plugin is installed, you can use it as a custom executor in your projec
}
```

Then to use the plugin, run
Then to use the executor, run

```bash
nx verify-build my-app
```

### check-issues executor

This executor serves as a watchdog for external issues tracked on github that are listed in workspace root's **ISSUES.md** file. This is useful when for example when a workaround has been introduced becaues of a bug, and we would like to remove that workaround when the issue is resolved.

When this executor is run, it takes all issues listed in the **ISSUES.md** file, check if they are still active and lists all that have already been closed. That allows us to take action and remove that issue from the list.

#### Example contents of `ISSUES.md`

```markdown
# Issues to be watched

https://github.com/ziacik/nx-tools/issues/1
[https://github.com/ziacik/nx-tools/issues/2](https://github.com/ziacik/nx-tools/issues/2)
[Some issue](https://github.com/ziacik/nx-tools/issues/3)
```

#### Example configuration

```json
{
"name": "my-app",
...
"targets": {
"check-issues": {
"executor": "@ziacik/upgrade-verify:check-issues"
},
...
},
...
}
```

Then to use the executor, run

```bash
nx check-issues my-app
```

## License

This project is licensed under the MIT License.
5 changes: 5 additions & 0 deletions packages/upgrade-verify/executors.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"implementation": "./src/executors/verify-build/executor",
"schema": "./src/executors/verify-build/schema.json",
"description": "Verifies that the build does not differ too much from the previous one after NX upgrade. Or, if this is the first run, creates stats for future verifications."
},
"check-issues": {
"implementation": "./src/executors/check-issues/executor",
"schema": "./src/executors/check-issues/schema.json",
"description": "Checks a list of issue links to see if they have been closed already."
}
}
}
3 changes: 3 additions & 0 deletions packages/upgrade-verify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,8 @@
".": {
"require": "./package.json"
}
},
"dependencies": {
"@nx/devkit": "16.10.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ExecutorContext, logger } from '@nx/devkit';
import * as fsPromises from 'fs/promises';
import executor from './executor';
import { CheckIssuesExecutorSchema } from './schema';

describe('CheckIssues Executor', () => {
let context: ExecutorContext;
let options: CheckIssuesExecutorSchema;

beforeEach(() => {
options = {};

globalThis.fetch = jest.fn().mockImplementation(async (url) => {
if (url === 'https://api.github.com/repos/ziacik/nx-tools/issues/1') {
return new Response(JSON.stringify({ state: 'closed' }));
} else if (url === 'https://api.github.com/repos/ziacik/nx-tools/issues/12') {
return new Response(JSON.stringify({ state: 'open' }));
} else if (url === 'https://api.github.com/repos/ziacik/nx-tools/issues/135') {
return new Response(JSON.stringify({ state: 'open' }));
} else {
throw new Error('Unexpected url');
}
});

jest.spyOn(logger, 'info').mockImplementation();
jest.spyOn(fsPromises, 'readFile').mockImplementation(async () => {
const error: NodeJS.ErrnoException = new Error('ENOENT');
error.code = 'ENOENT';
throw error;
});

context = createContext();
});

it('tries to read ISSUES.md file in the workspace root', async () => {
await executor(options, context);
expect(fsPromises.readFile).toHaveBeenCalledWith('/some/root/ISSUES.md', 'utf8');
});

it('just returns success if there is no ISSUES.md file in the workspace root', async () => {
const output = await executor(options, context);
expect(logger.info).toHaveBeenCalledWith("There is no 'ISSUES.md' file in the workspace root.");
expect(output.success).toBe(true);
});

it('tells that no issues have been closed if no issues have been closed', async () => {
jest.mocked(fsPromises.readFile).mockResolvedValue(`# Issues
https://github.com/ziacik/nx-tools/issues/12
[Some issue](https://github.com/ziacik/nx-tools/issues/135)
`);
const output = await executor(options, context);

expect(logger.info).not.toHaveBeenCalledWith('Issues which are closed now:');
expect(logger.info).toHaveBeenCalledWith('No issues have been closed.');

expect(output.success).toBe(true);
});

it('checks all github issues found in ISSUES.md file in workspace root', async () => {
jest.mocked(fsPromises.readFile).mockResolvedValue(`# Issues
[https://github.com/ziacik/nx-tools/issues/1](https://github.com/ziacik/nx-tools/issues/1)
https://github.com/ziacik/nx-tools/issues/12
[Some issue](https://github.com/ziacik/nx-tools/issues/135)
`);
const output = await executor(options, context);
expect(fetch).toHaveBeenCalledWith('https://api.github.com/repos/ziacik/nx-tools/issues/1');
expect(fetch).toHaveBeenCalledWith('https://api.github.com/repos/ziacik/nx-tools/issues/12');
expect(fetch).toHaveBeenCalledWith('https://api.github.com/repos/ziacik/nx-tools/issues/135');
expect(fetch).toHaveBeenCalledTimes(3);
expect(output.success).toBe(true);
});

it('lists all issues which are in the list and have become closed already', async () => {
jest.mocked(fsPromises.readFile).mockResolvedValue(`# Issues
[https://github.com/ziacik/nx-tools/issues/1](https://github.com/ziacik/nx-tools/issues/1)
https://github.com/ziacik/nx-tools/issues/12
[Some issue](https://github.com/ziacik/nx-tools/issues/135)
`);
const output = await executor(options, context);
expect(logger.info).toHaveBeenCalledWith('Issues which are closed now:');
expect(logger.info).toHaveBeenCalledWith('- [https://github.com/ziacik/nx-tools/issues/1](https://github.com/ziacik/nx-tools/issues/1)');
expect(logger.info).not.toHaveBeenCalledWith('- https://github.com/ziacik/nx-tools/issues/12');
expect(logger.info).not.toHaveBeenCalledWith('- https://github.com/ziacik/nx-tools/issues/135');
expect(logger.info).toHaveBeenCalledTimes(2);
expect(output.success).toBe(true);
});
});

function createContext(): ExecutorContext {
return {
root: '/some/root',
cwd: '.',
isVerbose: false,
projectName: 'my-project',
};
}
70 changes: 70 additions & 0 deletions packages/upgrade-verify/src/executors/check-issues/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ExecutorContext, logger } from '@nx/devkit';
import { readFile } from 'fs/promises';
import { join } from 'path';
import { CheckIssuesExecutorSchema } from './schema';

const GITHUB_ISSUE_REGEX = /https:\/\/github.com\/(?<owner>[^/]+)\/(?<repo>[^/]+)\/issues\/(?<issueNumber>\d+)/;

export default async function runExecutor(options: CheckIssuesExecutorSchema, context: ExecutorContext) {
const issuesMd = await tryLoadIssuesMd(context.root);

if (issuesMd != null) {
const issueMdLines = issuesMd.split('\n');
await printClosedIssueLines(issueMdLines);
} else {
logger.info("There is no 'ISSUES.md' file in the workspace root.");
}

return {
success: true,
};
}

async function printClosedIssueLines(issueMdLines: string[]) {
const resolvedLines = await Promise.all(issueMdLines.map(resolveIssueMdLine));
const closedLines = resolvedLines.filter((resolvedLine) => resolvedLine.result === 'closed');

if (closedLines.length > 0) {
logger.info('Issues which are closed now:');

for (const closedLine of closedLines) {
logger.info(`- ${closedLine.issueMdLine}`);
}
} else {
logger.info('No issues have been closed.');
}
}

async function resolveIssueMdLine(issueMdLine: string): Promise<{ issueMdLine: string; result: 'noissue' | 'active' | 'closed' }> {
const githubLink = issueMdLine.match(GITHUB_ISSUE_REGEX);

if (!githubLink) {
return { issueMdLine, result: 'noissue' };
}

const [, owner, repo, issueNumber] = githubLink;

const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`);
const { state } = await response.json();

return {
issueMdLine,
result: state === 'closed' ? 'closed' : 'active',
};
}

async function tryLoadIssuesMd(root: string): Promise<string | undefined> {
try {
return await readFile(join(root, 'ISSUES.md'), 'utf8');
} catch (e) {
if (isNodeError(e) && e.code === 'ENOENT') {
return undefined;
} else {
throw e;
}
}
}

function isNodeError(error: unknown): error is NodeJS.ErrnoException {
return error instanceof Error && 'code' in error;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export interface CheckIssuesExecutorSchema {} // eslint-disable-line
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/schema",
"version": 2,
"title": "CheckIssues executor",
"description": "",
"type": "object",
"properties": {},
"required": []
}

0 comments on commit 9ee1512

Please sign in to comment.