Skip to content
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@azure/avocado",
"version": "0.3.0",
"version": "0.3.2",
"description": "A validator of OpenAPI configurations",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
8 changes: 6 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as stringMap from "@ts-common/string-map"
import * as yaml from "js-yaml"
import * as path from "path"

export type Report = {
readonly error: (error: unknown) => void
Expand All @@ -15,6 +14,11 @@ export type Config = {
readonly env: stringMap.StringMap<string>
}

export const defaultConfig = () => ({
cwd: process.cwd(),
env: process.env
})

/**
* The function executes the given `tool` and prints errors to `stderr`.
*
Expand All @@ -26,7 +30,7 @@ export const run = async <T>(
report: Report = { error: console.error, info: console.log }
): Promise<void> => {
try {
const errors = await tool({ cwd: path.resolve("./"), env: process.env })
const errors = await tool(defaultConfig())
// tslint:disable-next-line:no-let
let errorsNumber = 0
for await (const e of errors) {
Expand Down
56 changes: 56 additions & 0 deletions src/dev-ops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as cli from "./cli"
import * as git from "./git"
import * as path from "path"
import * as fs from "@ts-common/fs"

export type PullRequestProperties = {
readonly targetBranch: string
readonly sourceBranch: string
readonly workingDir: string
readonly checkout: (branch: string) => Promise<void>
// tslint:disable-next-line:prettier
readonly diff: () => Promise<readonly string[]>
}

const sourceBranch = "source-b6791c5f-e0a5-49b1-9175-d7fd3e341cb8"

/**
* Currently, the algorithm is recognizing Azure Dev Ops Pull Request if the `env` has
* `SYSTEM_PULLREQUEST_TARGETBRANCH`. `cwd` should points to the source Git repository.
*/
export const createPullRequestProperties = async (
// tslint:disable-next-line:prettier
{ cwd, env }: cli.Config
): Promise<PullRequestProperties | undefined> => {
const targetBranch = env.SYSTEM_PULLREQUEST_TARGETBRANCH
if (targetBranch === undefined) {
return undefined
}
const originGitRepository = git.repository(cwd)
await originGitRepository({ branch: [sourceBranch] })
await originGitRepository({
branch: [targetBranch, `remotes/origin/${targetBranch}`]
})

// we have to clone the repository because we need to switch branches.
// Switching branches in the current repository can be dangerous because Avocado
// may be running from it.
const workingDir = path.resolve(path.join(cwd, "..", "c93b354fd9c14905bb574a8834c4d69b"))
await fs.mkdir(workingDir)
const workingGitRepository = git.repository(workingDir)
await workingGitRepository({ clone: [cwd, "."] })
return {
targetBranch,
sourceBranch,
workingDir,
checkout: async (branch: string) => {
await workingGitRepository({ checkout: [branch] })
},
diff: async () => {
const { stdout } = await originGitRepository({
diff: ["--name-only", sourceBranch, targetBranch]
})
return stdout.split("\n").filter(v => v !== "")
}
}
}
2 changes: 1 addition & 1 deletion src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type Command =
{ readonly branch: readonly [string]|readonly [string, string] } |
{ readonly remote: readonly ["add", string, string] } |
{ readonly clone: readonly [string, string] } |
{ readonly diff: readonly ["--name-status", string, string] }
{ readonly diff: readonly ["--name-status" | "--name-only", string, string] }

export const repository = (repositoryPath: string) =>
async (command: Command) => {
Expand Down
67 changes: 37 additions & 30 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import * as json from "@ts-common/json"
import * as stringMap from "@ts-common/string-map"
import * as commonmark from "commonmark"
import * as cli from "./cli"
import * as git from "./git"
import nodeObjectHash = require("node-object-hash")
import * as devOps from "./dev-ops"

export { createPullRequestProperties, PullRequestProperties } from "./dev-ops"
export { defaultConfig, Config } from "./cli"

export type JsonParseError = {
readonly code: "JSON_PARSE"
Expand Down Expand Up @@ -65,50 +68,54 @@ const validateSpecificationFolder = (cwd: string) =>
}
})

const validateSpecificationFolderMap = async (cwd: string) => {
/**
* Creates a map of unique errors for the given folder `cwd`.
*
* @param cwd
*/
const avocadoForDir = async (cwd: string) => {
const map = new Map<string, Error>()
for await (const e of validateSpecificationFolder(cwd)) {
map.set(errorCorrelationId(e), e)
}
return map
}

const sourceBranch = "source-b6791c5f-e0a5-49b1-9175-d7fd3e341cb8"
/**
* Run Avocado in Azure DevOps for a Pull Request.
*
* @param pr Pull Request properties
*/
const avocadoForDevOps = (pr: devOps.PullRequestProperties): asyncIt.AsyncIterableEx<Error> =>
asyncIt.iterable<Error>(async function*() {
// collect all errors from the 'targetBranch'
await pr.checkout(pr.targetBranch)
const targetMap = await avocadoForDir(pr.workingDir)

// collect all errors from the 'sourceBranch'
await pr.checkout(pr.sourceBranch)
const sourceMap = await avocadoForDir(pr.workingDir)

// remove existing errors.
for (const e of targetMap.keys()) {
sourceMap.delete(e)
}
yield* sourceMap.values()
})

/**
* The function validates files in the given `cwd` folder and returns errors.
*
* @param { cwd, env }
* @param config
*/
export const avocado = ({ cwd, env }: cli.Config): asyncIt.AsyncIterableEx<Error> =>
export const avocado = (config: cli.Config): asyncIt.AsyncIterableEx<Error> =>
asyncIt.iterable<Error>(async function*() {
const targetBranch = env.SYSTEM_PULLREQUEST_TARGETBRANCH
const pr = await devOps.createPullRequestProperties(config)
// detect Azure DevOps Pull Request validation.
if (targetBranch !== undefined) {
const sourceGitRepository = git.repository(cwd)
await sourceGitRepository({ branch: [sourceBranch] })
await sourceGitRepository({ branch: [targetBranch, `remotes/origin/${targetBranch}`] })

// we have to clone the repository because we need to switch branches.
// Switching branches in the current repository can be dangerous because Avocado
// may be running from it.
const target = path.resolve(path.join(cwd, "..", "target"))
await fs.mkdir(target)
const targetGitRepository = git.repository(target)
await targetGitRepository({ clone: [cwd, "."] })

await targetGitRepository({ checkout: [targetBranch] })
const targetMap = await validateSpecificationFolderMap(target)

await targetGitRepository({ checkout: [sourceBranch] })
const sourceMap = await validateSpecificationFolderMap(target)

for (const e of targetMap.keys()) {
sourceMap.delete(e)
}
yield* sourceMap.values()
if (pr !== undefined) {
yield* avocadoForDevOps(pr)
} else {
yield* (await validateSpecificationFolderMap(cwd)).values()
yield* (await avocadoForDir(config.cwd)).values()
}
})

Expand Down
78 changes: 78 additions & 0 deletions src/test/dev-ops-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as path from "path"
import * as pfs from "@ts-common/fs"
import * as avocado from "../index"
import * as git from "../git"
import * as assert from "assert"
import * as cli from "../cli"
import * as devOps from "../dev-ops"

/**
* Create Azure DevOps environment for testing.
*
* @param name an environment name. It's used as a unique directory suffix.
*/
const createDevOpsEnv = async (name: string): Promise<cli.Config> => {
const tmp = path.resolve(path.join("..", `avocado-tmp-${name}`))

if (await pfs.exists(tmp)) {
await pfs.recursiveRmdir(tmp)
}

// Create "tmp/remote" folder.
await pfs.mkdir(tmp)
const remote = path.join(tmp, "remote")
await pfs.mkdir(remote)

const gitRemote = git.repository(remote)

// create a Git repository
await gitRemote({ init: [] })
await gitRemote({ config: ["user.email", "test@example.com"] })
await gitRemote({ config: ["user.name", "test"] })

// commit invalid "specification/readme.md" to "master".
const specification = path.join(remote, "specification")
await pfs.mkdir(specification)
await pfs.writeFile(path.join(specification, "readme.md"), "")
await gitRemote({ add: ["."] })
await gitRemote({ commit: ["-m", '"add specification/readme.md"', "--no-gpg-sign"] })

// commit removing "specification/readme.md" to "source".
await gitRemote({ checkout: ["-b", "source"] })
await pfs.unlink(path.join(specification, "readme.md"))
await gitRemote({ add: ["."] })
await gitRemote({ commit: ["-m", '"delete specification/readme.md"', "--no-gpg-sign"] })

// create local Git repository
const local = path.join(tmp, "local")
await pfs.mkdir(local)
const gitLocal = git.repository(local)
await gitLocal({ clone: ["../remote", "."] })

return {
cwd: local,
env: {
SYSTEM_PULLREQUEST_TARGETBRANCH: "master"
}
}
}

describe("Azure DevOps", () => {
it("Azure DevOps and Avocado", async () => {
const cfg = await createDevOpsEnv("458e3de4-ca9c-4f98-858a-6bb9863189e6")

// run avocado as AzureDevOps pull request.
const errors = await avocado.avocado(cfg).toArray()
assert.deepStrictEqual(errors, [])
}).timeout(5000)

it("PR diff", async () => {
const cfg = await createDevOpsEnv("cb48-4995-9348-af800342b723")
const pr = await devOps.createPullRequestProperties(cfg)
if (pr === undefined) {
throw new Error("pr === undefined")
}
const files = await pr.diff()
assert.deepStrictEqual(files, ["specification/readme.md"])
}).timeout(5000)
})
72 changes: 0 additions & 72 deletions src/test/git-test.ts

This file was deleted.

1 change: 1 addition & 0 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"trailing-comma": false,
"arrow-parens": false,
"object-literal-sort-keys": false,
"no-floating-promises": true,

// Recommended built-in rules
"no-var-keyword": true,
Expand Down