Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(datasources): add bitrise datasource #30138

Merged
merged 12 commits into from
Jul 25, 2024
2 changes: 2 additions & 0 deletions lib/modules/datasource/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AzureBicepResourceDatasource } from './azure-bicep-resource';
import { AzurePipelinesTasksDatasource } from './azure-pipelines-tasks';
import { BazelDatasource } from './bazel';
import { BitbucketTagsDatasource } from './bitbucket-tags';
import { BitriseDatasource } from './bitrise';
import { CdnJsDatasource } from './cdnjs';
import { ClojureDatasource } from './clojure';
import { ConanDatasource } from './conan';
Expand Down Expand Up @@ -73,6 +74,7 @@ api.set(AzureBicepResourceDatasource.id, new AzureBicepResourceDatasource());
api.set(AzurePipelinesTasksDatasource.id, new AzurePipelinesTasksDatasource());
api.set(BazelDatasource.id, new BazelDatasource());
api.set(BitbucketTagsDatasource.id, new BitbucketTagsDatasource());
api.set(BitriseDatasource.id, new BitriseDatasource());
api.set(CdnJsDatasource.id, new CdnJsDatasource());
api.set(ClojureDatasource.id, new ClojureDatasource());
api.set(ConanDatasource.id, new ConanDatasource());
Expand Down
235 changes: 235 additions & 0 deletions lib/modules/datasource/bitrise/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { codeBlock } from 'common-tags';
import * as httpMock from '../../../../test/http-mock';
import { toBase64 } from '../../../util/string';
import { getPkgReleases } from '../index';
import { BitriseDatasource } from './index';

describe('modules/datasource/bitrise/index', () => {
describe('getReleases()', () => {
it('returns null for unsupported registryUrl', async () => {
await expect(
getPkgReleases({
datasource: BitriseDatasource.id,
packageName: 'script',
registryUrls: ['https://gitlab.com/bitrise-io/bitrise-steplib'],
}),
).resolves.toBeNull();
});

it('support GitHub Enterprise API URL', async () => {
httpMock
.scope(
'https://github.mycompany.com/api/v3/repos/foo/bar/contents/steps',
)
.get('/script')
.reply(200, [
{
type: 'dir',
name: '1.0.0',
path: 'steps/script/1.0.0',
},
])
.get('/script/1.0.0/step.yml')
.reply(200, {
type: 'file',
name: 'step.yml',
path: 'steps/script/1.0.0/step.yml',
encoding: 'base64',
content: toBase64(codeBlock`
published_at: 2024-03-19T13:54:48.081077+01:00
source_code_url: https://github.com/bitrise-steplib/bitrise-step-script
website: https://github.com/bitrise-steplib/bitrise-step-script
`),
});
await expect(
getPkgReleases({
datasource: BitriseDatasource.id,
packageName: 'script',
registryUrls: ['https://github.mycompany.com/foo/bar'],
}),
).resolves.toEqual({
homepage: 'https://bitrise.io/integrations/steps/script',
registryUrl: 'https://github.mycompany.com/foo/bar',
releases: [
{
releaseTimestamp: '2024-03-19T12:54:48.081Z',
sourceUrl: 'https://github.com/bitrise-steplib/bitrise-step-script',
version: '1.0.0',
},
],
});
});

it('returns version and filters out the asset folder', async () => {
httpMock
.scope(
'https://api.github.com/repos/bitrise-io/bitrise-steplib/contents/steps',
)
.get('/activate-build-cache-for-bazel')
.reply(200, [
{
type: 'dir',
name: '1.0.0',
path: 'steps/activate-build-cache-for-bazel/1.0.0',
},
{
type: 'dir',
name: '1.0.1',
path: 'steps/activate-build-cache-for-bazel/1.0.1',
},
{
type: 'dir',
name: 'assets',
path: 'steps/activate-build-cache-for-bazel/assets',
},
])
.get('/activate-build-cache-for-bazel/1.0.0/step.yml')
.reply(200, {
type: 'file',
name: 'step.yml',
path: 'steps/activate-build-cache-for-bazel/1.0.0/step.yml',
encoding: 'base64',
content: toBase64(codeBlock`
published_at: 2024-03-19T13:54:48.081077+01:00
source_code_url: https://github.com/bitrise-steplib/bitrise-step-activate-build-cache-for-bazel
website: https://github.com/bitrise-steplib/bitrise-step-activate-build-cache-for-bazel
`),
})
.get('/activate-build-cache-for-bazel/1.0.1/step.yml')
.reply(200, {
type: 'file',
name: 'step.yml',
path: 'steps/activate-build-cache-for-bazel/1.0.1/step.yml',
encoding: 'base64',
content: toBase64(codeBlock`
published_at: "2024-07-03T08:53:25.668504731Z"
source_code_url: https://github.com/bitrise-steplib/bitrise-step-activate-build-cache-for-bazel
website: https://github.com/bitrise-steplib/bitrise-step-activate-build-cache-for-bazel
`),
});

await expect(
getPkgReleases({
datasource: BitriseDatasource.id,
packageName: 'activate-build-cache-for-bazel',
}),
).resolves.toEqual({
homepage:
'https://bitrise.io/integrations/steps/activate-build-cache-for-bazel',
registryUrl: 'https://github.com/bitrise-io/bitrise-steplib.git',
releases: [
{
releaseTimestamp: '2024-03-19T12:54:48.081Z',
sourceUrl:
'https://github.com/bitrise-steplib/bitrise-step-activate-build-cache-for-bazel',
version: '1.0.0',
},
{
releaseTimestamp: '2024-07-03T08:53:25.668Z',
sourceUrl:
'https://github.com/bitrise-steplib/bitrise-step-activate-build-cache-for-bazel',
version: '1.0.1',
},
],
});
});

it('returns null if there are no releases', async () => {
httpMock
.scope(
'https://api.github.com/repos/bitrise-io/bitrise-steplib/contents/steps',
)
.get('/activate-build-cache-for-bazel')
.reply(200, [
{
type: 'dir',
name: 'assets',
path: 'steps/activate-build-cache-for-bazel/assets',
},
]);

await expect(
getPkgReleases({
datasource: BitriseDatasource.id,
packageName: 'activate-build-cache-for-bazel',
}),
).resolves.toBeNull();
});

it('returns null if the package has an unexpected format', async () => {
httpMock
.scope(
'https://api.github.com/repos/bitrise-io/bitrise-steplib/contents/steps',
)
.get('/activate-build-cache-for-bazel')
.reply(200, {
type: 'file',
name: 'assets',
path: 'steps/activate-build-cache-for-bazel/assets',
});

await expect(
getPkgReleases({
datasource: BitriseDatasource.id,
packageName: 'activate-build-cache-for-bazel',
}),
).resolves.toBeNull();
});

it('returns null if the file object has no content', async () => {
httpMock
.scope(
'https://api.github.com/repos/bitrise-io/bitrise-steplib/contents/steps',
)
.get('/script')
.reply(200, [
{
type: 'dir',
name: '1.0.0',
path: 'steps/script/1.0.0',
},
])
.get('/script/1.0.0/step.yml')
.reply(200, {
type: 'file',
name: 'step.yml',
path: 'steps/script/1.0.0/step.yml',
});
await expect(
getPkgReleases({
datasource: BitriseDatasource.id,
packageName: 'script',
}),
).resolves.toBeNull();
});

it('returns null if the file object has an unexpected encoding', async () => {
httpMock
.scope(
'https://api.github.com/repos/bitrise-io/bitrise-steplib/contents/steps',
)
.get('/script')
.reply(200, [
{
type: 'dir',
name: '1.0.0',
path: 'steps/script/1.0.0',
},
])
.get('/script/1.0.0/step.yml')
.reply(200, {
type: 'file',
name: 'step.yml',
path: 'steps/script/1.0.0/step.yml',
encoding: 'none',
content: '',
});
await expect(
getPkgReleases({
datasource: BitriseDatasource.id,
packageName: 'script',
}),
).resolves.toBeNull();
});
});
});
138 changes: 138 additions & 0 deletions lib/modules/datasource/bitrise/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import is from '@sindresorhus/is';
import { logger } from '../../../logger';
import { cache } from '../../../util/cache/package/decorator';
import { detectPlatform } from '../../../util/common';
import { parseGitUrl } from '../../../util/git/url';
import { GithubHttp } from '../../../util/http/github';
import { fromBase64 } from '../../../util/string';
import { joinUrlParts } from '../../../util/url';
import { parseSingleYaml } from '../../../util/yaml';
import { GithubContentResponse } from '../../platform/github/schema';
import semver from '../../versioning/semver';
import { Datasource } from '../datasource';
import type { GetReleasesConfig, ReleaseResult } from '../types';
import { BitriseStepFile } from './schema';

export class BitriseDatasource extends Datasource {
static readonly id = 'bitrise';

override readonly http: GithubHttp;

constructor() {
super(BitriseDatasource.id);

this.http = new GithubHttp(this.id);
}

override readonly customRegistrySupport = true;

override readonly defaultRegistryUrls = [
'https://github.com/bitrise-io/bitrise-steplib.git',
];

override readonly releaseTimestampSupport = true;
override readonly releaseTimestampNote =
'The release timestamp is determined from the `published_at` field in the results.';
override readonly sourceUrlSupport = 'release';
override readonly sourceUrlNote =
'The source URL is determined from the `source_code_url` field of the release object in the results.';

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

const parsedUrl = parseGitUrl(registryUrl);
if (detectPlatform(registryUrl) !== 'github') {
logger.once.warn(
`${parsedUrl.source} is not a supported Git hoster for this datasource`,
secustor marked this conversation as resolved.
Show resolved Hide resolved
);
return null;
}

const result: ReleaseResult = {
releases: [],
};

const massagedPackageName = encodeURIComponent(packageName);
const baseApiURL =
parsedUrl.resource === 'github.com'
? 'https://api.github.com'
: `https://${parsedUrl.resource}/api/v3`;
const packageUrl = joinUrlParts(
baseApiURL,
'repos',
parsedUrl.full_name,
'contents/steps',
massagedPackageName,
);

const { body: packageRaw } = await this.http.getJson(
packageUrl,
GithubContentResponse,
);

if (!is.array(packageRaw)) {
logger.warn(
{ data: packageRaw, url: packageUrl },
'Got unexpected response for Bitrise package location',
);
return null;
}

for (const versionDir of packageRaw.filter((element) =>
semver.isValid(element.name),
)) {
const stepUrl = joinUrlParts(packageUrl, versionDir.name, 'step.yml');
// TODO use getRawFile when ready #30155
const { body } = await this.http.getJson(stepUrl, GithubContentResponse);
if (!('content' in body)) {
logger.warn(
{ data: body, url: stepUrl },
'Got unexpected response for Bitrise step location',
);
return null;
}
if (body.encoding !== 'base64') {
logger.warn(
{ data: body, url: stepUrl },
`Got unexpected encoding for Bitrise step location '${body.encoding}'`,
);
return null;
}

const content = fromBase64(body.content);
const { published_at, source_code_url } = parseSingleYaml(content, {
customSchema: BitriseStepFile,
});

const releaseTimestamp = is.string(published_at)
? published_at
: published_at.toISOString();
result.releases.push({
version: versionDir.name,
releaseTimestamp,
sourceUrl: source_code_url,
});
}

// if we have no releases return null
if (!result.releases.length) {
return null;
}

return {
...result,
homepage: `https://bitrise.io/integrations/steps/${packageName}`,
};
secustor marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading