diff --git a/lib/manager/azure-pipelines/__fixtures__/azure-pipelines-invalid.yaml b/lib/manager/azure-pipelines/__fixtures__/azure-pipelines-invalid.yaml new file mode 100644 index 0000000000000000000000000000000000000000..aa326c7b7bd471083be062a76084c2ce9bdb1de3 --- /dev/null +++ b/lib/manager/azure-pipelines/__fixtures__/azure-pipelines-invalid.yaml @@ -0,0 +1,3 @@ +steps: +- bash: echo "hello" + displayName: echo diff --git a/lib/manager/azure-pipelines/__fixtures__/azure-pipelines-no-dependency.yaml b/lib/manager/azure-pipelines/__fixtures__/azure-pipelines-no-dependency.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f3b34ce08bf44a3ce37b380bf47f0283054c1d61 --- /dev/null +++ b/lib/manager/azure-pipelines/__fixtures__/azure-pipelines-no-dependency.yaml @@ -0,0 +1,7 @@ +resources: + pipelines: + - pipeline: MyAppA + source: MyCIPipelineA + - pipeline: MyAppB + source: MyCIPipelineB + trigger: true diff --git a/lib/manager/azure-pipelines/__fixtures__/azure-pipelines.yaml b/lib/manager/azure-pipelines/__fixtures__/azure-pipelines.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8d61b6468d9e66429e040a1f69fc142f292f51f4 --- /dev/null +++ b/lib/manager/azure-pipelines/__fixtures__/azure-pipelines.yaml @@ -0,0 +1,14 @@ +resources: + repositories: + - type: github + name: renovate/renovate + ref: refs/heads/master + - type: github + name: user/repo + ref: refs/tags/v0.5.1 + containers: + - container: linux + image: ubuntu:16.04 + - container: python + image: python:3.7@sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077 + - container: missingimage diff --git a/lib/manager/azure-pipelines/__snapshots__/extract.spec.ts.snap b/lib/manager/azure-pipelines/__snapshots__/extract.spec.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..1005f0dfae356811ef51d513a50da6144d4f53e9 --- /dev/null +++ b/lib/manager/azure-pipelines/__snapshots__/extract.spec.ts.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`manager/azure-pipelines/extract extractContainer() should extract container information 1`] = ` +Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": "16.04", + "datasource": "docker", + "depName": "ubuntu", + "depType": "docker", + "replaceString": "ubuntu:16.04", +} +`; + +exports[`manager/azure-pipelines/extract extractPackageFile() extracts dependencies 1`] = ` +Array [ + Object { + "autoReplaceStringTemplate": "refs/tags/{{newValue}}", + "currentValue": "v0.5.1", + "datasource": "git-tags", + "depName": "user/repo", + "depType": "gitTags", + "lookupName": "https://github.com/user/repo.git", + "replaceString": "refs/tags/v0.5.1", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": "16.04", + "datasource": "docker", + "depName": "ubuntu", + "depType": "docker", + "replaceString": "ubuntu:16.04", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": "sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077", + "currentValue": "3.7", + "datasource": "docker", + "depName": "python", + "depType": "docker", + "replaceString": "python:3.7@sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077", + }, +] +`; + +exports[`manager/azure-pipelines/extract extractRepository() should extract repository information 1`] = ` +Object { + "autoReplaceStringTemplate": "refs/tags/{{newValue}}", + "currentValue": "v1.0.0", + "datasource": "git-tags", + "depName": "user/repo", + "depType": "gitTags", + "lookupName": "https://github.com/user/repo.git", + "replaceString": "refs/tags/v1.0.0", +} +`; diff --git a/lib/manager/azure-pipelines/extract.spec.ts b/lib/manager/azure-pipelines/extract.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a354466f09734087cd8da1c8a5c5640b461eb0ea --- /dev/null +++ b/lib/manager/azure-pipelines/extract.spec.ts @@ -0,0 +1,103 @@ +import { readFileSync } from 'fs'; +import { + extractContainer, + extractPackageFile, + extractRepository, + parseAzurePipelines, +} from './extract'; + +const azurePipelines = readFileSync( + 'lib/manager/azure-pipelines/__fixtures__/azure-pipelines.yaml', + 'utf8' +); + +const azurePipelinesInvalid = readFileSync( + 'lib/manager/azure-pipelines/__fixtures__/azure-pipelines-invalid.yaml', + 'utf8' +); + +const azurePipelinesNoDependency = readFileSync( + 'lib/manager/azure-pipelines/__fixtures__/azure-pipelines-no-dependency.yaml', + 'utf8' +); + +describe('manager/azure-pipelines/extract', () => { + it('should parse a valid azure-pipelines file', () => { + const file = parseAzurePipelines(azurePipelines); + expect(file).not.toBeNull(); + }); + + it('return null on an invalid file', () => { + const file = parseAzurePipelines(azurePipelinesInvalid); + expect(file).toBeNull(); + }); + + describe('extractRepository()', () => { + it('should extract repository information', () => { + expect( + extractRepository({ + type: 'github', + name: 'user/repo', + ref: 'refs/tags/v1.0.0', + }) + ).toMatchSnapshot(); + }); + + it('should return null when repository type is not github', () => { + expect( + extractRepository({ + type: 'bitbucket', + name: 'user/repo', + ref: 'refs/tags/v1.0.0', + }) + ).toBeNull(); + }); + + it('should return null when reference is not defined', () => { + expect( + extractRepository({ + type: 'github', + name: 'user/repo', + ref: null, + }) + ).toBeNull(); + }); + + it('should return null when reference is invalid tag format', () => { + expect( + extractRepository({ + type: 'github', + name: 'user/repo', + ref: 'refs/head/master', + }) + ).toBeNull(); + }); + }); + + describe('extractContainer()', () => { + it('should extract container information', () => { + expect( + extractContainer({ + image: 'ubuntu:16.04', + }) + ).toMatchSnapshot(); + }); + it('should return null if image field is missing', () => { + expect(extractContainer({ image: null })).toBeNull(); + }); + }); + + describe('extractPackageFile()', () => { + it('returns null for invalid azure pipelines files', () => { + expect(extractPackageFile('')).toBeNull(); + }); + it('extracts dependencies', () => { + const res = extractPackageFile(azurePipelines); + expect(res.deps).toMatchSnapshot(); + expect(res.deps).toHaveLength(3); + }); + it('should return null when there is no dependency found', () => { + expect(extractPackageFile(azurePipelinesNoDependency)).toBeNull(); + }); + }); +}); diff --git a/lib/manager/azure-pipelines/extract.ts b/lib/manager/azure-pipelines/extract.ts new file mode 100644 index 0000000000000000000000000000000000000000..d94c3881277e73a789b59c380d93c7190c0c64c8 --- /dev/null +++ b/lib/manager/azure-pipelines/extract.ts @@ -0,0 +1,117 @@ +import { safeLoad } from 'js-yaml'; +import * as datasourceGitTags from '../../datasource/git-tags'; +import { logger } from '../../logger'; +import { PackageDependency, PackageFile } from '../common'; +import { getDep } from '../dockerfile/extract'; + +interface Container { + image: string; +} + +interface Repository { + type: 'git' | 'github' | 'bitbucket'; + name: string; + ref: string; +} + +interface Resources { + repositories: Repository[]; + containers: Container[]; +} + +interface AzurePipelines { + resources: Resources; +} + +export function extractRepository( + repository: Repository +): PackageDependency | null { + if (repository.type !== 'github') { + return null; + } + + if (!repository.ref?.startsWith('refs/tags/')) { + return null; + } + + return { + autoReplaceStringTemplate: 'refs/tags/{{newValue}}', + currentValue: repository.ref.replace('refs/tags/', ''), + datasource: datasourceGitTags.id, + depName: repository.name, + depType: 'gitTags', + lookupName: `https://github.com/${repository.name}.git`, + replaceString: repository.ref, + }; +} + +export function extractContainer( + container: Container +): PackageDependency | null { + if (!container.image) { + return null; + } + + const dep = getDep(container.image); + logger.debug( + { + depName: dep.depName, + currentValue: dep.currentValue, + currentDigest: dep.currentDigest, + }, + 'Azure pipelines docker image' + ); + dep.depType = 'docker'; + + return dep; +} + +export function parseAzurePipelines(content: string): AzurePipelines | null { + let pkg = null; + try { + pkg = safeLoad(content); + } catch (err) /* istanbul ignore next */ { + logger.error({ err }, 'Error parsing azure-pipelines content'); + return null; + } + + if (!pkg || !pkg.resources) { + return null; + } + + pkg.resources.containers = pkg.resources.containers || []; + pkg.resources.repositories = pkg.resources.repositories || []; + + return pkg; +} + +export function extractPackageFile(content: string): PackageFile | null { + logger.trace('azurePipelines.extractPackageFile()'); + const deps: PackageDependency[] = []; + + const pkg = parseAzurePipelines(content); + if (!pkg) { + return null; + } + + // grab the repositories tags + for (const repository of pkg.resources.repositories) { + const dep = extractRepository(repository); + if (dep) { + deps.push(dep); + } + } + + // grab the containers tags + for (const container of pkg.resources.containers) { + const dep = extractContainer(container); + if (dep) { + deps.push(dep); + } + } + + if (!deps.length) { + return null; + } + return { deps }; +} diff --git a/lib/manager/azure-pipelines/index.ts b/lib/manager/azure-pipelines/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..adbfba940a0e4158be1d67e9454d0bf1d9c706e9 --- /dev/null +++ b/lib/manager/azure-pipelines/index.ts @@ -0,0 +1,5 @@ +export { extractPackageFile } from './extract'; + +export const defaultConfig = { + fileMatch: ['azure.*pipelines?.*\\.ya?ml$'], +}; diff --git a/lib/manager/azure-pipelines/readme.md b/lib/manager/azure-pipelines/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..4207eae6e595535b8306c303867567408c05a583 --- /dev/null +++ b/lib/manager/azure-pipelines/readme.md @@ -0,0 +1,31 @@ +The `azure-pipelines` manager extracts container and repository resources from the `resources:` block. For example: + +```yaml +resources: + repositories: + - type: github + name: renovate/renovate + ref: refs/heads/master + - type: github + name: user/repo + ref: refs/tags/v0.5.1 + containers: + - container: linux + image: ubuntu:16.04 + - container: python + image: python:3.7@sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077 +``` + +More about the resources block can be found on the [Azure pipelines documentation](https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema%2Cparameter-schema#resources). + +Files that are processed by the manager includes: + +- `.azure-pipelines/**/*.yaml` +- `.azure-pipelines.yaml` +- `.azure-pipelines.yml` +- `azure-pipelines/**/*.yaml` +- `azure-pipelines.yaml` +- `azure-pipelines.yml` +- `azure-pipeline/**/*.yaml` +- `azure-pipeline.yaml` +- `azure-pipeline.yml`