Add logic to discern tags from branches in dependencies

This commit is contained in:
Pedro Pombeiro 2019-01-17 19:01:17 +01:00
parent aaed828003
commit 0653b8f8e3
No known key found for this signature in database
GPG Key ID: A65DEB11E4BBC647
2 changed files with 102 additions and 81 deletions

View File

@ -1,3 +1,73 @@
import { Context } from 'probot/lib/context'
export function getDependenciesFromJSON (dependenciesJSON: any): Dependency[] {
const dependencies: Dependency[] = []
for (const name in dependenciesJSON) {
if (dependenciesJSON.hasOwnProperty(name)) {
dependencies.push(new Dependency(name, dependenciesJSON[name]))
}
}
return dependencies
}
export async function checkDependenciesAsync (context: Context, contents: string, dependencies: Dependency[], filename: string, result: AnalysisResult) {
if (!dependencies || dependencies.length === 0) {
return
}
const urlRegex = /^(http:\/\/|https:\/\/|git\+http:\/\/|git\+https:\/\/|ssh:\/\/|git\+ssh:\/\/|github:)([a-zA-Z0-9_\-./]+)(#(.*))?$/gm
const requiredProtocol = 'git+https://'
result.checkedDependencyCount += dependencies.length
for (const dependency of dependencies) {
const url = dependency.url
const match = urlRegex.exec(url)
if (!match) {
continue
}
const protocol = match[1]
const address = protocol === 'github:' ? `github.com/${match[2]}` : match[2]
const tag = match.length > 4 ? match[4] : ''
const { line } = findLineColumn(contents, contents.indexOf(url))
const optimalAddress = address.endsWith('.git') ? address : address.concat('.git')
const isTag = await isTagAsync(context, address, tag)
const optimalTag = isTag ? tag : '#<release-tag>'
const suggestedUrl = `${requiredProtocol}${optimalAddress}${optimalTag}`
const annotationSource: annotationSource = {
dependency: dependency,
filename: filename,
line: line
}
if (protocol !== requiredProtocol) {
result.annotations.push(createAnnotation(annotationSource, suggestedUrl, 'warning',
`Found protocol ${protocol} being used in dependency`,
`Protocol should be ${requiredProtocol}.`))
}
if (protocol !== 'github:' && !address.endsWith('.git')) {
result.annotations.push(createAnnotation(annotationSource, suggestedUrl, 'warning',
'Address should end with .git for consistency.',
'Android builds have been known to fail when dependency addresses don\'t end with .git.'
))
}
if (!tag) {
result.annotations.push(createAnnotation(annotationSource, suggestedUrl, 'failure',
'Dependency is not locked with a tag/release.',
`${url} is not a deterministic dependency locator.\r\nIf the branch advances, it will be impossible to rebuild the same output in the future.`
))
} else if (!isTag) {
result.annotations.push(createAnnotation(annotationSource, suggestedUrl, 'failure',
'Dependency is locked with a branch, instead of a tag/release.',
`${url} is not a deterministic dependency locator.\r\nIf the branch advances, it will be impossible to rebuild the same output in the future.`
))
}
}
}
export class AnnotationResult { export class AnnotationResult {
title?: string title?: string
message: string message: string
@ -9,13 +79,13 @@ export class AnnotationResult {
rawDetails?: string rawDetails?: string
constructor (title: string, constructor (title: string,
message: string, message: string,
annotationLevel: "notice" | "warning" | "failure", annotationLevel: "notice" | "warning" | "failure",
dependency: Dependency, dependency: Dependency,
path: string, path: string,
startLine: number, startLine: number,
endLine: number, endLine: number,
rawDetails: string) { rawDetails: string) {
this.title = title this.title = title
this.message = message this.message = message
this.annotationLevel = annotationLevel this.annotationLevel = annotationLevel
@ -53,73 +123,6 @@ type annotationSource = {
line: number line: number
} }
export function getDependenciesFromJSON (dependenciesJSON: any): Dependency[] {
const dependencies: Dependency[] = []
for (const name in dependenciesJSON) {
if (dependenciesJSON.hasOwnProperty(name)) {
dependencies.push(new Dependency(name, dependenciesJSON[name]))
}
}
return dependencies
}
export function checkDependencies (contents: string, dependencies: Dependency[], filename: string, result: AnalysisResult) {
if (!dependencies || dependencies.length === 0) {
return
}
const urlRegex = /^(http:\/\/|https:\/\/|git\+http:\/\/|git\+https:\/\/|ssh:\/\/|git\+ssh:\/\/|github:)([a-zA-Z0-9_\-./]+)(#(.*))?$/gm
const requiredProtocol = 'git+https://'
result.checkedDependencyCount += dependencies.length
for (const dependency of dependencies) {
const url = dependency.url
const match = urlRegex.exec(url)
if (!match) {
continue
}
const protocol = match[1]
const address = match[2]
const tag = match.length > 4 ? match[4] : ''
const { line } = findLineColumn(contents, contents.indexOf(url))
const optimalAddress = address.endsWith('.git') ? address : address.concat('.git')
const optimalTag = isTag(tag) ? tag : '#<release-tag>'
const suggestedUrl = `${requiredProtocol}${optimalAddress}${optimalTag}`
const annotationSource: annotationSource = {
dependency: dependency,
filename: filename,
line: line
}
if (protocol !== requiredProtocol) {
result.annotations.push(createAnnotation(annotationSource, suggestedUrl, 'warning',
`Found protocol ${protocol} being used in dependency`,
`Protocol should be ${requiredProtocol}.`))
}
if (protocol !== 'github:' && !address.endsWith('.git')) {
result.annotations.push(createAnnotation(annotationSource, suggestedUrl, 'warning',
'Address should end with .git for consistency.',
'Android builds have been known to fail when dependency addresses don\'t end with .git.'
))
}
if (!tag) {
result.annotations.push(createAnnotation(annotationSource, suggestedUrl, 'failure',
'Dependency is not locked with a tag/release.',
`${url} is not a deterministic dependency locator.\r\nIf the branch advances, it will be impossible to rebuild the same output in the future.`
))
} else if (!isTag(tag)) {
result.annotations.push(createAnnotation(annotationSource, suggestedUrl, 'failure',
'Dependency is locked with a branch, instead of a tag/release.',
`${url} is not a deterministic dependency locator.\r\nIf the branch advances, it will be impossible to rebuild the same output in the future.`
))
}
}
}
function findLineColumn (contents: string, index: number) { function findLineColumn (contents: string, index: number) {
const lines = contents.split('\n') const lines = contents.split('\n')
const line = contents.substr(0, index).split('\n').length const line = contents.substr(0, index).split('\n').length
@ -155,7 +158,25 @@ function createAnnotation (
) )
} }
function isTag (tag: string): boolean { async function isTagAsync (context: Context, address: string, tag: string): Promise<boolean> {
// TODO: We need to check the actual repo to see if it is a branch or a tag if (!tag) {
return tag !== '' && tag !== 'master' && tag !== 'develop' return false
}
// 'github.com/status-im/bignumber.js'
const parts = address.split('/')
if (parts[0] === 'github.com') {
try {
const getRefResponse = await context.github.gitdata.getRef({ owner: parts[1], repo: parts[2].replace('.git', ''), ref: `tags/${tag}` })
if (getRefResponse.status === 200) {
return true
}
} catch (error) {
context.log.error(error)
return false
}
}
// Educated guess
return false
} }

View File

@ -3,7 +3,7 @@
import { Application, Context } from 'probot' // eslint-disable-line no-unused-vars import { Application, Context } from 'probot' // eslint-disable-line no-unused-vars
import Octokit from '@octokit/rest' import Octokit from '@octokit/rest'
import Humanize from 'humanize-plus' import Humanize from 'humanize-plus'
import { checkDependencies, getDependenciesFromJSON, AnalysisResult, AnnotationResult } from './dependency-check' import { checkDependenciesAsync, getDependenciesFromJSON, AnalysisResult, AnnotationResult } from './dependency-check'
const pendingChecks: any = [] const pendingChecks: any = []
@ -184,9 +184,9 @@ async function checkPackageFileAsync (analysisResult: AnalysisResult, context: C
const contents = Buffer.from(contentsResponse.data.content, 'base64').toString('utf8') const contents = Buffer.from(contentsResponse.data.content, 'base64').toString('utf8')
const contentsJSON = JSON.parse(contents) const contentsJSON = JSON.parse(contents)
checkDependencies(contents, getDependenciesFromJSON(contentsJSON.dependencies), filename, analysisResult) await checkDependenciesAsync(context, contents, getDependenciesFromJSON(contentsJSON.dependencies), filename, analysisResult)
checkDependencies(contents, getDependenciesFromJSON(contentsJSON.devDependencies), filename, analysisResult) await checkDependenciesAsync(context, contents, getDependenciesFromJSON(contentsJSON.devDependencies), filename, analysisResult)
checkDependencies(contents, getDependenciesFromJSON(contentsJSON.optionalDependencies), filename, analysisResult) await checkDependenciesAsync(context, contents, getDependenciesFromJSON(contentsJSON.optionalDependencies), filename, analysisResult)
return analysisResult.checkedDependencyCount return analysisResult.checkedDependencyCount
} }