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;
+}