diff --git a/lib/modules/datasource/api.ts b/lib/modules/datasource/api.ts index 20c5fed2a57855f26b599a2fb4f7a7bc21799635..6d684fd115d3df01eabf9969e4882b52b7f96b07 100644 --- a/lib/modules/datasource/api.ts +++ b/lib/modules/datasource/api.ts @@ -33,6 +33,7 @@ import { NugetDatasource } from './nuget'; import { OrbDatasource } from './orb'; import { PackagistDatasource } from './packagist'; import { PodDatasource } from './pod'; +import { PuppetForgeDatasource } from './puppet-forge'; import { PypiDatasource } from './pypi'; import { RepologyDatasource } from './repology'; import { RubyVersionDatasource } from './ruby-version'; @@ -81,6 +82,7 @@ api.set(NugetDatasource.id, new NugetDatasource()); api.set(OrbDatasource.id, new OrbDatasource()); api.set(PackagistDatasource.id, new PackagistDatasource()); api.set(PodDatasource.id, new PodDatasource()); +api.set(PuppetForgeDatasource.id, new PuppetForgeDatasource()); api.set(PypiDatasource.id, new PypiDatasource()); api.set(RepologyDatasource.id, new RepologyDatasource()); api.set(RubyVersionDatasource.id, new RubyVersionDatasource()); diff --git a/lib/modules/datasource/puppet-forge/__fixtures__/puppetforge-deprecated-for.json b/lib/modules/datasource/puppet-forge/__fixtures__/puppetforge-deprecated-for.json new file mode 100644 index 0000000000000000000000000000000000000000..111f50c16207f7fd6d91ac79ebe6f428495ded90 --- /dev/null +++ b/lib/modules/datasource/puppet-forge/__fixtures__/puppetforge-deprecated-for.json @@ -0,0 +1,36 @@ +{ + "uri": "/v3/modules/puppetlabs-apache", + "slug": "puppetlabs-apache", + "name": "apache", + "downloads": 11063567, + "created_at": "2010-05-20 22:43:19 -0700", + "updated_at": "2021-10-11 07:47:24 -0700", + "deprecated_at": null, + "deprecated_for": "use another module ...", + "superseded_by": null, + "supported": true, + "endorsement": "supported", + "module_group": "base", + "owner": { + "uri": "/v3/users/puppetlabs", + "slug": "puppetlabs", + "username": "puppetlabs", + "gravatar_id": "fdd009b7c1ec96e088b389f773e87aec" + }, + "premium": false, + "releases": [ + { + "uri": "/v3/releases/puppetlabs-apache-7.0.0", + "slug": "puppetlabs-apache-7.0.0", + "version": "7.0.0", + "supported": false, + "created_at": "2021-10-11 07:47:24 -0700", + "deleted_at": null, + "file_uri": "/v3/files/puppetlabs-apache-7.0.0.tar.gz", + "file_size": 331833 + } + ], + "feedback_score": 83, + "homepage_url": "https://github.com/puppetlabs/puppetlabs-apache", + "issues_url": "https://tickets.puppetlabs.com/browse/MODULES" +} diff --git a/lib/modules/datasource/puppet-forge/__fixtures__/puppetforge-no-releases.json b/lib/modules/datasource/puppet-forge/__fixtures__/puppetforge-no-releases.json new file mode 100644 index 0000000000000000000000000000000000000000..7adc234b035ac914d259e5b719fd2e6f3213c37b --- /dev/null +++ b/lib/modules/datasource/puppet-forge/__fixtures__/puppetforge-no-releases.json @@ -0,0 +1,26 @@ +{ + "uri": "/v3/modules/puppetlabs-apache", + "slug": "puppetlabs-apache", + "name": "apache", + "downloads": 11063567, + "created_at": "2010-05-20 22:43:19 -0700", + "updated_at": "2021-10-11 07:47:24 -0700", + "deprecated_at": null, + "deprecated_for": null, + "superseded_by": null, + "supported": true, + "endorsement": "supported", + "module_group": "base", + "owner": { + "uri": "/v3/users/puppetlabs", + "slug": "puppetlabs", + "username": "puppetlabs", + "gravatar_id": "fdd009b7c1ec96e088b389f773e87aec" + }, + "premium": false, + "releases": [ + ], + "feedback_score": 83, + "homepage_url": "https://github.com/puppetlabs/puppetlabs-apache", + "issues_url": "https://tickets.puppetlabs.com/browse/MODULES" +} diff --git a/lib/modules/datasource/puppet-forge/__fixtures__/puppetforge-response-with-nulls.json b/lib/modules/datasource/puppet-forge/__fixtures__/puppetforge-response-with-nulls.json new file mode 100644 index 0000000000000000000000000000000000000000..923a9e61aa9f27655288322fd9327e8a9bd58555 --- /dev/null +++ b/lib/modules/datasource/puppet-forge/__fixtures__/puppetforge-response-with-nulls.json @@ -0,0 +1,36 @@ +{ + "uri": "/v3/modules/puppetlabs-apache", + "slug": "puppetlabs-apache", + "name": "apache", + "downloads": 11063567, + "created_at": "2010-05-20 22:43:19 -0700", + "updated_at": "2021-10-11 07:47:24 -0700", + "deprecated_at": null, + "deprecated_for": null, + "superseded_by": null, + "supported": true, + "endorsement": null, + "module_group": "base", + "owner": { + "uri": "/v3/users/puppetlabs", + "slug": "puppetlabs", + "username": "puppetlabs", + "gravatar_id": "fdd009b7c1ec96e088b389f773e87aec" + }, + "premium": false, + "releases": [ + { + "uri": "/v3/releases/puppetlabs-apache-7.0.0", + "slug": "puppetlabs-apache-7.0.0", + "version": "7.0.0", + "supported": false, + "created_at": "2021-10-11 07:47:24 -0700", + "deleted_at": null, + "file_uri": "/v3/files/puppetlabs-apache-7.0.0.tar.gz", + "file_size": 331833 + } + ], + "feedback_score": 83, + "homepage_url": "https://github.com/puppetlabs/puppetlabs-apache", + "issues_url": "https://tickets.puppetlabs.com/browse/MODULES" +} diff --git a/lib/modules/datasource/puppet-forge/__fixtures__/puppetforge-response.json b/lib/modules/datasource/puppet-forge/__fixtures__/puppetforge-response.json new file mode 100644 index 0000000000000000000000000000000000000000..7d9acf0c89d128c3be4d37bde109071c99c767dd --- /dev/null +++ b/lib/modules/datasource/puppet-forge/__fixtures__/puppetforge-response.json @@ -0,0 +1,66 @@ +{ + "uri": "/v3/modules/puppetlabs-apache", + "slug": "puppetlabs-apache", + "name": "apache", + "downloads": 11063567, + "created_at": "2010-05-20 22:43:19 -0700", + "updated_at": "2021-10-11 07:47:24 -0700", + "deprecated_at": null, + "deprecated_for": null, + "superseded_by": null, + "supported": true, + "endorsement": "supported", + "module_group": "base", + "owner": { + "uri": "/v3/users/puppetlabs", + "slug": "puppetlabs", + "username": "puppetlabs", + "gravatar_id": "fdd009b7c1ec96e088b389f773e87aec" + }, + "premium": false, + "releases": [ + { + "uri": "/v3/releases/puppetlabs-apache-7.0.0", + "slug": "puppetlabs-apache-7.0.0", + "version": "7.0.0", + "supported": false, + "created_at": "2021-10-11 07:47:24 -0700", + "deleted_at": null, + "file_uri": "/v3/files/puppetlabs-apache-7.0.0.tar.gz", + "file_size": 331833 + }, + { + "uri": "/v3/releases/puppetlabs-apache-6.5.1", + "slug": "puppetlabs-apache-6.5.1", + "version": "6.5.1", + "supported": false, + "created_at": "2021-08-25 04:16:27 -0700", + "deleted_at": null, + "file_uri": "/v3/files/puppetlabs-apache-6.5.1.tar.gz", + "file_size": 331386 + }, + { + "uri": "/v3/releases/puppetlabs-apache-6.5.0", + "slug": "puppetlabs-apache-6.5.0", + "version": "6.5.0", + "supported": false, + "created_at": "2021-08-24 08:20:22 -0700", + "deleted_at": null, + "file_uri": "/v3/files/puppetlabs-apache-6.5.0.tar.gz", + "file_size": 331330 + }, + { + "uri": "/v3/releases/puppetlabs-apache-6.4.0", + "slug": "puppetlabs-apache-6.4.0", + "version": "6.4.0", + "supported": false, + "created_at": "2021-08-02 06:49:41 -0700", + "deleted_at": null, + "file_uri": "/v3/files/puppetlabs-apache-6.4.0.tar.gz", + "file_size": 331201 + } + ], + "feedback_score": 83, + "homepage_url": "https://github.com/puppetlabs/puppetlabs-apache", + "issues_url": "https://tickets.puppetlabs.com/browse/MODULES" +} diff --git a/lib/modules/datasource/puppet-forge/common.ts b/lib/modules/datasource/puppet-forge/common.ts new file mode 100644 index 0000000000000000000000000000000000000000..9480006c60ee9a998d0cf0a507b792274051e90c --- /dev/null +++ b/lib/modules/datasource/puppet-forge/common.ts @@ -0,0 +1 @@ +export const PUPPET_FORGE = 'https://forgeapi.puppet.com'; diff --git a/lib/modules/datasource/puppet-forge/index.spec.ts b/lib/modules/datasource/puppet-forge/index.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9bf5bdb481e4f3c077f6352807c13d84701046a --- /dev/null +++ b/lib/modules/datasource/puppet-forge/index.spec.ts @@ -0,0 +1,225 @@ +import { getPkgReleases } from '..'; +import { Fixtures } from '../../../../test/fixtures'; +import * as httpMock from '../../../../test/http-mock'; +import { PuppetForgeDatasource } from '.'; + +const puppetforgeReleases = Fixtures.get('puppetforge-response.json'); + +const datasource = PuppetForgeDatasource.id; + +describe('modules/datasource/puppet-forge/index', () => { + describe('getReleases', () => { + it('should use default forge if no other provided', async () => { + httpMock + .scope('https://forgeapi.puppet.com') + .get('/v3/modules/puppetlabs-apache') + .query({ exclude_fields: 'current_release' }) + .reply(200, puppetforgeReleases); + + const res = await getPkgReleases({ + datasource, + depName: 'puppetlabs/apache', + packageName: 'puppetlabs/apache', + }); + expect(res).toMatchObject({ + registryUrl: 'https://forgeapi.puppet.com', + releases: [ + { version: '6.4.0' }, + { version: '6.5.0' }, + { version: '6.5.1' }, + { version: '7.0.0' }, + ], + }); + }); + + it('parses real data', async () => { + httpMock + .scope('https://forgeapi.puppet.com') + .get('/v3/modules/puppetlabs-apache') + .query({ exclude_fields: 'current_release' }) + .reply(200, puppetforgeReleases); + + const res = await getPkgReleases({ + datasource, + depName: 'puppetlabs/apache', + packageName: 'puppetlabs/apache', + registryUrls: ['https://forgeapi.puppet.com'], + }); + + expect(res).toEqual({ + registryUrl: 'https://forgeapi.puppet.com', + releases: [ + { + downloadUrl: '/v3/files/puppetlabs-apache-6.4.0.tar.gz', + registryUrl: 'https://forgeapi.puppet.com', + releaseTimestamp: '2021-08-02T13:49:41.000Z', + version: '6.4.0', + }, + { + downloadUrl: '/v3/files/puppetlabs-apache-6.5.0.tar.gz', + registryUrl: 'https://forgeapi.puppet.com', + releaseTimestamp: '2021-08-24T15:20:22.000Z', + version: '6.5.0', + }, + { + downloadUrl: '/v3/files/puppetlabs-apache-6.5.1.tar.gz', + registryUrl: 'https://forgeapi.puppet.com', + releaseTimestamp: '2021-08-25T11:16:27.000Z', + version: '6.5.1', + }, + { + downloadUrl: '/v3/files/puppetlabs-apache-7.0.0.tar.gz', + registryUrl: 'https://forgeapi.puppet.com', + releaseTimestamp: '2021-10-11T14:47:24.000Z', + version: '7.0.0', + }, + ], + sourceUrl: 'https://github.com/puppetlabs/puppetlabs-apache', + }); + }); + + it('has a deprecated for reason', async () => { + httpMock + .scope('https://forgeapi.puppet.com') + .get('/v3/modules/puppetlabs-apache') + .query({ exclude_fields: 'current_release' }) + .reply(200, Fixtures.get('puppetforge-deprecated-for.json')); + + const res = await getPkgReleases({ + datasource, + depName: 'puppetlabs/apache', + packageName: 'puppetlabs/apache', + }); + expect(res).toEqual({ + deprecationMessage: 'use another module ...', + registryUrl: 'https://forgeapi.puppet.com', + releases: [ + { + downloadUrl: '/v3/files/puppetlabs-apache-7.0.0.tar.gz', + registryUrl: 'https://forgeapi.puppet.com', + releaseTimestamp: '2021-10-11T14:47:24.000Z', + version: '7.0.0', + }, + ], + sourceUrl: 'https://github.com/puppetlabs/puppetlabs-apache', + }); + }); + }); + + // https://forgeapi.puppet.com/#operation/getModule + it('should return null if lookup fails 400', async () => { + httpMock + .scope('https://forgeapi.puppet.com') + .get('/v3/modules/foobar') + .query({ exclude_fields: 'current_release' }) + .reply(400); + + const res = await getPkgReleases({ + datasource, + depName: 'foobar', + registryUrls: ['https://forgeapi.puppet.com'], + }); + expect(res).toBeNull(); + }); + + // https://forgeapi.puppet.com/#operation/getModule + it('should return null if lookup fails', async () => { + httpMock + .scope('https://forgeapi.puppet.com') + .get('/v3/modules/foobar') + .query({ exclude_fields: 'current_release' }) + .reply(404); + const res = await getPkgReleases({ + datasource, + depName: 'foobar', + registryUrls: ['https://forgeapi.puppet.com'], + }); + expect(res).toBeNull(); + }); + + it('should fetch package info from custom registry', async () => { + httpMock + .scope('https://puppet.mycustomregistry.com', {}) + .get('/v3/modules/foobar') + .query({ exclude_fields: 'current_release' }) + .reply(200, puppetforgeReleases); + const registryUrls = ['https://puppet.mycustomregistry.com']; + const res = await getPkgReleases({ + datasource, + depName: 'foobar', + registryUrls, + }); + + expect(res).toEqual({ + registryUrl: 'https://puppet.mycustomregistry.com', + releases: [ + { + downloadUrl: '/v3/files/puppetlabs-apache-6.4.0.tar.gz', + registryUrl: 'https://puppet.mycustomregistry.com', + releaseTimestamp: '2021-08-02T13:49:41.000Z', + version: '6.4.0', + }, + { + downloadUrl: '/v3/files/puppetlabs-apache-6.5.0.tar.gz', + registryUrl: 'https://puppet.mycustomregistry.com', + releaseTimestamp: '2021-08-24T15:20:22.000Z', + version: '6.5.0', + }, + { + downloadUrl: '/v3/files/puppetlabs-apache-6.5.1.tar.gz', + registryUrl: 'https://puppet.mycustomregistry.com', + releaseTimestamp: '2021-08-25T11:16:27.000Z', + version: '6.5.1', + }, + { + downloadUrl: '/v3/files/puppetlabs-apache-7.0.0.tar.gz', + registryUrl: 'https://puppet.mycustomregistry.com', + releaseTimestamp: '2021-10-11T14:47:24.000Z', + version: '7.0.0', + }, + ], + sourceUrl: 'https://github.com/puppetlabs/puppetlabs-apache', + }); + }); + + it('load all possible null values', async () => { + httpMock + .scope('https://forgeapi.puppet.com', {}) + .get('/v3/modules/foobar') + .query({ exclude_fields: 'current_release' }) + .reply(200, Fixtures.get('puppetforge-response-with-nulls.json')); + + const res = await getPkgReleases({ + datasource, + depName: 'foobar', + }); + + expect(res).toEqual({ + registryUrl: 'https://forgeapi.puppet.com', + releases: [ + { + downloadUrl: '/v3/files/puppetlabs-apache-7.0.0.tar.gz', + registryUrl: 'https://forgeapi.puppet.com', + releaseTimestamp: '2021-10-11T14:47:24.000Z', + version: '7.0.0', + }, + ], + sourceUrl: 'https://github.com/puppetlabs/puppetlabs-apache', + }); + }); + + it('no releases available -> return null', async () => { + httpMock + .scope('https://forgeapi.puppet.com', {}) + .get('/v3/modules/foobar') + .query({ exclude_fields: 'current_release' }) + .reply(200, Fixtures.get('puppetforge-no-releases.json')); + + const res = await getPkgReleases({ + datasource, + depName: 'foobar', + }); + + expect(res).toBeNull(); + }); +}); diff --git a/lib/modules/datasource/puppet-forge/index.ts b/lib/modules/datasource/puppet-forge/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..beeb323001cf7d57613047d01bdd82742cdddfdc --- /dev/null +++ b/lib/modules/datasource/puppet-forge/index.ts @@ -0,0 +1,61 @@ +import { Datasource } from '../datasource'; +import type { GetReleasesConfig, Release, ReleaseResult } from '../types'; +import { PUPPET_FORGE } from './common'; +import type { PuppetModule } from './types'; + +export class PuppetForgeDatasource extends Datasource { + static id = 'puppet-forge'; + + constructor() { + super(PuppetForgeDatasource.id); + } + + override readonly defaultRegistryUrls = [PUPPET_FORGE]; + + async getReleases({ + packageName, + registryUrl, + }: GetReleasesConfig): Promise<ReleaseResult | null> { + // https://forgeapi.puppet.com + const moduleSlug = packageName.replace('/', '-'); + const url = `${registryUrl}/v3/modules/${moduleSlug}?exclude_fields=current_release`; + + let module: PuppetModule; + + try { + const response = await this.http.getJson<PuppetModule>(url); + module = response.body; + } catch (err) { + this.handleGenericErrors(err); + } + + const releases: Release[] = module?.releases?.map((release) => ({ + version: release.version, + downloadUrl: release.file_uri, + releaseTimestamp: release.created_at, + registryUrl, + })); + + if (!releases?.length) { + return null; + } + + return PuppetForgeDatasource.createReleaseResult(releases, module); + } + + static createReleaseResult( + releases: Release[], + module: PuppetModule + ): ReleaseResult { + const result: ReleaseResult = { + releases, + homepage: module.homepage_url, + }; + + if (module.deprecated_for) { + result.deprecationMessage = module.deprecated_for; + } + + return result; + } +} diff --git a/lib/modules/datasource/puppet-forge/types.ts b/lib/modules/datasource/puppet-forge/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..34afc0e9c2e301d3b9b8eead207f93b5cc1d5368 --- /dev/null +++ b/lib/modules/datasource/puppet-forge/types.ts @@ -0,0 +1,89 @@ +export interface PuppetModule { + uri: string; + slug: string; + name: string; + deprecated_at: string | null; + owner: PuppetModuleOwner; + downloads: number; + created_at: string; + updated_at: string; + deprecated_for: string | null; + superseded_by: PuppetSupercededBy | null; + endorsement: PuppetEndorsement | null; + module_group: PuppetModuleGroup; + premium: boolean; + current_release: PuppetRelease; + releases: PuppetReleaseAbbreviated[]; + homepage_url: string; + issues_url: string; +} + +export type PuppetModuleAbbreviated = Pick< + PuppetModule, + 'uri' | 'slug' | 'name' | 'deprecated_at' | 'owner' +>; + +export interface PuppetRelease { + uri: string; + slug: string; + module: PuppetModuleAbbreviated; + version: string; + metadata: Record<string, any>; + tags: string[]; + pdk: boolean; + file_uri: string; + file_size: number; + file_md5: string; + file_sha256: string; + downloads: number; + readme: string; + changelog: string; + license: string; + reference: string; + pe_compatibility: string[] | null | undefined; + tasks: PuppetBoltTask[]; + plans: PuppetBoltPlan[]; + created_at: string; + updated_at: string; + deleted_at: string | null; + deleted_for: string | null; +} + +export type PuppetReleaseAbbreviated = Pick< + PuppetRelease, + | 'uri' + | 'slug' + | 'version' + | 'created_at' + | 'deleted_at' + | 'file_uri' + | 'file_size' +>; + +export interface PuppetBoltPlan { + uri: string; + name: string; + private: boolean; +} + +export interface PuppetBoltTask { + name: string; + executables: string[]; + description: string; + metadata: Record<string, any>; +} + +export interface PuppetSupercededBy { + uri: string; + slug: string; +} + +export interface PuppetModuleOwner { + uri: string; + slug: string; + username: string; + gravatar_id: string; +} + +export type PuppetEndorsement = 'supported' | 'approved' | 'partner'; +export type PuppetModuleGroup = 'base' | 'pe_only'; diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts index 59f8d9c223771e2fd4dd51036f0f08540b37d49f..cc7ff295fc3eac9a55e136b278ea9dabb86aa9dd 100644 --- a/lib/modules/manager/api.ts +++ b/lib/modules/manager/api.ts @@ -56,6 +56,7 @@ import * as pipenv from './pipenv'; import * as poetry from './poetry'; import * as preCommit from './pre-commit'; import * as pub from './pub'; +import * as puppet from './puppet'; import * as pyenv from './pyenv'; import * as regex from './regex'; import * as rubyVersion from './ruby-version'; @@ -131,6 +132,7 @@ api.set('pipenv', pipenv); api.set('poetry', poetry); api.set('pre-commit', preCommit); api.set('pub', pub); +api.set('puppet', puppet); api.set('pyenv', pyenv); api.set('regex', regex); api.set('ruby-version', rubyVersion); diff --git a/lib/modules/manager/puppet/__fixtures__/Puppetfile.git_tag b/lib/modules/manager/puppet/__fixtures__/Puppetfile.git_tag new file mode 100644 index 0000000000000000000000000000000000000000..72531282f3ca56e7b933628d9a03bb5bdfeabada --- /dev/null +++ b/lib/modules/manager/puppet/__fixtures__/Puppetfile.git_tag @@ -0,0 +1,19 @@ +mod 'apache', + :git => 'https://gitlab.com/example/project.git', + :tag => '0.9.0' + +mod 'stdlib', + :git => 'git@gitlab.com:example/project_stdlib.git', + :tag => '5.0.0' + +mod 'multiple_dirs_ssh', + :git => 'git@gitlab.com:dir1/dir2/project.git', + :tag => '1.0.0' + +mod 'multiple_dirs_https', + :git => 'https://gitlab.com/dir1/dir2/project.git', + :tag => '1.9.0' + +mod 'invalid_url', + :git => 'hello world', + :tag => '0.0.0' diff --git a/lib/modules/manager/puppet/__fixtures__/Puppetfile.github_tag b/lib/modules/manager/puppet/__fixtures__/Puppetfile.github_tag new file mode 100644 index 0000000000000000000000000000000000000000..0c4c57ab9bb6d8f4dc68f001b4cb2c276594fed3 --- /dev/null +++ b/lib/modules/manager/puppet/__fixtures__/Puppetfile.github_tag @@ -0,0 +1,7 @@ +mod 'apache', + :git => 'https://github.com/puppetlabs/puppetlabs-apache', + :tag => '0.9.0' + +mod 'stdlib', + :git => 'git@github.com:puppetlabs/puppetlabs-stdlib.git', + :tag => '5.0.0' diff --git a/lib/modules/manager/puppet/__fixtures__/Puppetfile.multiple_forges b/lib/modules/manager/puppet/__fixtures__/Puppetfile.multiple_forges new file mode 100644 index 0000000000000000000000000000000000000000..f55e46173dfa89da7263d37bb46bc9e7257e67e5 --- /dev/null +++ b/lib/modules/manager/puppet/__fixtures__/Puppetfile.multiple_forges @@ -0,0 +1,19 @@ +forge "https://forgeapi.puppetlabs.com" + +######################### +## Puppetforge Modules ## +######################### + +mod 'puppetlabs/stdlib', '8.0.0' +mod 'puppetlabs/apache', '6.5.1' +mod 'puppetlabs/puppetdb', '7.9.0' + +forge "https://some-other-puppet-forge.com" + +########################### +## Some Other Mock Forge ## +########################### + +mod 'mock/mockstdlib', '10.0.0' +mod 'mock/mockapache', '2.5.1' +mod 'mock/mockpuppetdb', '1.9.0' diff --git a/lib/modules/manager/puppet/__fixtures__/Puppetfile.with_comments b/lib/modules/manager/puppet/__fixtures__/Puppetfile.with_comments new file mode 100644 index 0000000000000000000000000000000000000000..7007f60b8f8ec3caed62781b1b8f5b5b90b50082 --- /dev/null +++ b/lib/modules/manager/puppet/__fixtures__/Puppetfile.with_comments @@ -0,0 +1,15 @@ +mod 'puppetlabs/stdlib', '8.0.0' +mod 'puppetlabs/apache', '6.5.1' # This is a "comment" +# mod 'puppetlabs/puppetdb', '7.9.0' + +mod 'apache', + :git => 'https://github.com/puppetlabs/puppetlabs-apache', +# :tag => '0.9.0' + +mod 'stdlib', +# :git => 'git@github.com:puppetlabs/puppetlabs-stdlib.git', + :tag => '5.0.0' + +mod 'stdlib2', :git => 'git@github.com:puppetlabs/puppetlabs-stdlib2.git' # This is a "comment" + # :tag => '5.0.0' + diff --git a/lib/modules/manager/puppet/common.spec.ts b/lib/modules/manager/puppet/common.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..48a050c39d4c33c74b29be6a34067804dfd8a376 --- /dev/null +++ b/lib/modules/manager/puppet/common.spec.ts @@ -0,0 +1,44 @@ +import { + RE_REPOSITORY_GENERIC_GIT_SSH_FORMAT, + parseGitOwnerRepo, +} from './common'; + +describe('modules/manager/puppet/common', () => { + describe('RE_REPOSITORY_GENERIC_GIT_SSH_FORMAT', () => { + it('access by index', () => { + const regex = RE_REPOSITORY_GENERIC_GIT_SSH_FORMAT.exec( + 'git@gitlab.com:dir1/dir2/project.git' + ); + expect(regex).not.toBeNull(); + expect(String(regex)).toBe( + 'git@gitlab.com:dir1/dir2/project.git,dir1/dir2/project.git' + ); + }); + + it('access by named group', () => { + const regex = RE_REPOSITORY_GENERIC_GIT_SSH_FORMAT.exec( + 'git@gitlab.com:dir1/dir2/project.git' + ); + expect(regex).not.toBeNull(); + expect(String(regex)).toBe( + 'git@gitlab.com:dir1/dir2/project.git,dir1/dir2/project.git' + ); + expect(regex?.groups).not.toBeNull(); + expect(regex?.groups?.repository).toBe('dir1/dir2/project.git'); + }); + }); + + describe('parseGitOwnerRepo', () => { + it('unable to parse url', () => { + expect(parseGitOwnerRepo('invalid-url-example', false)).toBeNull(); + }); + + it('parseable url', () => { + const url = parseGitOwnerRepo( + 'https://gitlab.com/example/example', + false + ); + expect(url).toBe('example/example'); + }); + }); +}); diff --git a/lib/modules/manager/puppet/common.ts b/lib/modules/manager/puppet/common.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b501d05296e79afafa67d86dc1ce51a63dc0ac7 --- /dev/null +++ b/lib/modules/manager/puppet/common.ts @@ -0,0 +1,39 @@ +import { regEx } from '../../../util/regex'; +import { parseUrl } from '../../../util/url'; + +export const RE_REPOSITORY_GENERIC_GIT_SSH_FORMAT = regEx( + /^git@[^:]*:(?<repository>.+)$/ +); + +export function parseGitOwnerRepo( + git: string, + githubUrl: boolean +): string | null { + const genericGitSsh = RE_REPOSITORY_GENERIC_GIT_SSH_FORMAT.exec(git); + + if (genericGitSsh?.groups) { + return genericGitSsh.groups.repository.replace(regEx(/\.git$/), ''); + } else { + if (githubUrl) { + return git + .replace(regEx(/^github:/), '') + .replace(regEx(/^git\+/), '') + .replace(regEx(/^https:\/\/github\.com\//), '') + .replace(regEx(/\.git$/), ''); + } + + const url = parseUrl(git); + + if (!url) { + return null; + } + + return url.pathname.replace(regEx(/\.git$/), '').replace(regEx(/^\//), ''); + } +} + +export function isGithubUrl(gitUrl: string, parsedUrl: URL | null): boolean { + return ( + parsedUrl?.host === 'github.com' || gitUrl.startsWith('git@github.com') + ); +} diff --git a/lib/modules/manager/puppet/extract.spec.ts b/lib/modules/manager/puppet/extract.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8c5c8aa8ca1fa90c2ae1fa32851af7e7c483c90c --- /dev/null +++ b/lib/modules/manager/puppet/extract.spec.ts @@ -0,0 +1,246 @@ +import { EOL } from 'os'; +import { Fixtures } from '../../../../test/fixtures'; +import { GitTagsDatasource } from '../../datasource/git-tags'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; +import { PuppetForgeDatasource } from '../../datasource/puppet-forge'; +import { extractPackageFile } from '.'; + +describe('modules/manager/puppet/extract', () => { + describe('extractPackageFile()', () => { + it('returns null for empty Puppetfile', () => { + expect(extractPackageFile('')).toBeNull(); + }); + + it('extracts multiple modules from Puppetfile without a forge', () => { + const res = extractPackageFile( + [ + "mod 'puppetlabs/stdlib', '8.0.0'", + "mod 'puppetlabs/apache', '6.5.1'", + "mod 'puppetlabs/puppetdb', '7.9.0'", + ].join(EOL) + ); + + expect(res).toMatchObject({ + deps: [ + { + datasource: PuppetForgeDatasource.id, + depName: 'puppetlabs/stdlib', + packageName: 'puppetlabs/stdlib', + currentValue: '8.0.0', + }, + { + datasource: PuppetForgeDatasource.id, + depName: 'puppetlabs/apache', + packageName: 'puppetlabs/apache', + currentValue: '6.5.1', + }, + { + datasource: PuppetForgeDatasource.id, + depName: 'puppetlabs/puppetdb', + packageName: 'puppetlabs/puppetdb', + currentValue: '7.9.0', + }, + ], + }); + }); + + it('extracts multiple modules from Puppetfile with multiple forges/registries', () => { + const res = extractPackageFile( + Fixtures.get('Puppetfile.multiple_forges') + ); + + expect(res).toMatchObject({ + deps: [ + { + datasource: PuppetForgeDatasource.id, + depName: 'puppetlabs/stdlib', + packageName: 'puppetlabs/stdlib', + currentValue: '8.0.0', + registryUrls: ['https://forgeapi.puppetlabs.com'], + }, + { + datasource: PuppetForgeDatasource.id, + depName: 'puppetlabs/apache', + packageName: 'puppetlabs/apache', + currentValue: '6.5.1', + registryUrls: ['https://forgeapi.puppetlabs.com'], + }, + { + datasource: PuppetForgeDatasource.id, + depName: 'puppetlabs/puppetdb', + packageName: 'puppetlabs/puppetdb', + currentValue: '7.9.0', + registryUrls: ['https://forgeapi.puppetlabs.com'], + }, + { + datasource: PuppetForgeDatasource.id, + depName: 'mock/mockstdlib', + packageName: 'mock/mockstdlib', + currentValue: '10.0.0', + registryUrls: ['https://some-other-puppet-forge.com'], + }, + { + datasource: PuppetForgeDatasource.id, + depName: 'mock/mockapache', + packageName: 'mock/mockapache', + currentValue: '2.5.1', + registryUrls: ['https://some-other-puppet-forge.com'], + }, + { + datasource: PuppetForgeDatasource.id, + depName: 'mock/mockpuppetdb', + packageName: 'mock/mockpuppetdb', + currentValue: '1.9.0', + registryUrls: ['https://some-other-puppet-forge.com'], + }, + ], + }); + }); + + it('extracts multiple git tag modules from Puppetfile', () => { + const res = extractPackageFile(Fixtures.get('Puppetfile.github_tag')); + + expect(res).toMatchObject({ + deps: [ + { + datasource: GithubTagsDatasource.id, + depName: 'apache', + packageName: 'puppetlabs/puppetlabs-apache', + currentValue: '0.9.0', + sourceUrl: 'https://github.com/puppetlabs/puppetlabs-apache', + gitRef: true, + }, + { + datasource: GithubTagsDatasource.id, + depName: 'stdlib', + packageName: 'puppetlabs/puppetlabs-stdlib', + currentValue: '5.0.0', + sourceUrl: 'git@github.com:puppetlabs/puppetlabs-stdlib.git', + gitRef: true, + }, + ], + }); + }); + + it('Use GithubTagsDatasource only if host is exactly github.com', () => { + const res = extractPackageFile( + `mod 'apache', :git => 'https://github.com.example.com/puppetlabs/puppetlabs-apache', :tag => '0.9.0'` + ); + + expect(res).toEqual({ + deps: [ + { + datasource: GitTagsDatasource.id, + depName: 'apache', + packageName: + 'https://github.com.example.com/puppetlabs/puppetlabs-apache', + sourceUrl: + 'https://github.com.example.com/puppetlabs/puppetlabs-apache', + currentValue: '0.9.0', + gitRef: true, + }, + ], + }); + }); + + it('Github url without https is skipped', () => { + const res = extractPackageFile( + `mod 'apache', :git => 'http://github.com/puppetlabs/puppetlabs-apache', :tag => '0.9.0'` + ); + + expect(res).toMatchObject({ + deps: [ + { + depName: 'apache', + sourceUrl: 'http://github.com/puppetlabs/puppetlabs-apache', + skipReason: 'invalid-url', + }, + ], + }); + }); + + it('Git module without a tag should result in a skip reason', () => { + const res = extractPackageFile( + [ + "mod 'stdlib',", + " :git => 'git@github.com:puppetlabs/puppetlabs-stdlib.git',", + ].join(EOL) + ); + + expect(res).toEqual({ + deps: [ + { + depName: 'stdlib', + skipReason: 'invalid-version', + sourceUrl: 'git@github.com:puppetlabs/puppetlabs-stdlib.git', + }, + ], + }); + }); + + it('Skip reason should be overwritten by parser', () => { + const res = extractPackageFile( + [ + "mod 'stdlib', '0.1.0', 'i create a skip reason'", + " :git => 'git@github.com:puppetlabs/puppetlabs-stdlib.git',", + ].join(EOL) + ); + + expect(res).toMatchObject({ + deps: [ + { + depName: 'stdlib', + skipReason: 'invalid-config', + sourceUrl: 'git@github.com:puppetlabs/puppetlabs-stdlib.git', + }, + ], + }); + }); + + it('GitTagsDatasource', () => { + const res = extractPackageFile(Fixtures.get('Puppetfile.git_tag')); + + expect(res).toEqual({ + deps: [ + { + datasource: GitTagsDatasource.id, + depName: 'apache', + packageName: 'https://gitlab.com/example/project.git', + sourceUrl: 'https://gitlab.com/example/project.git', + gitRef: true, + currentValue: '0.9.0', + }, + { + datasource: GitTagsDatasource.id, + depName: 'stdlib', + packageName: 'git@gitlab.com:example/project_stdlib.git', + sourceUrl: 'git@gitlab.com:example/project_stdlib.git', + gitRef: true, + currentValue: '5.0.0', + }, + { + datasource: GitTagsDatasource.id, + depName: 'multiple_dirs_ssh', + packageName: 'git@gitlab.com:dir1/dir2/project.git', + sourceUrl: 'git@gitlab.com:dir1/dir2/project.git', + gitRef: true, + currentValue: '1.0.0', + }, + { + datasource: GitTagsDatasource.id, + depName: 'multiple_dirs_https', + packageName: 'https://gitlab.com/dir1/dir2/project.git', + sourceUrl: 'https://gitlab.com/dir1/dir2/project.git', + gitRef: true, + currentValue: '1.9.0', + }, + { + depName: 'invalid_url', + sourceUrl: 'hello world', + skipReason: 'invalid-url', + }, + ], + }); + }); + }); +}); diff --git a/lib/modules/manager/puppet/extract.ts b/lib/modules/manager/puppet/extract.ts new file mode 100644 index 0000000000000000000000000000000000000000..efa5ab610ad85e22cf05c6b9f1be8d49d3016708 --- /dev/null +++ b/lib/modules/manager/puppet/extract.ts @@ -0,0 +1,114 @@ +import { logger } from '../../../logger'; +import { parseUrl } from '../../../util/url'; +import { GitTagsDatasource } from '../../datasource/git-tags'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; +import { PuppetForgeDatasource } from '../../datasource/puppet-forge'; +import type { PackageDependency, PackageFile } from '../types'; +import { isGithubUrl, parseGitOwnerRepo } from './common'; +import { parsePuppetfile } from './puppetfile-parser'; +import type { PuppetfileModule } from './types'; + +function parseForgeDependency( + module: PuppetfileModule, + forgeUrl: string | null +): PackageDependency { + const dep: PackageDependency = { + depName: module.name, + datasource: PuppetForgeDatasource.id, + packageName: module.name, + currentValue: module.version, + }; + + if (forgeUrl) { + dep.registryUrls = [forgeUrl]; + } + + return dep; +} + +function parseGitDependency(module: PuppetfileModule): PackageDependency { + const moduleName = module.name; + + const git = module.tags?.get('git'); + const tag = module.tags?.get('tag'); + + if (!git || !tag) { + return { + depName: moduleName, + sourceUrl: git, + skipReason: 'invalid-version', + }; + } + + const parsedUrl = parseUrl(git); + const githubUrl = isGithubUrl(git, parsedUrl); + + if (githubUrl && parsedUrl && parsedUrl.protocol !== 'https:') { + logger.warn( + `Access to github is only allowed for https, your url was: ${git}` + ); + return { + depName: moduleName, + sourceUrl: git, + skipReason: 'invalid-url', + }; + } + const gitOwnerRepo = parseGitOwnerRepo(git, githubUrl); + + if (!gitOwnerRepo) { + // failed to parse git url + return { + depName: moduleName, + sourceUrl: git, + skipReason: 'invalid-url', + }; + } + + const packageDependency: PackageDependency = { + depName: moduleName, + packageName: git, + sourceUrl: git, + gitRef: true, + currentValue: tag, + datasource: GitTagsDatasource.id, + }; + + if (githubUrl) { + packageDependency.packageName = gitOwnerRepo; + packageDependency.datasource = GithubTagsDatasource.id; + } + + return packageDependency; +} + +function isGitModule(module: PuppetfileModule): boolean { + return module.tags?.has('git') ?? false; +} + +export function extractPackageFile(content: string): PackageFile | null { + logger.trace('puppet.extractPackageFile()'); + + const puppetFile = parsePuppetfile(content); + const deps: PackageDependency[] = []; + + for (const forgeUrl of puppetFile.getForges()) { + for (const module of puppetFile.getModulesOfForge(forgeUrl)) { + let packageDependency: PackageDependency; + + if (isGitModule(module)) { + packageDependency = parseGitDependency(module); + } else { + packageDependency = parseForgeDependency(module, forgeUrl); + } + + if (module.skipReason) { + // the PuppetfileModule skip reason is dominant over the packageDependency skip reason + packageDependency.skipReason = module.skipReason; + } + + deps.push(packageDependency); + } + } + + return deps.length ? { deps } : null; +} diff --git a/lib/modules/manager/puppet/index.ts b/lib/modules/manager/puppet/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3340bbcb2f65314ba1a08b8755579217e4c37f30 --- /dev/null +++ b/lib/modules/manager/puppet/index.ts @@ -0,0 +1,18 @@ +import { ProgrammingLanguage } from '../../../constants'; +import { GitTagsDatasource } from '../../datasource/git-tags'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; +import { PuppetForgeDatasource } from '../../datasource/puppet-forge'; + +export { extractPackageFile } from './extract'; + +export const language = ProgrammingLanguage.Ruby; + +export const defaultConfig = { + fileMatch: ['(^|\\/)Puppetfile$'], +}; + +export const supportedDatasources = [ + PuppetForgeDatasource.id, + GithubTagsDatasource.id, + GitTagsDatasource.id, +]; diff --git a/lib/modules/manager/puppet/puppetfile-parser.spec.ts b/lib/modules/manager/puppet/puppetfile-parser.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4cf0f0dc2b07a6874190aa2a453f9748bd1176b4 --- /dev/null +++ b/lib/modules/manager/puppet/puppetfile-parser.spec.ts @@ -0,0 +1,228 @@ +import { EOL } from 'os'; +import { Fixtures } from '../../../../test/fixtures'; +import { parsePuppetfile } from './puppetfile-parser'; + +const puppetLabsRegistryUrl = 'https://forgeapi.puppetlabs.com'; + +describe('modules/manager/puppet/puppetfile-parser', () => { + describe('parsePuppetfile()', () => { + it('Puppetfile_github_tag', () => { + const puppetfile = parsePuppetfile(Fixtures.get('Puppetfile.github_tag')); + const defaultRegistryModules = puppetfile.getModulesOfForge(undefined); + + expect(defaultRegistryModules).toEqual([ + { + name: 'apache', + tags: new Map([ + ['git', 'https://github.com/puppetlabs/puppetlabs-apache'], + ['tag', '0.9.0'], + ]), + }, + { + name: 'stdlib', + tags: new Map([ + ['git', 'git@github.com:puppetlabs/puppetlabs-stdlib.git'], + ['tag', '5.0.0'], + ]), + }, + ]); + }); + + it('Puppetfile_github_tag_single_line', () => { + const puppetfile = parsePuppetfile( + [ + "mod 'apache', :git => 'https://github.com/puppetlabs/puppetlabs-apache', :tag => '0.9.0'", + "mod 'stdlib', :git => 'git@github.com:puppetlabs/puppetlabs-stdlib.git', :tag => '5.0.0'", + ].join(EOL) + ); + const defaultRegistryModules = puppetfile.getModulesOfForge(undefined); + + expect(defaultRegistryModules).toEqual([ + { + name: 'apache', + tags: new Map([ + ['git', 'https://github.com/puppetlabs/puppetlabs-apache'], + ['tag', '0.9.0'], + ]), + }, + { + name: 'stdlib', + tags: new Map([ + ['git', 'git@github.com:puppetlabs/puppetlabs-stdlib.git'], + ['tag', '5.0.0'], + ]), + }, + ]); + }); + + it('Puppetfile with an invalid module creates PuppetfileModule with skipReason "invalid-config"', () => { + const puppetFileContent = `mod 'puppetlabs/stdlib', '8.0.0', 'i should trigger a skip reason'`; + const puppetfile = parsePuppetfile(puppetFileContent); + expect(puppetfile.getForges()).toHaveLength(1); + + const defaultRegistryModules = puppetfile.getModulesOfForge(undefined); + + expect(defaultRegistryModules).toEqual([ + { + name: 'puppetlabs/stdlib', + version: '8.0.0', + skipReason: 'invalid-config', + }, + ]); + }); + + it('get default forge with null or undefined returns the same', () => { + const puppetFileContent = `mod 'puppetlabs/stdlib', '8.0.0', 'i should trigger a skip reason'`; + const puppetfile = parsePuppetfile(puppetFileContent); + expect(puppetfile.getForges()).toHaveLength(1); + + const defaultRegistryModulesUndefined = + puppetfile.getModulesOfForge(undefined); + const defaultRegistryModulesNull = puppetfile.getModulesOfForge(null); + + expect(defaultRegistryModulesUndefined).toEqual( + defaultRegistryModulesNull + ); + }); + + it('Puppetfile_multiple_forges', () => { + const puppetfile = parsePuppetfile( + Fixtures.get('Puppetfile.multiple_forges') + ); + expect(puppetfile.getForges()).toHaveLength(2); + + const defaultRegistryModules = puppetfile.getModulesOfForge( + puppetLabsRegistryUrl + ); + + expect(defaultRegistryModules).toEqual([ + { + name: 'puppetlabs/stdlib', + version: '8.0.0', + }, + { + name: 'puppetlabs/apache', + version: '6.5.1', + }, + { + name: 'puppetlabs/puppetdb', + version: '7.9.0', + }, + ]); + + const someOtherPuppetForgeModules = puppetfile.getModulesOfForge( + 'https://some-other-puppet-forge.com' + ); + + expect(someOtherPuppetForgeModules).toEqual([ + { + name: 'mock/mockstdlib', + version: '10.0.0', + }, + { + name: 'mock/mockapache', + version: '2.5.1', + }, + { + name: 'mock/mockpuppetdb', + version: '1.9.0', + }, + ]); + }); + + it('Puppetfile_no_forge', () => { + const puppetfile = parsePuppetfile( + [ + "mod 'puppetlabs/stdlib', '8.0.0'", + "mod 'puppetlabs/apache', '6.5.1'", + "mod 'puppetlabs/puppetdb', '7.9.0'", + ].join(EOL) + ); + expect(puppetfile.getForges()).toHaveLength(1); + + const defaultRegistryModules = puppetfile.getModulesOfForge(undefined); + + expect(defaultRegistryModules).toEqual([ + { + name: 'puppetlabs/stdlib', + version: '8.0.0', + }, + { + name: 'puppetlabs/apache', + version: '6.5.1', + }, + { + name: 'puppetlabs/puppetdb', + version: '7.9.0', + }, + ]); + }); + + it('Puppetfile_single_forge', () => { + const puppetfile = parsePuppetfile( + [ + 'forge "https://forgeapi.puppetlabs.com"', + "mod 'puppetlabs/stdlib', '8.0.0'", + "mod 'puppetlabs/apache', '6.5.1'", + "mod 'puppetlabs/puppetdb', '7.9.0'", + ].join(EOL) + ); + expect(puppetfile.getForges()).toHaveLength(1); + + const defaultRegistryModules = puppetfile.getModulesOfForge( + puppetLabsRegistryUrl + ); + + expect(defaultRegistryModules).toEqual([ + { + name: 'puppetlabs/stdlib', + version: '8.0.0', + }, + { + name: 'puppetlabs/apache', + version: '6.5.1', + }, + { + name: 'puppetlabs/puppetdb', + version: '7.9.0', + }, + ]); + }); + + it('Puppetfile_with_comments', () => { + const puppetfile = parsePuppetfile( + Fixtures.get('Puppetfile.with_comments') + ); + expect(puppetfile.getForges()).toHaveLength(1); + + const defaultRegistryModules = puppetfile.getModulesOfForge(undefined); + + expect(defaultRegistryModules).toEqual([ + { + name: 'puppetlabs/stdlib', + version: '8.0.0', + }, + { + name: 'puppetlabs/apache', + version: '6.5.1', + }, + { + name: 'apache', + tags: new Map([ + ['git', 'https://github.com/puppetlabs/puppetlabs-apache'], + ]), + }, + { + name: 'stdlib', + tags: new Map([['tag', '5.0.0']]), + }, + { + name: 'stdlib2', + tags: new Map([ + ['git', 'git@github.com:puppetlabs/puppetlabs-stdlib2.git'], + ]), + }, + ]); + }); + }); +}); diff --git a/lib/modules/manager/puppet/puppetfile-parser.ts b/lib/modules/manager/puppet/puppetfile-parser.ts new file mode 100644 index 0000000000000000000000000000000000000000..a039d4049a67ae896fbb62b68936cb074dd4b4b2 --- /dev/null +++ b/lib/modules/manager/puppet/puppetfile-parser.ts @@ -0,0 +1,105 @@ +import { newlineRegex, regEx } from '../../../util/regex'; +import type { PuppetfileModule } from './types'; + +const forgeRegex = regEx(/^forge\s+['"]([^'"]+)['"]/); +const commentRegex = regEx(/#.*$/); + +/** + * For us a Puppetfile is build up of forges that have Modules. + * + * Modules are the updatable parts. + * + */ +export class Puppetfile { + private readonly forgeModules = new Map<string | null, PuppetfileModule[]>(); + + public add(currentForge: string | null, module: PuppetfileModule): void { + if (Object.keys(module).length === 0) { + return; + } + + if (!this.forgeModules.has(currentForge)) { + this.forgeModules.set(currentForge, []); + } + + this.forgeModules.get(currentForge)?.push(module); + } + + public getForges(): (string | null)[] { + return Array.from(this.forgeModules.keys()); + } + + public getModulesOfForge( + forgeUrl: string | null | undefined + ): PuppetfileModule[] { + const modules = this.forgeModules.get(forgeUrl ?? null); + + return modules ?? []; + } +} + +export function parsePuppetfile(content: string): Puppetfile { + const puppetfile: Puppetfile = new Puppetfile(); + + let currentForge: string | null = null; + let currentPuppetfileModule: PuppetfileModule = {}; + + for (const rawLine of content.split(newlineRegex)) { + // remove comments + const line = rawLine.replace(commentRegex, ''); + + const forgeResult = forgeRegex.exec(line); + if (forgeResult) { + puppetfile.add(currentForge, currentPuppetfileModule); + + currentPuppetfileModule = {}; + + currentForge = forgeResult[1]; + continue; + } + + const moduleStart = line.startsWith('mod'); + + if (moduleStart) { + puppetfile.add(currentForge, currentPuppetfileModule); + currentPuppetfileModule = {}; + } + + const moduleValueRegex = regEx(/(?:\s*:(\w+)\s+=>\s+)?['"]([^'"]+)['"]/g); + let moduleValue: RegExpExecArray | null; + + while ((moduleValue = moduleValueRegex.exec(line)) !== null) { + const key = moduleValue[1]; + const value = moduleValue[2]; + + if (key) { + currentPuppetfileModule.tags = + currentPuppetfileModule.tags ?? new Map(); + currentPuppetfileModule.tags.set(key, value); + } else { + fillPuppetfileModule(currentPuppetfileModule, value); + } + } + } + + puppetfile.add(currentForge, currentPuppetfileModule); + + return puppetfile; +} + +function fillPuppetfileModule( + currentPuppetfileModule: PuppetfileModule, + value: string +): void { + // "positional" module values + if (currentPuppetfileModule.name === undefined) { + // moduleName + currentPuppetfileModule.name = value; + } else if (currentPuppetfileModule.version === undefined) { + // second value without a key is the version + currentPuppetfileModule.version = value; + } else { + // 3+ value without a key is not supported + currentPuppetfileModule.skipReason = 'invalid-config'; + } +} diff --git a/lib/modules/manager/puppet/readme.md b/lib/modules/manager/puppet/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..7656583efeda8ccff06d8b2a310f47c27f97de0b --- /dev/null +++ b/lib/modules/manager/puppet/readme.md @@ -0,0 +1,86 @@ +simply keeps Puppetfiles updated + +### How It Works + +1. Renovate searches in each repository for any `Puppetfile` files +1. Existing dependencies are extracted from the relevant sections of the file +1. Renovate resolves the dependency on the provided forges (or uses `https://forgeapi.puppetlabs.com` as default) +1. A PR is created with `Puppetfile` updated in the same commit +1. If the source repository has either a "changelog" file or uses GitHub releases, then Release Notes for each version will be embedded in the generated PR + +### supported Puppetfile formats + +the manager extracts the deps from one Puppetfile + +the Puppetfile supports at the moment different ways to configure forges + +1. no forge defined + + ```ruby + mod 'puppetlabs/apt', '8.3.0' + mod 'puppetlabs/apache', '7.0.0' + ``` + +2. one forge defined: `forge "https://forgeapi.puppetlabs.com"` + + ```ruby + forge "https://forgeapi.puppetlabs.com" + + mod 'puppetlabs/apt', '8.3.0' + mod 'puppetlabs/apache', '7.0.0' + mod 'puppetlabs/concat', '7.1.1' + ``` + +3. multiple forges defined + + ```ruby + forge "https://forgeapi.puppetlabs.com" + + mod 'puppetlabs/apt', '8.3.0' + mod 'puppetlabs/apache', '7.0.0' + mod 'puppetlabs/concat', '7.1.1' + + # private forge + forge "https://forgeapi.example.com" + + mod 'example/infra', '3.3.0' + ``` + +4. github based version + + ```ruby + # tag based + mod 'example/standalone_jar', + :git => 'git@gitlab.example.de:puppet/example-standalone_jar', + :tag => '0.9.0' + ``` + +5. git based version + + ```ruby + # tag based + mod 'stdlib', + :git => 'git@gitlab.com:example/project_stdlib.git', + :tag => '5.0.0' + ``` + +### possible improvements + +#### further git-support + +usually you can add the versions to a forge and use the already provided +way of updating + +```ruby +# branch based +mod 'example/samba', + :git => 'https://github.com/example/puppet-samba', + :branch => 'stable_version' +``` + +```ruby +# ref based +mod 'example/samba', + :git => 'https://github.com/example/puppet-samba', + :ref => 'stable_version' +``` diff --git a/lib/modules/manager/puppet/types.ts b/lib/modules/manager/puppet/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..34cf58203bbf1d915f0845d1ec3090e3fbe201d4 --- /dev/null +++ b/lib/modules/manager/puppet/types.ts @@ -0,0 +1,8 @@ +import type { SkipReason } from '../../../types'; + +export interface PuppetfileModule { + name?: string; + version?: string; + tags?: Map<string, string>; + skipReason?: SkipReason; +}