Skip to content
1 change: 1 addition & 0 deletions docs/usage/self-hosted-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ Other valid cache namespaces are as follows:
- `datasource-jenkins-plugins`
- `datasource-maven:cache-provider`
- `datasource-maven:postprocess-reject`
- `datasource-nextcloud`
- `datasource-node-version`
- `datasource-npm:cache-provider`
- `datasource-nuget-v3`
Expand Down
2 changes: 2 additions & 0 deletions lib/modules/datasource/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { JavaVersionDatasource } from './java-version';
import { JenkinsPluginsDatasource } from './jenkins-plugins';
import { KubernetesApiDatasource } from './kubernetes-api';
import { MavenDatasource } from './maven';
import { NextcloudDatasource } from './nextcloud';
import { NodeVersionDatasource } from './node-version';
import { NpmDatasource } from './npm';
import { NugetDatasource } from './nuget';
Expand Down Expand Up @@ -135,6 +136,7 @@ api.set(JavaVersionDatasource.id, new JavaVersionDatasource());
api.set(JenkinsPluginsDatasource.id, new JenkinsPluginsDatasource());
api.set(KubernetesApiDatasource.id, new KubernetesApiDatasource());
api.set(MavenDatasource.id, new MavenDatasource());
api.set(NextcloudDatasource.id, new NextcloudDatasource());
api.set(NodeVersionDatasource.id, new NodeVersionDatasource());
api.set(NpmDatasource.id, new NpmDatasource());
api.set(NugetDatasource.id, new NugetDatasource());
Expand Down
166 changes: 166 additions & 0 deletions lib/modules/datasource/nextcloud/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { getPkgReleases } from '..';
import { NextcloudDatasource } from '.';
import * as httpMock from '~test/http-mock';

describe('modules/datasource/nextcloud/index', () => {
it(`no registryUrl`, async () => {
const res = await getPkgReleases({
datasource: NextcloudDatasource.id,
packageName: 'user_oidc',
registryUrls: [],
});

expect(res).toBeNull();
});

it(`no package`, async () => {
const data = '[]';

httpMock.scope('https://custom.registry.com').get('/').reply(200, data);

const res = await getPkgReleases({
datasource: NextcloudDatasource.id,
packageName: 'user_oidc',
registryUrls: ['https://custom.registry.com'],
});

expect(res).toBeNull();
});

it(`package with no versions`, async () => {
const data = `
[
{
"id": "user_oidc",
"website": "https://github.com/nextcloud/user_oidc",
"created": "2020-05-25T10:51:12.430005Z",
"releases": []
}
]
`;

httpMock.scope('https://custom.registry.com').get('/').reply(200, data);

const res = await getPkgReleases({
datasource: NextcloudDatasource.id,
packageName: 'user_oidc',
registryUrls: ['https://custom.registry.com'],
});

expect(res?.homepage).toBeUndefined();
expect(res?.registryUrl).toBe('https://custom.registry.com');

expect(res?.releases).toBeEmpty();
});

it.each([
{
website: 'https://github.com/nextcloud/user_oidc',
changelogUrl: 'https://github.com/nextcloud-releases/user_oidc',
},
{
website: 'https://custom.app',
changelogUrl: 'https://custom.app',
},
])(
'package with website %s returns %s',
async ({ website, changelogUrl }) => {
const data = `
[
{
"id": "user_oidc",
"website": "${website}",
"created": "2020-05-25T10:51:12.430005Z",
"releases": [
{
"version": "7.3.0",
"created": "2025-07-25T09:41:26.318411Z",
"isNightly": false,
"translations": {
"en": {
"changelog": "testChangelog"
}
}
}
]
}
]
`;

httpMock.scope('https://custom.registry.com').get('/').reply(200, data);

const res = await getPkgReleases({
datasource: NextcloudDatasource.id,
packageName: 'user_oidc',
registryUrls: ['https://custom.registry.com'],
});

expect(res?.releases[0].changelogUrl).toBe(changelogUrl);
},
);

it(`package with changelog content and url`, async () => {
const data = `
[
{
"id": "user_oidc",
"website": "https://github.com/nextcloud/user_oidc",
"created": "2020-05-25T10:51:12.430005Z",
"releases": [
{
"version": "7.3.0",
"created": "2025-07-25T09:41:26.318411Z",
"isNightly": false,
"translations": {
"en": {
"changelog": "testChangelog"
}
}
},
{
"version": "7.2.0",
"created": "2025-04-24T09:24:43.232337Z",
"isNightly": true,
"translations": {
"en": {
"changelog": ""
}
}
}
]
}
]
`;

httpMock.scope('https://custom.registry.com').get('/').reply(200, data);

const res = await getPkgReleases({
datasource: NextcloudDatasource.id,
packageName: 'user_oidc',
registryUrls: ['https://custom.registry.com'],
});

expect(res).toStrictEqual({
registryUrl: 'https://custom.registry.com',
releases: [
{
changelogContent: undefined,
changelogUrl: 'https://github.com/nextcloud-releases/user_oidc',
isStable: false,
registryUrl: 'https://custom.registry.com',
releaseTimestamp: '2025-04-24T09:24:43.232Z',
version: '7.2.0',
},
{
changelogContent: 'testChangelog',
changelogUrl: 'https://github.com/nextcloud-releases/user_oidc',
isStable: true,
registryUrl: 'https://custom.registry.com',
releaseTimestamp: '2025-07-25T09:41:26.318Z',
version: '7.3.0',
},
],
sourceUrl: 'https://github.com/nextcloud/user_oidc',
});
});
});
74 changes: 74 additions & 0 deletions lib/modules/datasource/nextcloud/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { cache } from '../../../util/cache/package/decorator';
import { regEx } from '../../../util/regex';
import { asTimestamp } from '../../../util/timestamp';
import * as semanticVersioning from '../../versioning/semver';
import { Datasource } from '../datasource';
import type { GetReleasesConfig, ReleaseResult } from '../types';
import { Applications } from './schema';

export class NextcloudDatasource extends Datasource {
static readonly id = 'nextcloud';

static readonly defaultTranslationLanguage = 'en';

override readonly defaultVersioning = semanticVersioning.id;

private static readonly sourceUrlRegex = regEx(
/(?<prefix>.*github.com\/nextcloud)(?<suffix>\/.*)/,
);

constructor() {
super(NextcloudDatasource.id);
}

@cache({
namespace: `datasource-${NextcloudDatasource.id}`,
key: ({ registryUrl, packageName }: GetReleasesConfig) =>
`${registryUrl}:${packageName}`,
})
async getReleases({
packageName,
registryUrl,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
if (!registryUrl) {
return null;
}

const response = await this.http.getJson(registryUrl, Applications);

const application = response.body.find((a) => a.id === packageName);

if (!application) {
return null;
}

const result: ReleaseResult = {
releases: [],
homepage: application.website,
registryUrl,
};

for (const release of application.releases) {
const changelogContent =
release.translations[NextcloudDatasource.defaultTranslationLanguage]
.changelog;
const sourceUrlMatches = NextcloudDatasource.sourceUrlRegex.exec(
application.website,
);

result.releases.push({
version: release.version,
releaseTimestamp: asTimestamp(release.created),
changelogContent:
changelogContent.length > 0 ? changelogContent : undefined,
changelogUrl: sourceUrlMatches?.groups
? `${sourceUrlMatches.groups.prefix}-releases${sourceUrlMatches.groups.suffix}`
: application.website,
isStable: !release.isNightly,
registryUrl,
});
}

return result;
}
}
28 changes: 28 additions & 0 deletions lib/modules/datasource/nextcloud/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
This datasource finds Nextcloud application updates from Nextcloud feeds.

By default, Renovate has no default registry url for this datasource. You need to override the default behavior with the `registryUrls` config option.
For example:

```json
{
"matchDatasources": ["nextcloud"],
"registryUrls": [
"https://apps.nextcloud.com/api/v1/platform/30.0.0/apps.json"
]
}
```

Additionally, if you want Renovate to automatically update the platform version, you can create a custom manager.
For example:

```json
{
"customType": "regex",
"managerFilePatterns": ["/(^|/)renovate.json$/"],
"matchStrings": [
"https://apps.nextcloud.com/api/v1/platform/(?<currentValue>.*)/apps.json"
],
"depNameTemplate": "nextcloud/server",
"datasourceTemplate": "github-releases"
}
```
20 changes: 20 additions & 0 deletions lib/modules/datasource/nextcloud/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { z } from 'zod';

const Translation = z.object({
changelog: z.string(),
});

export const ApplicationRelease = z.object({
created: z.string(),
isNightly: z.boolean(),
translations: z.record(z.string(), Translation),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably use LooseRecord, so it filters Translation with missing changelog and doesn't discard all

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? I'm not an expert in typescript or zod, can you give me an example? Thank you.

version: z.string(),
});

export const Application = z.object({
id: z.string(),
releases: z.array(ApplicationRelease),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LooseArray ?

website: z.string(),
});

export const Applications = z.array(Application);
1 change: 1 addition & 0 deletions lib/util/cache/package/namespaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const packageCacheNamespaces = [
'datasource-jenkins-plugins',
'datasource-maven:cache-provider',
'datasource-maven:postprocess-reject',
'datasource-nextcloud',
'datasource-node-version',
'datasource-npm:cache-provider',
'datasource-nuget-v3',
Expand Down