From 62e7fe3ac232c5559cfccf0c51a38715637dd253 Mon Sep 17 00:00:00 2001 From: Alexandre Gaudreault <47184027+agaudreault-jive@users.noreply.github.com> Date: Mon, 22 Mar 2021 11:08:52 -0400 Subject: [PATCH] feat(jenkins-plugins): support yaml file format (#9069) Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: Michael Kriese --- lib/manager/jenkins/__fixtures__/empty.yaml | 0 lib/manager/jenkins/__fixtures__/invalid.yaml | 4 + lib/manager/jenkins/__fixtures__/plugins.yaml | 26 +++++ .../__snapshots__/extract.spec.ts.snap | 60 +++++++++- lib/manager/jenkins/extract.spec.ts | 43 ++++++-- lib/manager/jenkins/extract.ts | 103 ++++++++++++++++-- lib/manager/jenkins/index.ts | 2 +- lib/manager/jenkins/readme.md | 15 +-- lib/manager/jenkins/types.ts | 19 ++++ 9 files changed, 239 insertions(+), 33 deletions(-) create mode 100644 lib/manager/jenkins/__fixtures__/empty.yaml create mode 100644 lib/manager/jenkins/__fixtures__/invalid.yaml create mode 100644 lib/manager/jenkins/__fixtures__/plugins.yaml create mode 100644 lib/manager/jenkins/types.ts diff --git a/lib/manager/jenkins/__fixtures__/empty.yaml b/lib/manager/jenkins/__fixtures__/empty.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/lib/manager/jenkins/__fixtures__/invalid.yaml b/lib/manager/jenkins/__fixtures__/invalid.yaml new file mode 100644 index 00000000000000..6b0e8ec61b863f --- /dev/null +++ b/lib/manager/jenkins/__fixtures__/invalid.yaml @@ -0,0 +1,4 @@ +## This is an invalid YAML document + +text and plugins are not supported and +invalid-plugin:0.0.0 diff --git a/lib/manager/jenkins/__fixtures__/plugins.yaml b/lib/manager/jenkins/__fixtures__/plugins.yaml new file mode 100644 index 00000000000000..e1117fcbb7cb66 --- /dev/null +++ b/lib/manager/jenkins/__fixtures__/plugins.yaml @@ -0,0 +1,26 @@ +plugins: + - artifactId: git + source: + version: latest + - artifactId: job-import-plugin + source: + version: '2.10' + - artifactId: invalid-version-plugin + source: + version: 2.10 # Float will cause the version to be 2.1 and will be ignored with a warning + - artifactId: ignore-plugin + source: + version: '2.10' + renovate: + ignore: true + - artifactId: docker + - artifactId: cloudbees-bitbucket-branch-source + source: + version: experimental + - artifactId: script-security + source: + url: http://ftp-chi.osuosl.org/pub/jenkins/plugins/script-security/1.56/script-security.hpi + - artifactId: workflow-step-api + groupId: org.jenkins-ci.plugins.workflow + source: + version: 2.19-rc289.d09828a05a74 diff --git a/lib/manager/jenkins/__snapshots__/extract.spec.ts.snap b/lib/manager/jenkins/__snapshots__/extract.spec.ts.snap index af450c7d8644ab..73d6523922e672 100644 --- a/lib/manager/jenkins/__snapshots__/extract.spec.ts.snap +++ b/lib/manager/jenkins/__snapshots__/extract.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`manager/jenkins/extract extractPackageFile() extracts multiple image lines 1`] = ` +exports[`manager/jenkins/extract extractPackageFile() extracts multiple image lines in text format 1`] = ` Array [ Object { "currentValue": "1.2.3", @@ -42,3 +42,61 @@ Array [ }, ] `; + +exports[`manager/jenkins/extract extractPackageFile() extracts multiple image lines in yaml format 1`] = ` +Array [ + Object { + "currentValue": "latest", + "datasource": "jenkins-plugins", + "depName": "git", + "skipReason": "unsupported-version", + "versioning": "docker", + }, + Object { + "currentValue": "2.10", + "datasource": "jenkins-plugins", + "depName": "job-import-plugin", + "versioning": "docker", + }, + Object { + "currentValue": "2.1", + "datasource": "jenkins-plugins", + "depName": "invalid-version-plugin", + "skipReason": "invalid-version", + "versioning": "docker", + }, + Object { + "currentValue": "2.10", + "datasource": "jenkins-plugins", + "depName": "ignore-plugin", + "skipReason": "ignored", + "versioning": "docker", + }, + Object { + "datasource": "jenkins-plugins", + "depName": "docker", + "skipReason": "no-version", + "versioning": "docker", + }, + Object { + "currentValue": "experimental", + "datasource": "jenkins-plugins", + "depName": "cloudbees-bitbucket-branch-source", + "skipReason": "unsupported-version", + "versioning": "docker", + }, + Object { + "datasource": "jenkins-plugins", + "depName": "script-security", + "skipReason": "internal-package", + "versioning": "docker", + }, + Object { + "currentValue": "2.19-rc289.d09828a05a74", + "datasource": "jenkins-plugins", + "depName": "workflow-step-api", + "skipReason": "unsupported-version", + "versioning": "docker", + }, +] +`; diff --git a/lib/manager/jenkins/extract.spec.ts b/lib/manager/jenkins/extract.spec.ts index 1e4be3392c0895..36775a6e261ffa 100644 --- a/lib/manager/jenkins/extract.spec.ts +++ b/lib/manager/jenkins/extract.spec.ts @@ -2,27 +2,56 @@ import { readFileSync } from 'fs'; import { getName } from '../../../test/util'; import { extractPackageFile } from './extract'; -const pluginsFile = readFileSync( +const invalidYamlFile = readFileSync( + 'lib/manager/jenkins/__fixtures__/invalid.yaml', + 'utf8' +); + +const pluginsTextFile = readFileSync( 'lib/manager/jenkins/__fixtures__/plugins.txt', 'utf8' ); +const pluginsYamlFile = readFileSync( + 'lib/manager/jenkins/__fixtures__/plugins.yaml', + 'utf8' +); -const pluginsEmptyFile = readFileSync( +const pluginsEmptyTextFile = readFileSync( 'lib/manager/jenkins/__fixtures__/empty.txt', 'utf8' ); +const pluginsEmptyYamlFile = readFileSync( + 'lib/manager/jenkins/__fixtures__/empty.yaml', + 'utf8' +); describe(getName(__filename), () => { describe('extractPackageFile()', () => { - it('returns empty list for an empty file', () => { - const res = extractPackageFile(pluginsEmptyFile); - expect(res.deps).toHaveLength(0); + it('returns empty list for an empty text file', () => { + const res = extractPackageFile(pluginsEmptyTextFile, 'path/file.txt'); + expect(res).toBeNull(); }); - it('extracts multiple image lines', () => { - const res = extractPackageFile(pluginsFile); + it('returns empty list for an empty yaml file', () => { + const res = extractPackageFile(pluginsEmptyYamlFile, 'path/file.yaml'); + expect(res).toBeNull(); + }); + + it('returns empty list for an invalid yaml file', () => { + const res = extractPackageFile(invalidYamlFile, 'path/file.yaml'); + expect(res).toBeNull(); + }); + + it('extracts multiple image lines in text format', () => { + const res = extractPackageFile(pluginsTextFile, 'path/file.txt'); expect(res.deps).toMatchSnapshot(); expect(res.deps).toHaveLength(6); }); + + it('extracts multiple image lines in yaml format', () => { + const res = extractPackageFile(pluginsYamlFile, 'path/file.yml'); + expect(res.deps).toMatchSnapshot(); + expect(res.deps).toHaveLength(8); + }); }); }); diff --git a/lib/manager/jenkins/extract.ts b/lib/manager/jenkins/extract.ts index 497b465237286f..c99cdc2750a244 100644 --- a/lib/manager/jenkins/extract.ts +++ b/lib/manager/jenkins/extract.ts @@ -1,33 +1,112 @@ +import yaml from 'js-yaml'; import * as datasourceJenkins from '../../datasource/jenkins-plugins'; import { logger } from '../../logger'; import { SkipReason } from '../../types'; import { isSkipComment } from '../../util/ignore'; import * as dockerVersioning from '../../versioning/docker'; import type { PackageDependency, PackageFile } from '../types'; +import type { JenkinsPlugin, JenkinsPlugins } from './types'; -export function extractPackageFile(content: string): PackageFile | null { - logger.trace('jenkins.extractPackageFile()'); +const YamlExtension = /\.ya?ml$/; + +function getDependency(plugin: JenkinsPlugin): PackageDependency { + const dep: PackageDependency = { + datasource: datasourceJenkins.id, + versioning: dockerVersioning.id, + depName: plugin.artifactId, + }; + + if (plugin.source?.version) { + dep.currentValue = plugin.source.version.toString(); + if (typeof plugin.source.version !== 'string') { + dep.skipReason = SkipReason.InvalidVersion; + logger.warn( + { dep }, + 'Jenkins plugin dependency version is not a string and will be ignored' + ); + } + } else { + dep.skipReason = SkipReason.NoVersion; + } + + if ( + plugin.source?.version === 'latest' || + plugin.source?.version === 'experimental' || + plugin.groupId + ) { + dep.skipReason = SkipReason.UnsupportedVersion; + } + + if (plugin.source?.url) { + dep.skipReason = SkipReason.InternalPackage; + } + + if (!dep.skipReason && plugin.renovate?.ignore) { + dep.skipReason = SkipReason.Ignored; + } + + logger.debug({ dep }, 'Jenkins plugin dependency'); + return dep; +} + +function extractYaml(content: string): PackageDependency[] { + const deps: PackageDependency[] = []; + + try { + const doc = yaml.safeLoad(content, { json: true }) as JenkinsPlugins; + if (doc?.plugins) { + for (const plugin of doc.plugins) { + if (plugin.artifactId) { + const dep = getDependency(plugin); + deps.push(dep); + } + } + } + } catch (err) { + logger.warn({ err }, 'Error parsing Jenkins plugins'); + } + return deps; +} + +function extractText(content: string): PackageDependency[] { const deps: PackageDependency[] = []; const regex = /^\s*(?[\d\w-]+):(?[^#\s]+)[#\s]*(?.*)$/; for (const line of content.split('\n')) { const match = regex.exec(line); - if (match) { const { depName, currentValue, comment } = match.groups; - const dep: PackageDependency = { - datasource: datasourceJenkins.id, - versioning: dockerVersioning.id, - depName, - currentValue, + const plugin: JenkinsPlugin = { + artifactId: depName, + source: { + version: currentValue, + }, + renovate: { + ignore: isSkipComment(comment), + }, }; - - if (isSkipComment(comment)) { - dep.skipReason = SkipReason.Ignored; - } + const dep = getDependency(plugin); deps.push(dep); } } + return deps; +} + +export function extractPackageFile( + content: string, + fileName: string +): PackageFile | null { + logger.trace('jenkins.extractPackageFile()'); + const deps: PackageDependency[] = []; + if (YamlExtension.test(fileName)) { + deps.push(...extractYaml(content)); + } else { + deps.push(...extractText(content)); + } + + if (deps.length === 0) { + return null; + } return { deps }; } diff --git a/lib/manager/jenkins/index.ts b/lib/manager/jenkins/index.ts index c33e432967973d..6b8b845d29a5ee 100644 --- a/lib/manager/jenkins/index.ts +++ b/lib/manager/jenkins/index.ts @@ -1,5 +1,5 @@ export { extractPackageFile } from './extract'; export const defaultConfig = { - fileMatch: ['(^|/)plugins\\.txt'], + fileMatch: ['(^|/)plugins\\.(txt|ya?ml)$'], }; diff --git a/lib/manager/jenkins/readme.md b/lib/manager/jenkins/readme.md index d143762a841a86..c50aa77a43eff7 100644 --- a/lib/manager/jenkins/readme.md +++ b/lib/manager/jenkins/readme.md @@ -1,13 +1,4 @@ -The Jenkins manager supports the following format of the plugin list: +The Jenkins manager supports a custom text or YAML format of the plugin list as described [here](https://github.com/jenkinsci/plugin-installation-manager-tool#plugin-input-format). +Only versions from the main [Jenkins update center](https://updates.jenkins.io/) are supported. -```text -plugin1:1.2.3 -plugin2:4.5 # this is a comment - -# this line is ignored - -# Renovate will not upgrade the following dependency: -plugin3:7.8.9 # renovate:ignore -``` - -There's no strict specification on the name of the files, but usually it's `plugins.txt` +There are no strict filename rules, the convention is to name the file `plugins.txt` or `plugins.yaml`. diff --git a/lib/manager/jenkins/types.ts b/lib/manager/jenkins/types.ts new file mode 100644 index 00000000000000..85f0bc3fbcf2f2 --- /dev/null +++ b/lib/manager/jenkins/types.ts @@ -0,0 +1,19 @@ +export interface JenkinsPluginRenovate { + ignore?: boolean; +} + +export interface JenkinsPluginSource { + version?: string; + url?: string; +} + +export interface JenkinsPlugin { + artifactId?: string; + groupId?: string; + source?: JenkinsPluginSource; + renovate?: JenkinsPluginRenovate; +} + +export interface JenkinsPlugins { + plugins?: JenkinsPlugin[]; +}