Breaking change configurable (#77)
* Add breaking change filter * send labels if message type is error * adding change log
This commit is contained in:
Родитель
ef4aedacb9
Коммит
c366e859b7
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## 0.14.0
|
||||
|
||||
- Support configuring breaking change rules base on config.
|
||||
- Adding details for lintDiff & oad details in unified pipeline report.
|
||||
|
||||
## 0.13.4
|
||||
|
||||
- Upgrade oav to 0.22.9 with rule change in response in case of both x-ms-secret and required are annotated.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@azure/rest-api-specs-scripts",
|
||||
"version": "0.13.4",
|
||||
"version": "0.14.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -101,9 +101,9 @@
|
|||
}
|
||||
},
|
||||
"@azure/swagger-validation-common": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@azure/swagger-validation-common/-/swagger-validation-common-0.0.3.tgz",
|
||||
"integrity": "sha512-do3uVDob4fTveCxOI1CojhXaMvI5zsEX9YRRIMqGuT3ccXXvge1C1x+2VSoa6WjGCmwetVMBG/anrFv4obtEkA=="
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@azure/swagger-validation-common/-/swagger-validation-common-0.1.2.tgz",
|
||||
"integrity": "sha512-QjmSpAliTzc77WTCnm3+zS2qVGv5U9/2h2y1ICZKQp5+0JSMcy3dPbS9WiKKkpsGQ2b1BJbDugJqKVg6a+8AeA=="
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.10.4",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@azure/rest-api-specs-scripts",
|
||||
"version": "0.13.4",
|
||||
"version": "0.14.0",
|
||||
"description": "Scripts for the Azure RestAPI specification repository 'azure-rest-api-specs'.",
|
||||
"types": "dist/index.d.ts",
|
||||
"main": "dist/index.js",
|
||||
|
@ -45,7 +45,7 @@
|
|||
"@azure/avocado": "^0.4.1",
|
||||
"@azure/oad": "^0.8.1",
|
||||
"@ts-common/string-map": "^0.3.0",
|
||||
"@azure/swagger-validation-common": "^0.0.3",
|
||||
"@azure/swagger-validation-common": "^0.1.2",
|
||||
"commonmark": "0.27.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"glob": "^7.1.3",
|
||||
|
|
|
@ -10,6 +10,8 @@ import { targetHref } from "./utils";
|
|||
import * as utils from "./utils";
|
||||
import { glob } from 'glob';
|
||||
import { getVersionFromInputFile } from './readmeUtils';
|
||||
import { ruleManager } from './breakingChangeRuleManager'
|
||||
import { UnifiedPipeLineStore, oadTracer } from './unifiedPipelineHelper';
|
||||
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License. See License in the project root for license information.
|
||||
|
@ -84,7 +86,7 @@ function blobHref(file: string) {
|
|||
*
|
||||
* @param newSpec Path to the new swagger specification file.
|
||||
*/
|
||||
async function runOad(oldSpec: string, newSpec: string , isCrossVersion = false) {
|
||||
async function runOad(oldSpec: string, newSpec: string) {
|
||||
if (
|
||||
oldSpec === null ||
|
||||
oldSpec === undefined ||
|
||||
|
@ -113,55 +115,14 @@ async function runOad(oldSpec: string, newSpec: string , isCrossVersion = false)
|
|||
console.log(`>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>`);
|
||||
|
||||
let result = await oad.compare(oldSpec, newSpec, { consoleLogLevel: "warn" });
|
||||
let oadResult = JSON.parse(result) as OadMessage[];
|
||||
|
||||
const pipelineResultData: format.ResultMessageRecord[] = oadResult.map(
|
||||
(it) => ({
|
||||
type: "Result",
|
||||
level: isCrossVersion ? "Warning" : it.type as format.MessageLevel,
|
||||
message: it.message,
|
||||
code: it.code,
|
||||
id: it.id,
|
||||
docUrl: it.docUrl,
|
||||
time: new Date(),
|
||||
extra: {
|
||||
mode: it.mode,
|
||||
},
|
||||
paths: [
|
||||
{
|
||||
tag: "New",
|
||||
path: blobHref(
|
||||
utils.getGithubStyleFilePath(
|
||||
utils.getRelativeSwaggerPathToRepo(it.new.location || "")
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
tag: "Old",
|
||||
path: targetHref(
|
||||
utils.getGithubStyleFilePath(
|
||||
utils.getRelativeSwaggerPathToRepo(it.old.location || "")
|
||||
)
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
const pipelineResult: format.MessageLine = pipelineResultData;
|
||||
|
||||
console.log("Write to pipe.log");
|
||||
fs.appendFileSync("pipe.log", JSON.stringify(pipelineResult) + "\n");
|
||||
|
||||
console.log(JSON.parse(result));
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
// fix up output from OAD, it does not output valid JSON
|
||||
result = result.replace(/}\s+{/gi, "},{");
|
||||
|
||||
return JSON.parse(result);
|
||||
let oadResult = JSON.parse(result) as OadMessage[];
|
||||
oadTracer.add(utils.getRelativeSwaggerPathToRepo(oldSpec),newSpec)
|
||||
|
||||
console.log(JSON.parse(result));
|
||||
return oadResult
|
||||
}
|
||||
|
||||
type SwaggerVersionType = "preview" | "stable";
|
||||
|
@ -279,20 +240,28 @@ export class SwaggerVersionManager {
|
|||
}
|
||||
|
||||
export class CrossVersionBreakingDetector {
|
||||
swaggers :string[]= []
|
||||
pr : devOps.PullRequestProperties
|
||||
versionManager: SwaggerVersionManager = new SwaggerVersionManager()
|
||||
constructor(pullRequest: devOps.PullRequestProperties,newSwaggers:string[]) {
|
||||
this.swaggers = newSwaggers
|
||||
this.pr = pullRequest
|
||||
swaggers: string[] = [];
|
||||
pr: devOps.PullRequestProperties;
|
||||
versionManager: SwaggerVersionManager = new SwaggerVersionManager();
|
||||
unifiedStore = new UnifiedPipeLineStore("");
|
||||
constructor(
|
||||
pullRequest: devOps.PullRequestProperties,
|
||||
newSwaggers: string[]
|
||||
) {
|
||||
this.swaggers = newSwaggers;
|
||||
this.pr = pullRequest;
|
||||
}
|
||||
|
||||
async diffOne(oldSpec: string ,newSpec : string) {
|
||||
async diffOne(oldSpec: string, newSpec: string) {
|
||||
try {
|
||||
await runOad(path.resolve(this.pr!.workingDir, oldSpec), newSpec);
|
||||
}
|
||||
catch(e) {
|
||||
const errors = []
|
||||
const oadResult = await runOad(
|
||||
path.resolve(this.pr!.workingDir, oldSpec),
|
||||
newSpec
|
||||
);
|
||||
const filterResult = ruleManager.handleCrossApiVersion(oadResult);
|
||||
this.unifiedStore.appendOadViolation(filterResult);
|
||||
} catch (e) {
|
||||
const errors = [];
|
||||
errors.push({
|
||||
error: e,
|
||||
old: targetHref(
|
||||
|
@ -302,28 +271,34 @@ export class CrossVersionBreakingDetector {
|
|||
),
|
||||
new: blobHref(utils.getRelativeSwaggerPathToRepo(newSpec)),
|
||||
});
|
||||
appendException(errors)
|
||||
appendException(errors);
|
||||
}
|
||||
}
|
||||
|
||||
async checkBreakingChangeBaseOnPreviewVersion() {
|
||||
for (const swagger of this.swaggers) {
|
||||
const previous = await utils.doOnTargetBranch(this.pr, async () => {
|
||||
return this.versionManager.getClosestPreview(swagger)
|
||||
})
|
||||
return this.versionManager.getClosestPreview(swagger);
|
||||
});
|
||||
if (previous) {
|
||||
await this.diffOne(path.resolve(this.pr!.workingDir,previous), swagger);
|
||||
await this.diffOne(
|
||||
path.resolve(this.pr!.workingDir, previous),
|
||||
swagger
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkBreakingChangeBaseOnStableVersion() {
|
||||
for (const swagger of this.swaggers) {
|
||||
const previous = await utils.doOnTargetBranch(this.pr, async () => {
|
||||
return this.versionManager.getClosestStale(swagger);
|
||||
});
|
||||
const previous = await utils.doOnTargetBranch(this.pr, async () => {
|
||||
return this.versionManager.getClosestStale(swagger);
|
||||
});
|
||||
if (previous) {
|
||||
await this.diffOne(path.resolve(this.pr!.workingDir,previous), swagger);
|
||||
await this.diffOne(
|
||||
path.resolve(this.pr!.workingDir, previous),
|
||||
swagger
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -356,6 +331,8 @@ export async function runCrossVersionBreakingChangeDetection(type:SwaggerVersion
|
|||
else {
|
||||
detector.checkBreakingChangeBaseOnStableVersion()
|
||||
}
|
||||
oadTracer.save()
|
||||
ruleManager.addBreakingChangeLabels()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -390,6 +367,7 @@ function changeTargetBranch(pr: devOps.PullRequestProperties | undefined) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
//main function
|
||||
export async function runScript() {
|
||||
console.log(`ENV: ${JSON.stringify(process.env)}`);
|
||||
|
@ -422,7 +400,7 @@ export async function runScript() {
|
|||
const newFiles = [];
|
||||
|
||||
const errors: RuntimeError[] = [];
|
||||
|
||||
const unifiedStore = new UnifiedPipeLineStore("")
|
||||
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)) {
|
||||
|
@ -437,9 +415,11 @@ export async function runScript() {
|
|||
swagger // Since the swagger resolving will be done at the oad , here to ensure the position output is consistent with the origin swagger,do not use the resolved swagger
|
||||
);
|
||||
if (diffs) {
|
||||
diffFiles[swagger] = diffs;
|
||||
for (const diff of diffs) {
|
||||
if (diff["type"] === "Error") {
|
||||
const filterDiffs = ruleManager.handleSameApiVersion(diffs);
|
||||
unifiedStore.appendOadViolation(filterDiffs);
|
||||
diffFiles[swagger] = filterDiffs;
|
||||
for (const diff of filterDiffs) {
|
||||
if (diff["type"].toLowerCase() === "error") {
|
||||
if (errorCnt === 0) {
|
||||
console.log(
|
||||
`There are potential breaking changes in this PR. Please review before moving forward. Thanks!`
|
||||
|
@ -464,6 +444,8 @@ export async function runScript() {
|
|||
});
|
||||
}
|
||||
}
|
||||
oadTracer.save();
|
||||
ruleManager.addBreakingChangeLabels();
|
||||
|
||||
if (errors.length > 0) {
|
||||
process.exitCode = 1;
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
import * as fs from "fs-extra";
|
||||
import * as yaml from "js-yaml";
|
||||
import { OadMessage } from './breaking-change';
|
||||
import { exception } from 'console';
|
||||
import { sendLabels } from "@azure/swagger-validation-common";
|
||||
import { devOps } from '@azure/avocado';
|
||||
import * as path from "path";
|
||||
|
||||
type OverrideBody = string | {from:string,to:string}[]
|
||||
|
||||
interface BreakingChangeRule {
|
||||
appliedTo: string;
|
||||
override?: { code?: OverrideBody , message?:OverrideBody , type?:OverrideBody};
|
||||
directive?: {addingLabels:string[]}
|
||||
}
|
||||
|
||||
interface BreakingChangeScenario {
|
||||
Scenario: string;
|
||||
rules: BreakingChangeRule[];
|
||||
}
|
||||
|
||||
interface RuleConfig<T> {
|
||||
load(configPath:string):boolean
|
||||
getConfig(sectionName: string): Map<string,T> | undefined
|
||||
}
|
||||
|
||||
class LocalRuleConfig implements RuleConfig<BreakingChangeRule> {
|
||||
private AllConfig: BreakingChangeScenario[] | undefined;
|
||||
load(configPath: string): boolean {
|
||||
try {
|
||||
const config = fs.readFileSync(configPath).toString();
|
||||
this.AllConfig = yaml.safeLoad(config) as BreakingChangeScenario[];
|
||||
return !!this.AllConfig;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
getConfig(scenarioName: string): Map<string, BreakingChangeRule> | undefined {
|
||||
if (this.AllConfig) {
|
||||
try {
|
||||
const sectionConfig = new Map<string, BreakingChangeRule>();
|
||||
const rulesIndex = this.AllConfig.findIndex(
|
||||
(v) => v.Scenario === scenarioName
|
||||
);
|
||||
if (rulesIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const rules = this.AllConfig[rulesIndex].rules;
|
||||
for (const key in Object.keys(rules)) {
|
||||
const ruleContent = rules[key];
|
||||
const rule = ruleContent as BreakingChangeRule;
|
||||
|
||||
if (!rule) {
|
||||
throw exception("invalid config");
|
||||
}
|
||||
sectionConfig.set(rule.appliedTo.toLowerCase(), rule);
|
||||
}
|
||||
return sectionConfig;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BreakingChangeLabels = new Set<string>()
|
||||
interface ruleHandler {
|
||||
process(message: OadMessage,rule: BreakingChangeRule): OadMessage
|
||||
}
|
||||
|
||||
const overrideHandler = {
|
||||
process(message: OadMessage, rule: BreakingChangeRule): OadMessage {
|
||||
let result = {...message} as any
|
||||
if (rule.override && typeof rule.override === "object") {
|
||||
for (const [key,value] of Object.entries(rule.override)) {
|
||||
if (typeof value === "string") {
|
||||
if (result[key]) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
else if (value) {
|
||||
for ( const pair of value) {
|
||||
if ((result[key] as string).toLowerCase() === pair.from.toLowerCase()) {
|
||||
result[key] = pair.to
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const directiveHandler = {
|
||||
process(message: OadMessage, rule: BreakingChangeRule): OadMessage {
|
||||
if (rule.directive && rule.directive.addingLabels && message.type.toLowerCase() === "error") {
|
||||
for (const label of rule.directive.addingLabels){
|
||||
BreakingChangeLabels.add(label)
|
||||
}
|
||||
}
|
||||
return message
|
||||
}
|
||||
};
|
||||
|
||||
class OadMessageEngine {
|
||||
private config: LocalRuleConfig;
|
||||
private HandlersMap = new Map<string, ruleHandler>();
|
||||
private scenario = "default";
|
||||
constructor(config: LocalRuleConfig) {
|
||||
this.config = config;
|
||||
this.initHandlerMap();
|
||||
}
|
||||
initHandlerMap() {
|
||||
this.HandlersMap.set("override", overrideHandler);
|
||||
this.HandlersMap.set("directive", directiveHandler);
|
||||
}
|
||||
public setScenario(scenarioName: string) {
|
||||
this.scenario = scenarioName;
|
||||
return this
|
||||
}
|
||||
getRulesMap() {
|
||||
return this.config.getConfig(this.scenario);
|
||||
}
|
||||
handle(messages: OadMessage[]): OadMessage[] {
|
||||
const ruleMap = this.getRulesMap();
|
||||
if (!ruleMap) {
|
||||
return messages
|
||||
}
|
||||
console.log("---- begin breaking change filter ----")
|
||||
const result: OadMessage[] = [];
|
||||
for (const message of messages) {
|
||||
const ruleId = message.id.toLowerCase();
|
||||
const ruleCode = message.code.toLowerCase();
|
||||
if (ruleMap && (ruleMap.has(ruleId) || ruleMap.has(ruleCode))) {
|
||||
const rule = ruleMap.get(ruleId) || ruleMap.get(ruleCode);
|
||||
let postMessage : OadMessage | undefined = message
|
||||
for (const [key,handler] of this.HandlersMap.entries()) {
|
||||
if (rule && Object.keys(rule).includes(key) && postMessage) {
|
||||
postMessage = handler.process(postMessage, rule);
|
||||
}
|
||||
}
|
||||
if (postMessage) {
|
||||
result.push(postMessage)
|
||||
}
|
||||
}
|
||||
else {
|
||||
result.push(message);
|
||||
}
|
||||
}
|
||||
console.log("----- end breaking change filter ----");
|
||||
console.log(result)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class BreakingChangeRuleManager {
|
||||
private getBreakingChangeConfigPath(){
|
||||
let breakingChangeRulesConfigPath = "config/BreakingChangeRules.yml";
|
||||
if (process.env.BREAKING_CHANGE_RULE_CONFIG_PATH) {
|
||||
breakingChangeRulesConfigPath =
|
||||
process.env.BREAKING_CHANGE_RULE_CONFIG_PATH;
|
||||
}
|
||||
return breakingChangeRulesConfigPath
|
||||
}
|
||||
|
||||
private buildRuleConfig() {
|
||||
const configPath = this.getBreakingChangeConfigPath()
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.log(`Config file:${configPath} was not existing.`);
|
||||
return undefined;
|
||||
}
|
||||
const config = new LocalRuleConfig();
|
||||
if (!config.load(configPath)) {
|
||||
throw exception(`unable to load config file:${configPath}`);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
public handleCrossApiVersion(messages: OadMessage[]) {
|
||||
const ruleConfig = this.buildRuleConfig();
|
||||
if (!ruleConfig) {
|
||||
return messages;
|
||||
}
|
||||
return new OadMessageEngine(ruleConfig)
|
||||
.setScenario("CrossVersion")
|
||||
.handle(messages);
|
||||
}
|
||||
|
||||
public handleSameApiVersion (messages: OadMessage[]) {
|
||||
const ruleConfig = this.buildRuleConfig();
|
||||
if (!ruleConfig) {
|
||||
return messages;
|
||||
}
|
||||
return new OadMessageEngine(ruleConfig)
|
||||
.setScenario("SameVersion")
|
||||
.handle(messages);
|
||||
};
|
||||
|
||||
public addBreakingChangeLabels() {
|
||||
sendLabels([...BreakingChangeLabels.values()]);
|
||||
};
|
||||
}
|
||||
|
||||
export const ruleManager = new BreakingChangeRuleManager()
|
|
@ -9,6 +9,7 @@ import * as utils from "./utils";
|
|||
import * as fs from "fs";
|
||||
import { devOps, cli } from "@azure/avocado";
|
||||
import * as format from "@azure/swagger-validation-common";
|
||||
import { lintTracer } from './unifiedPipelineHelper';
|
||||
|
||||
type TypeUtils = typeof utils;
|
||||
type TypeDevOps = typeof devOps;
|
||||
|
@ -58,6 +59,7 @@ export async function getLinterResult(
|
|||
tagCmd +
|
||||
swaggerPath;
|
||||
console.log(`Executing: ${cmd}`);
|
||||
|
||||
const { err, stdout, stderr } = await new Promise((res) =>
|
||||
exec(
|
||||
cmd,
|
||||
|
@ -159,6 +161,7 @@ class LinterRunner {
|
|||
if (tags) {
|
||||
for (const tag of tags) {
|
||||
if (utils.isTagExisting(swagger, tag)) {
|
||||
lintTracer.add(swagger,tag,beforeOrAfter === "before")
|
||||
const linterErrors = await getLinterResult(swagger, tag);
|
||||
console.log(linterErrors);
|
||||
await this.updateResult(swagger, linterErrors, beforeOrAfter);
|
||||
|
@ -168,6 +171,7 @@ class LinterRunner {
|
|||
}
|
||||
/* to ensure lint ran at least once */
|
||||
if (runCnt == 0) {
|
||||
lintTracer.add(swagger, "", beforeOrAfter === "before");
|
||||
const linterErrors = await getLinterResult(swagger);
|
||||
console.log(linterErrors);
|
||||
await this.updateResult(swagger, linterErrors, beforeOrAfter);
|
||||
|
@ -261,7 +265,8 @@ export async function lintDiff(utils: TypeUtils, devOps: TypeDevOps) {
|
|||
await linter.runTools("before");
|
||||
});
|
||||
}
|
||||
|
||||
lintTracer.save()
|
||||
|
||||
store.writeContent(JSON.stringify(linter.getResult(), null, 2));
|
||||
|
||||
console.log("--- Lint Violation Result ----\n");
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import * as fs from "fs-extra";
|
||||
import * as util from "./utils"
|
||||
import * as YAML from "js-yaml";
|
||||
import {
|
||||
getInputFilesForTag,
|
||||
} from "@azure/openapi-markdown";
|
||||
|
||||
import { MarkDownEx, parse } from "@ts-common/commonmark-to-markdown";
|
||||
|
||||
export function getVersionFromInputFile(filePath: string): string | undefined {
|
||||
const apiVersionRegex = /^\d{4}-\d{2}-\d{2}(|-preview)$/;
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
- Scenario: "SameVersion"
|
||||
description: "this rule is a mapping between oad rules and breaking change rules"
|
||||
rules:
|
||||
- appliedTo: "1034"
|
||||
override:
|
||||
code: "Added Required Property"
|
||||
type:
|
||||
- from: "warning"
|
||||
to: "error"
|
||||
message: "override message"
|
||||
|
||||
- appliedTo: "RemovedPath"
|
||||
override:
|
||||
code: "Removed path"
|
||||
type: "error"
|
||||
message: "override message"
|
||||
|
||||
directive:
|
||||
addingLabels:
|
||||
- NewApiVersionRequired
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
- Scenario: "CrossVersion"
|
||||
description: "this rule is a mapping between oad rules and breaking change rules"
|
||||
rules:
|
||||
- appliedTo: "1034"
|
||||
override:
|
||||
code: "Added Required Property"
|
||||
type:
|
||||
- from: "Error"
|
||||
to: "info"
|
||||
message: "override message"
|
||||
|
||||
directive:
|
||||
addingLabels:
|
||||
- NewApiVersionRequired
|
|
@ -0,0 +1,161 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License. See License in the project root for license information.
|
||||
|
||||
import { suite, test, timeout } from "mocha-typescript";
|
||||
import * as assert from "assert";
|
||||
import {
|
||||
ruleManager,
|
||||
} from "../breakingChangeRuleManager";
|
||||
import { lintTracer } from '../unifiedPipelineHelper';
|
||||
import * as fs from "fs-extra";
|
||||
@suite
|
||||
class BreakingChangeRuleTest {
|
||||
cwd = process.cwd();
|
||||
before() {
|
||||
process.env.BREAKING_CHANGE_RULE_CONFIG_PATH = "./breakingChangeRules.yaml";
|
||||
}
|
||||
|
||||
@test testCrossApiVersion() {
|
||||
process.chdir("./src/tests/Resource/breakingChangeRule");
|
||||
const messages = [
|
||||
{
|
||||
id: "1034",
|
||||
code: "AddedRequiredProperty",
|
||||
message:
|
||||
"The new version has new required property 'capacity' that was not found in the old version.",
|
||||
old: {
|
||||
ref:
|
||||
"file:///home/vsts/work/1/c93b354fd9c14905bb574a8834c4d69b/specification/apimanagement/resource-manager/Microsoft.ApiManagement/preview/2019-12-01-preview/apimdeployment.json#/definitions/ApiManagementServiceResource/properties/sku",
|
||||
path: "definitions.ApiManagementServiceResource.properties.sku",
|
||||
location:
|
||||
"file:///home/vsts/work/1/c93b354fd9c14905bb574a8834c4d69b/specification/apimanagement/resource-manager/Microsoft.ApiManagement/preview/2019-12-01-preview/apimdeployment.json:1268:9",
|
||||
},
|
||||
new: {
|
||||
ref:
|
||||
"file:///tmp/resolved/specification/apimanagement/resource-manager/Microsoft.ApiManagement/preview/2019-12-01-preview/apimdeployment.json#/definitions/ApiManagementServiceResource/properties/sku",
|
||||
path: "definitions.ApiManagementServiceResource.properties.sku",
|
||||
location:
|
||||
"file:///tmp/resolved/specification/apimanagement/resource-manager/Microsoft.ApiManagement/preview/2019-12-01-preview/apimdeployment.json:3143:9",
|
||||
},
|
||||
type: "Error",
|
||||
docUrl:
|
||||
"https://github.com/Azure/openapi-diff/tree/master/docs/rules/1034.md",
|
||||
mode: "Addition",
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
id: "1034",
|
||||
code: "Added Required Property",
|
||||
message: "override message",
|
||||
old: {
|
||||
ref:
|
||||
"file:///home/vsts/work/1/c93b354fd9c14905bb574a8834c4d69b/specification/apimanagement/resource-manager/Microsoft.ApiManagement/preview/2019-12-01-preview/apimdeployment.json#/definitions/ApiManagementServiceResource/properties/sku",
|
||||
path: "definitions.ApiManagementServiceResource.properties.sku",
|
||||
location:
|
||||
"file:///home/vsts/work/1/c93b354fd9c14905bb574a8834c4d69b/specification/apimanagement/resource-manager/Microsoft.ApiManagement/preview/2019-12-01-preview/apimdeployment.json:1268:9",
|
||||
},
|
||||
new: {
|
||||
ref:
|
||||
"file:///tmp/resolved/specification/apimanagement/resource-manager/Microsoft.ApiManagement/preview/2019-12-01-preview/apimdeployment.json#/definitions/ApiManagementServiceResource/properties/sku",
|
||||
path: "definitions.ApiManagementServiceResource.properties.sku",
|
||||
location:
|
||||
"file:///tmp/resolved/specification/apimanagement/resource-manager/Microsoft.ApiManagement/preview/2019-12-01-preview/apimdeployment.json:3143:9",
|
||||
},
|
||||
type: "info",
|
||||
docUrl:
|
||||
"https://github.com/Azure/openapi-diff/tree/master/docs/rules/1034.md",
|
||||
mode: "Addition",
|
||||
},
|
||||
];
|
||||
const result = ruleManager.handleCrossApiVersion(messages);
|
||||
assert.deepEqual(result, expected);
|
||||
}
|
||||
|
||||
@test testSameApiVersion() {
|
||||
process.chdir("./src/tests/Resource/breakingChangeRule");
|
||||
process.env.BREAKING_CHANGE_RULE_CONFIG_PATH =
|
||||
"./breakingChangeRules-1.yaml";
|
||||
const messages = [
|
||||
{
|
||||
id: "1034",
|
||||
code: "AddedRequiredProperty",
|
||||
message:
|
||||
"The new version has new required property 'capacity' that was not found in the old version.",
|
||||
type: "Warning",
|
||||
new: {},
|
||||
old: {},
|
||||
docUrl:
|
||||
"https://github.com/Azure/openapi-diff/tree/master/docs/rules/1034.md",
|
||||
mode: "Addition",
|
||||
},
|
||||
{
|
||||
id: "1005",
|
||||
code: "RemovedPath",
|
||||
message:
|
||||
"The new version is missing a path that was found in the old version. Was path '/providers/Microsoft.Devices/operations' removed or restructured?",
|
||||
type: "Waring",
|
||||
new: {},
|
||||
old: {},
|
||||
docUrl:
|
||||
"https://github.com/Azure/openapi-diff/tree/master/docs/rules/1005.md",
|
||||
mode: "Removal",
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
id: "1034",
|
||||
code: "Added Required Property",
|
||||
message: "override message",
|
||||
type: "error",
|
||||
new: {},
|
||||
old: {},
|
||||
docUrl:
|
||||
"https://github.com/Azure/openapi-diff/tree/master/docs/rules/1034.md",
|
||||
mode: "Addition",
|
||||
},
|
||||
{
|
||||
id: "1005",
|
||||
code: "Removed path",
|
||||
message: "override message",
|
||||
type: "error",
|
||||
new: {},
|
||||
old: {},
|
||||
docUrl:
|
||||
"https://github.com/Azure/openapi-diff/tree/master/docs/rules/1005.md",
|
||||
mode: "Removal",
|
||||
},
|
||||
];
|
||||
const result = ruleManager.handleSameApiVersion(messages);
|
||||
assert.deepEqual(result, expected);
|
||||
}
|
||||
|
||||
@test TestLintTrace() {
|
||||
lintTracer.add(
|
||||
"specification/apimanagement/resource-manager/readme.md",
|
||||
"package-2020-08",
|
||||
true
|
||||
);
|
||||
lintTracer.add(
|
||||
"specification/apimanagement/resource-manager/readme.md",
|
||||
"package-2020-08",
|
||||
false
|
||||
);
|
||||
lintTracer.add(
|
||||
"specification/apimanagement/resource-manager/readme.md",
|
||||
"",
|
||||
false
|
||||
);
|
||||
const resultFile = "pipe.log";
|
||||
if (fs.existsSync(resultFile)) {
|
||||
fs.unlinkSync(resultFile);
|
||||
}
|
||||
lintTracer.save();
|
||||
const lintTraceInfo = JSON.parse(fs.readFileSync(resultFile).toString());
|
||||
assert.notEqual(undefined, lintTraceInfo);
|
||||
}
|
||||
|
||||
after() {
|
||||
process.chdir(this.cwd);
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ import * as asyncIt from "@ts-common/async-iterator";
|
|||
import _ from 'lodash';
|
||||
import { LintingResultMessage } from '../momentOfTruthUtils';
|
||||
import { ReadmeParser } from "../readmeUtils"
|
||||
import { LintMsgTransformer } from "../unifiedPipelineHelper";
|
||||
import { MsgTransformer } from "../unifiedPipelineHelper";
|
||||
|
||||
const sinon = require("sinon");
|
||||
let cwd = process.cwd();
|
||||
|
@ -30,7 +30,7 @@ class LintingRpaasTest {
|
|||
|
||||
@test TestLintMsgTransformer() {
|
||||
process.chdir("./src/tests/Resource/lintingRpaas");
|
||||
const transformer = new LintMsgTransformer();
|
||||
const transformer = new MsgTransformer();
|
||||
const testMsg = [
|
||||
({
|
||||
type: "Error",
|
||||
|
|
|
@ -47,6 +47,10 @@ class MomentOfTruthPostProcessingTest {
|
|||
let stub5 = sinon.stub(utils, "getTargetBranch").callsFake(() => "master");
|
||||
|
||||
await cleanUpDir("./output");
|
||||
const pipeFile = "./pipe.log";
|
||||
if (fs.existsSync(pipeFile)) {
|
||||
fs.unlinkSync(pipeFile);
|
||||
}
|
||||
await lintDiff(utils, devOps);
|
||||
await postProcessing();
|
||||
|
||||
|
@ -63,7 +67,7 @@ class MomentOfTruthPostProcessingTest {
|
|||
const logFile = "./output/1001.json";
|
||||
const result = JSON.parse(fs.readFileSync(logFile, { encoding: "utf8" }));
|
||||
const resultFiles = result.files;
|
||||
assert.deepEqual(Object.keys(resultFiles), [
|
||||
assert.deepEqual(Object.keys(resultFiles), [
|
||||
"specification/test-lint/readme.md",
|
||||
]);
|
||||
|
||||
|
@ -77,15 +81,15 @@ assert.deepEqual(Object.keys(resultFiles), [
|
|||
.after as Array<any>).map((error) => error.id).sort();
|
||||
assert.deepEqual(errorIds, ["D5001","R2054", "R3023"]);
|
||||
|
||||
const pipeFile = "./pipe.log";
|
||||
|
||||
console.log("------------- read from pipe.log -----------------");
|
||||
const chunck = fs.readFileSync(pipeFile, { encoding: "utf8" })
|
||||
console.log(chunck);
|
||||
const messages = chunck.split(/[\r\n]+/)
|
||||
const chunk = fs.readFileSync(pipeFile, { encoding: "utf8" })
|
||||
console.log(chunk);
|
||||
const messages = chunk.split(/[\r\n]+/)
|
||||
.filter(l => l) // filter out empty lines
|
||||
.map(l => JSON.parse(l.trim()) as MessageLine)
|
||||
.map(l => JSON.parse(l.trim()) as MessageLine).filter(m => m)
|
||||
.map(l => Array.isArray(l) ? l : [l]);
|
||||
const res: ResultMessageRecord[] = _.flatMap(messages, m => m).map(m => <ResultMessageRecord>m);
|
||||
const res: ResultMessageRecord[] = _.flatMap(messages, m => m).map(m => <ResultMessageRecord>m).filter(m => m.type === "Result");
|
||||
const resIds = res.map(m => m.id).sort();
|
||||
console.log("------------- parse validation message from[pipe.log] ------------------");
|
||||
console.log(JSON.stringify(res));
|
||||
|
|
|
@ -2,8 +2,13 @@
|
|||
import { LintingResultMessage , Mutable , Issue, getFile, getLine,getDocUrl, composeLintResult} from "./momentOfTruthUtils"
|
||||
import * as utils from "./utils";
|
||||
import * as fs from "fs-extra";
|
||||
import { OadMessage } from './breaking-change';
|
||||
import * as format from "@azure/swagger-validation-common";
|
||||
import { devOps } from '@azure/avocado';
|
||||
import { PullRequestProperties } from '@azure/avocado/dist/dev-ops';
|
||||
const packageJson = require("../package.json");
|
||||
|
||||
export class LintMsgTransformer {
|
||||
export class MsgTransformer {
|
||||
constructor() {}
|
||||
|
||||
lintMsgToUnifiedMsg(msg: LintingResultMessage[]) {
|
||||
|
@ -23,6 +28,42 @@ export class LintMsgTransformer {
|
|||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
OadMsgToUnifiedMsg(messages: OadMessage[]) {
|
||||
const pipelineResultData: format.ResultMessageRecord[] = messages.map(
|
||||
(it) => ({
|
||||
type: "Result",
|
||||
level: it.type as format.MessageLevel,
|
||||
message: it.message,
|
||||
code: it.code,
|
||||
id: it.id,
|
||||
docUrl: it.docUrl,
|
||||
time: new Date(),
|
||||
extra: {
|
||||
mode: it.mode,
|
||||
},
|
||||
paths: [
|
||||
{
|
||||
tag: "New",
|
||||
path: utils.blobHref(
|
||||
utils.getGithubStyleFilePath(
|
||||
utils.getRelativeSwaggerPathToRepo(it.new.location || "")
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
tag: "Old",
|
||||
path: utils.targetHref(
|
||||
utils.getGithubStyleFilePath(
|
||||
utils.getRelativeSwaggerPathToRepo(it.old.location || "")
|
||||
)
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
return JSON.stringify(pipelineResultData);
|
||||
}
|
||||
|
||||
rawErrorToUnifiedMsg(
|
||||
errType: string,
|
||||
errorMsg: string,
|
||||
|
@ -41,15 +82,29 @@ export class LintMsgTransformer {
|
|||
};
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
toMarkDownMsg(
|
||||
errorMsg: string,
|
||||
levelType = "Error"
|
||||
) {
|
||||
const result = {
|
||||
type: "Markdown",
|
||||
mode:"append",
|
||||
level: levelType,
|
||||
message: errorMsg,
|
||||
time: new Date(),
|
||||
} as format.MarkdownMessageRecord;
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class UnifiedPipeLineStore {
|
||||
logFile = "pipe.log";
|
||||
readme: string;
|
||||
transformer: LintMsgTransformer;
|
||||
transformer: MsgTransformer;
|
||||
constructor(readme: string) {
|
||||
this.transformer = new LintMsgTransformer();
|
||||
this.transformer = new MsgTransformer();
|
||||
this.readme = readme;
|
||||
}
|
||||
|
||||
|
@ -103,4 +158,130 @@ export class UnifiedPipeLineStore {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
public appendOadViolation(oadResult: OadMessage[]) {
|
||||
this.appendMsg(this.transformer.OadMsgToUnifiedMsg(oadResult));
|
||||
}
|
||||
|
||||
public appendMarkDown(markDown: string) {
|
||||
this.appendMsg(this.transformer.toMarkDownMsg(markDown));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AbstractToolTrace {
|
||||
genMarkDown() {
|
||||
return ""
|
||||
}
|
||||
save() {
|
||||
const markDown = this.genMarkDown();
|
||||
if (markDown) {
|
||||
return new UnifiedPipeLineStore("").appendMarkDown(markDown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// record lint invoking trace
|
||||
class LintTrace extends AbstractToolTrace {
|
||||
private traces = {
|
||||
source: new Map<string, string[]>(),
|
||||
target: new Map<string, string[]>(),
|
||||
};
|
||||
|
||||
// isFromTargetBranch indicates whether it's from target branch
|
||||
add(readmeRelatedPath: string, tag: string, isFromTargetBranch: boolean) {
|
||||
const targetMap = isFromTargetBranch
|
||||
? this.traces.target
|
||||
: this.traces.source;
|
||||
|
||||
if (targetMap.has(readmeRelatedPath)) {
|
||||
const tags = targetMap.get(readmeRelatedPath);
|
||||
if (tags) {
|
||||
tags.push(tag);
|
||||
}
|
||||
} else {
|
||||
targetMap.set(readmeRelatedPath, [tag]);
|
||||
}
|
||||
}
|
||||
|
||||
genMarkDown() {
|
||||
const classicLintVersion = process.env["CLASSIC_LINT_VERSION"]
|
||||
? process.env["CLASSIC_LINT_VERSION"]
|
||||
: "1.0.14";
|
||||
const lintVersion = process.env["LINT_VERSION"]
|
||||
? process.env["LINT_VERSION"]
|
||||
: "1.0.4";
|
||||
let content = "<br><ul>";
|
||||
let impactedTags = 0;
|
||||
for (const [beforeAfter, readmeTags] of Object.entries(this.traces)) {
|
||||
content += `<li>`;
|
||||
content += `Linted configuring files (Based on ${
|
||||
beforeAfter === "source" ? "source" : "target"
|
||||
} branch, openapi-validator <a href="https://www.npmjs.com/package/@microsoft.azure/openapi-validator/v/${lintVersion}" target="_blank"> v${lintVersion} </a>, classic-openapi-validator <a href="https://www.npmjs.com/package/@microsoft.azure/classic-openapi-validator/v/${classicLintVersion}" target="_blank"> v${classicLintVersion} </a>)`;
|
||||
content += `<ul> `;
|
||||
for (const [readme, tags] of readmeTags.entries()) {
|
||||
const url =
|
||||
beforeAfter === "target"
|
||||
? utils.targetHref(readme)
|
||||
: utils.blobHref(readme);
|
||||
const showReadme = readme
|
||||
.split(/[/|\\]/)
|
||||
.slice(-3)
|
||||
.join("/");
|
||||
for (const tag of tags) {
|
||||
content += "<li>";
|
||||
content += `<a href="${url}"target="_blank">${showReadme}</a> tag:<a href="${url}${
|
||||
tag ? "#tag-" : ""
|
||||
}${tag}" target="_blank">${tag ? tag : "default"}</a>`;
|
||||
content += "</li>";
|
||||
impactedTags ++
|
||||
}
|
||||
}
|
||||
content += `</ul></li>`;
|
||||
}
|
||||
if (impactedTags === 0) {
|
||||
return ""
|
||||
}
|
||||
content += "</ul>";
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
// record oad invoking trace
|
||||
class OadTrace extends AbstractToolTrace {
|
||||
private traces: { old: string; new: string }[] = [];
|
||||
add(oldSwagger: string, newSwagger: string) {
|
||||
this.traces.push({ old: oldSwagger, new: newSwagger });
|
||||
return this;
|
||||
}
|
||||
|
||||
genMarkDown() {
|
||||
const oadVersion = packageJson.dependencies["@azure/oad"].replace(
|
||||
/[\^~]/,
|
||||
""
|
||||
);
|
||||
if (this.traces.length === 0 ) {
|
||||
return ""
|
||||
}
|
||||
let content = `<br><ul><li>Compared Swaggers (Based on Oad <a href="https://www.npmjs.com/package/@azure/oad/v/${oadVersion}" target="_blank">v${oadVersion}</a>)<ul>`;
|
||||
for (const value of this.traces.values()) {
|
||||
content += "<li>";
|
||||
content += `original: <a href="${utils.targetHref(
|
||||
value.old
|
||||
)}" target="_blank">${value.old
|
||||
.split("/")
|
||||
.slice(-3)
|
||||
.join("/")} </a> <---> new: <a href="${utils.blobHref(
|
||||
value.new
|
||||
)} " target="_blank"> ${value.new.split("/").slice(-3).join("/")} </a>`;
|
||||
content += "</li>";
|
||||
}
|
||||
content += `</ul></li></ul>`;
|
||||
return content;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const oadTracer = new OadTrace();
|
||||
export const lintTracer = new LintTrace();
|
Загрузка…
Ссылка в новой задаче