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

Use breaking-changes and momentOfTruth from @azure/rest-api-specs-scripts #5660

Merged
merged 7 commits into from
Apr 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 0 additions & 25 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,11 @@ services:
env:
matrix:
- MODE=branchStrategy
- MODE=syntax CHECK_NAME="Syntax Validator"
- MODE=semantic PR_ONLY=true CHECK_NAME="Semantic Validator"
- MODE=semantic PR_ONLY=false
- MODE=model PR_ONLY=true CHECK_NAME="Model Validator"
# - MODE=model PR_ONLY=false
- MODE=BreakingChange PR_ONLY=true CHECK_NAME="Breaking Changes"
- MODE=lintdiff PR_ONLY=true CHECK_NAME="Linter Diff" NODE_OPTIONS=--max-old-space-size=8192
matrix:
fast_finish: true
allow_failures:
- env: MODE=semantic PR_ONLY=false
- env: MODE=model PR_ONLY=false
- env: MODE=model PR_ONLY=true CHECK_NAME="Model Validator"
- env: MODE=BreakingChange PR_ONLY=true CHECK_NAME="Breaking Changes"
install: true
script:
Expand All @@ -28,23 +20,6 @@ script:
# Check to ensure CI is not executing for a PR against the master branch in the private repository
! [[ $TRAVIS_PULL_REQUEST != 'false' && $TRAVIS_REPO_SLUG == 'Azure/azure-rest-api-specs-pr' && $TRAVIS_BRANCH == 'master' ]]
fi
- >-
if [[ $MODE == 'syntax' ]]; then
npm install
npm test -- test/syntax.js
fi
- >-
if [[ $MODE == 'semantic' ]]; then
npm install
npm run tsc
node scripts/semanticValidation.js
fi
- >-
if [[ $MODE == 'model' ]]; then
npm install
npm run tsc
node scripts/modelValidation.js
fi
- >-
if [[ $MODE == 'BreakingChange' ]]; then
scripts/install-dotnet.sh
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"devDependencies": {
"@azure/avocado": "^0.3.3",
"@azure/oad": "^0.5.1",
"@azure/rest-api-specs-scripts": "^0.1.8",
"@microsoft.azure/async-io": "^2.0.21",
"@microsoft.azure/literate": "^1.0.25",
"@microsoft.azure/polyfill": "^1.0.19",
Expand Down
239 changes: 2 additions & 237 deletions scripts/breaking-change.ts
Original file line number Diff line number Diff line change
@@ -1,245 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License in the project root for license information.

import * as stringMap from '@ts-common/string-map'
import * as tsUtils from './ts-utils'
import * as utils from '../test/util/utils'
import * as path from 'path'
import * as fs from 'fs-extra'
import * as os from 'os'
import * as childProcess from 'child_process'
import * as oad from '@azure/oad'
import * as util from 'util'

const exec = util.promisify(childProcess.exec)

// This map is used to store the mapping between files resolved and stored location
var resolvedMapForNewSpecs: stringMap.MutableStringMap<string> = {};
let outputFolder = path.join(os.tmpdir(), "resolved");
// Used to enable running script outside TravisCI for debugging
let isRunningInTravisCI = process.env.TRAVIS === 'true';

const headerText = `
| | Rule | Location | Message |
|-|------|----------|---------|
`;

function iconFor(type: unknown) {
if (type === 'Error') {
return ':x:';
} else if (type === 'Warning') {
return ':warning:';
} else if (type === 'Info') {
return ':speech_balloon:';
} else {
return '';
}
}

function shortName(filePath: string) {
return `${path.basename(path.dirname(filePath))}/&#8203;<strong>${path.basename(filePath)}</strong>`;
}

type Diff = {
readonly type: unknown
readonly id: string
readonly code: unknown
readonly message: unknown
}

function tableLine(filePath: string, diff: Diff) {
return `|${iconFor(diff['type'])}|[${diff['type']} ${diff['id']} - ${diff['code']}](https://github.com/Azure/openapi-diff/blob/master/docs/rules/${diff['id']}.md)|[${shortName(filePath)}](${blobHref(filePath)} "${filePath}")|${diff['message']}|\n`;
}

function blobHref(file: unknown) {
return `https://github.com/${process.env.TRAVIS_PULL_REQUEST_SLUG}/blob/${process.env.TRAVIS_PULL_REQUEST_SHA}/${file}`;
}

/**
* Compares old and new specifications for breaking change detection.
*
* @param oldSpec Path to the old swagger specification file.
*
* @param newSpec Path to the new swagger specification file.
*/
async function runOad(oldSpec: string, newSpec: string) {
if (oldSpec === null || oldSpec === undefined || typeof oldSpec.valueOf() !== 'string' || !oldSpec.trim().length) {
throw new Error('oldSpec is a required parameter of type "string" and it cannot be an empty string.');
}

if (newSpec === null || newSpec === undefined || typeof newSpec.valueOf() !== 'string' || !newSpec.trim().length) {
throw new Error('newSpec is a required parameter of type "string" and it cannot be an empty string.');
}

console.log(`>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>`);
console.log(`Old Spec: "${oldSpec}"`);
console.log(`New Spec: "${newSpec}"`);
console.log(`>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>`);

let result = await oad.compare(oldSpec, newSpec, { consoleLogLevel: 'warn' });
console.log(result);

if (!result) {
return;
}

// fix up output from OAD, it does not output valid JSON
result = '[' + result.replace(/}\s+{/gi,"},{") + ']'

return JSON.parse(result);
}

/**
* Processes the given swagger and stores the resolved swagger on to disk
*
* @param swaggerPath Path to the swagger specification file.
*/
async function processViaAutoRest(swaggerPath: string) {
if (swaggerPath === null || swaggerPath === undefined || typeof swaggerPath.valueOf() !== 'string' || !swaggerPath.trim().length) {
throw new Error('swaggerPath is a required parameter of type "string" and it cannot be an empty string.');
}

const swaggerOutputFolder = path.join(outputFolder, path.dirname(swaggerPath));
const swaggerOutputFileNameWithoutExt = path.basename(swaggerPath, '.json');
const autoRestCmd = `autorest --input-file=${swaggerPath} --output-artifact=swagger-document.json --output-file=${swaggerOutputFileNameWithoutExt} --output-folder=${swaggerOutputFolder}`;

console.log(`Executing : ${autoRestCmd}`);

try {
await fs.ensureDir(swaggerOutputFolder);
await exec(`${autoRestCmd}`, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 64 });
resolvedMapForNewSpecs[swaggerPath] = path.join(swaggerOutputFolder, swaggerOutputFileNameWithoutExt + '.json');
} catch (err) {
console.log(`Error processing via AutoRest: ${err}`);
}
}

//main function
async function runScript() {
// See whether script is in Travis CI context
console.log(`isRunningInTravisCI: ${isRunningInTravisCI}`);

let targetBranch = utils.getTargetBranch();
let swaggersToProcess = utils.getFilesChangedInPR();

console.log('Processing swaggers:');
console.log(swaggersToProcess);

console.log('Finding new swaggers...')
let newSwaggers: unknown[] = [];
if (isRunningInTravisCI && swaggersToProcess.length > 0) {
newSwaggers = await utils.doOnBranch(utils.getTargetBranch(), async () => {
return swaggersToProcess.filter((s: string) => !fs.existsSync(s))
});
}

console.log('Processing via AutoRest...');
for (const swagger of swaggersToProcess) {
if (!newSwaggers.includes(swagger)) {
await processViaAutoRest(swagger);
}
}

console.log(`Resolved map for the new specifications:`);
console.dir(resolvedMapForNewSpecs);

let errors = 0, warnings = 0;
const diffFiles: stringMap.MutableStringMap<Diff[]> = {};
const newFiles = [];

for (const swagger of swaggersToProcess) {
// If file does not exists in the previous commits then we ignore it as it's new file
if (newSwaggers.includes(swagger)) {
console.log(`File: "${swagger}" looks to be newly added in this PR.`);
newFiles.push(swagger);
continue;
}

const resolved = resolvedMapForNewSpecs[swagger]
if (resolved) {
const diffs = await runOad(swagger, resolved);
if (diffs) {
diffFiles[swagger] = diffs;
for (const diff of diffs) {
if (diff['type'] === 'Error') {
if (errors === 0) {
console.log(`There are potential breaking changes in this PR. Please review before moving forward. Thanks!`);
process.exitCode = 1;
}
errors += 1;
} else if (diff['type'] === 'Warning') {
warnings += 1;
}
}
}
}
}

if (isRunningInTravisCI) {
let summary = '';
if (errors > 0) {
summary += '**There are potential breaking changes in this PR. Please review before moving forward. Thanks!**\n\n';
}
summary += `Compared to the target branch (**${targetBranch}**), this pull request introduces:\n\n`;
summary += `&nbsp;&nbsp;&nbsp;${errors > 0 ? iconFor('Error') : ':white_check_mark:'}&nbsp;&nbsp;&nbsp;**${errors}** new error${errors !== 1 ? 's' : ''}\n\n`;
summary += `&nbsp;&nbsp;&nbsp;${warnings > 0 ? iconFor('Warning') : ':white_check_mark:'}&nbsp;&nbsp;&nbsp;**${warnings}** new warning${warnings !== 1 ? 's' : ''}\n\n`;

let message = '';
if (newFiles.length > 0) {
message += '### The following files look to be newly added in this PR:\n';
newFiles.sort();
for (const swagger of newFiles) {
message += `* [${swagger}](${blobHref(swagger)})\n`;
}
message += '<br><br>\n';
}

const diffFileNames = Object.keys(diffFiles);
if (diffFileNames.length > 0) {
message += '### OpenAPI diff results\n';
message += headerText;

diffFileNames.sort();
for (const swagger of diffFileNames) {
const diffs = tsUtils.asNonUndefined(diffFiles[swagger]);
diffs.sort((a, b) => {
if (a.type === b.type) {
return a.id.localeCompare(b.id);
} else if (a.type === "Error") {
return 1;
} else if (b.type === "Error") {
return -1;
} else if (a.type === "Warning") {
return 1;
} else {
return -1;
}
});

for (const diff of diffs) {
message += tableLine(swagger, diff);
}
}
} else {
message += '**There were no files containing new errors or warnings.**\n';
}

message += '\n<br><br>\nThanks for using breaking change tool to review.\nIf you encounter any issue(s), please open issue(s) at https://github.com/Azure/openapi-diff/issues.';

const output = {
title: `${errors === 0 ? 'No' : errors} potential breaking change${errors !== 1 ? 's' : ''}`,
summary,
text: message
};

console.log('---output');
console.log(JSON.stringify(output));
console.log('---');
}
}
import * as scripts from '@azure/rest-api-specs-scripts'

// magic starts here
runScript().then(() => {
scripts.breakingChange().then(() => {
console.log(`Thanks for using breaking change tool to review.`);
console.log(`If you encounter any issue(s), please open issue(s) at https://github.com/Azure/openapi-diff/issues .`);
}).catch(err => {
Expand Down
Loading