mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-11 14:36:25 +00:00
feat(maven): S3 Support (#14938)
Co-authored-by: Michael Kriese <michael.kriese@visualon.de> Co-authored-by: Sergei Zharinov <zharinov@users.noreply.github.com> Co-authored-by: Rhys Arkins <rhys@arkins.net>
This commit is contained in:
parent
5ef87c7698
commit
6ea0d5d6fb
14 changed files with 462 additions and 26 deletions
17
lib/modules/datasource/maven/__fixtures__/metadata-s3.xml
Normal file
17
lib/modules/datasource/maven/__fixtures__/metadata-s3.xml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<metadata>
|
||||||
|
<groupId>org.example</groupId>
|
||||||
|
<artifactId>package</artifactId>
|
||||||
|
<versioning>
|
||||||
|
<latest>1.0.2</latest>
|
||||||
|
<release>1.0.2</release>
|
||||||
|
<versions>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<version>1.0.1</version>
|
||||||
|
<version>1.0.2</version>
|
||||||
|
<version>1.0.3</version>
|
||||||
|
</versions>
|
||||||
|
<lastUpdated>20210101000000</lastUpdated>
|
||||||
|
</versioning>
|
||||||
|
</metadata>
|
|
@ -14,7 +14,7 @@ import type { GetReleasesConfig, Release, ReleaseResult } from '../types';
|
||||||
import { MAVEN_REPO } from './common';
|
import { MAVEN_REPO } from './common';
|
||||||
import type { MavenDependency, ReleaseMap } from './types';
|
import type { MavenDependency, ReleaseMap } from './types';
|
||||||
import {
|
import {
|
||||||
checkHttpResource,
|
checkResource,
|
||||||
createUrlForDependencyPom,
|
createUrlForDependencyPom,
|
||||||
downloadHttpProtocol,
|
downloadHttpProtocol,
|
||||||
downloadMavenXml,
|
downloadMavenXml,
|
||||||
|
@ -249,7 +249,7 @@ export class MavenDatasource extends Datasource {
|
||||||
const artifactUrl = getMavenUrl(dependency, repoUrl, pomUrl);
|
const artifactUrl = getMavenUrl(dependency, repoUrl, pomUrl);
|
||||||
const release: Release = { version };
|
const release: Release = { version };
|
||||||
|
|
||||||
const res = await checkHttpResource(this.http, artifactUrl);
|
const res = await checkResource(this.http, artifactUrl);
|
||||||
|
|
||||||
if (is.date(res)) {
|
if (is.date(res)) {
|
||||||
release.releaseTimestamp = res.toISOString();
|
release.releaseTimestamp = res.toISOString();
|
||||||
|
|
204
lib/modules/datasource/maven/s3.spec.ts
Normal file
204
lib/modules/datasource/maven/s3.spec.ts
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
import {
|
||||||
|
GetObjectCommand,
|
||||||
|
HeadObjectCommand,
|
||||||
|
S3Client,
|
||||||
|
} from '@aws-sdk/client-s3';
|
||||||
|
import { mockClient } from 'aws-sdk-client-mock';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { ReleaseResult, getPkgReleases } from '..';
|
||||||
|
import { Fixtures } from '../../../../test/fixtures';
|
||||||
|
import { logger } from '../../../../test/util';
|
||||||
|
import * as hostRules from '../../../util/host-rules';
|
||||||
|
import { id as versioning } from '../../versioning/maven';
|
||||||
|
import { MavenDatasource } from '.';
|
||||||
|
|
||||||
|
const datasource = MavenDatasource.id;
|
||||||
|
|
||||||
|
const baseUrlS3 = 's3://repobucket';
|
||||||
|
|
||||||
|
function get(
|
||||||
|
depName = 'org.example:package',
|
||||||
|
...registryUrls: string[]
|
||||||
|
): Promise<ReleaseResult | null> {
|
||||||
|
const conf = { versioning, datasource, depName };
|
||||||
|
return getPkgReleases(registryUrls ? { ...conf, registryUrls } : conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = Readable.from(
|
||||||
|
Buffer.from(Fixtures.get('metadata-s3.xml'), 'utf-8')
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('modules/datasource/maven/s3', () => {
|
||||||
|
const s3mock = mockClient(S3Client);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
hostRules.add({
|
||||||
|
hostType: datasource,
|
||||||
|
matchHost: 'custom.registry.renovatebot.com',
|
||||||
|
token: '123test',
|
||||||
|
});
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
s3mock.reset();
|
||||||
|
hostRules.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('S3', () => {
|
||||||
|
it('returns releases', async () => {
|
||||||
|
s3mock
|
||||||
|
.on(GetObjectCommand, {
|
||||||
|
Bucket: 'repobucket',
|
||||||
|
Key: 'org/example/package/maven-metadata.xml',
|
||||||
|
})
|
||||||
|
.resolvesOnce({ Body: meta })
|
||||||
|
.on(HeadObjectCommand, {
|
||||||
|
Bucket: 'repobucket',
|
||||||
|
Key: 'org/example/package/0.0.1/package-0.0.1.pom',
|
||||||
|
})
|
||||||
|
.resolvesOnce({ DeleteMarker: true })
|
||||||
|
.on(HeadObjectCommand, {
|
||||||
|
Bucket: 'repobucket',
|
||||||
|
Key: 'org/example/package/1.0.0/package-1.0.0.pom',
|
||||||
|
})
|
||||||
|
.rejectsOnce('NoSuchKey')
|
||||||
|
.on(HeadObjectCommand, {
|
||||||
|
Bucket: 'repobucket',
|
||||||
|
Key: 'org/example/package/1.0.1/package-1.0.1.pom',
|
||||||
|
})
|
||||||
|
.rejectsOnce('Unknown')
|
||||||
|
.on(HeadObjectCommand, {
|
||||||
|
Bucket: 'repobucket',
|
||||||
|
Key: 'org/example/package/1.0.2/package-1.0.2.pom',
|
||||||
|
})
|
||||||
|
.resolvesOnce({})
|
||||||
|
.on(HeadObjectCommand, {
|
||||||
|
Bucket: 'repobucket',
|
||||||
|
Key: 'org/example/package/1.0.3/package-1.0.3.pom',
|
||||||
|
})
|
||||||
|
.resolvesOnce({
|
||||||
|
LastModified: DateTime.fromISO(`2020-01-01T00:00:00.000Z`).toJSDate(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await get('org.example:package', baseUrlS3);
|
||||||
|
|
||||||
|
expect(res).toEqual({
|
||||||
|
display: 'org.example:package',
|
||||||
|
group: 'org.example',
|
||||||
|
name: 'package',
|
||||||
|
registryUrl: 's3://repobucket',
|
||||||
|
releases: [
|
||||||
|
{ version: '1.0.2' },
|
||||||
|
{ version: '1.0.3', releaseTimestamp: '2020-01-01T00:00:00.000Z' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('errors', () => {
|
||||||
|
it('returns null on auth error', async () => {
|
||||||
|
class CredentialsProviderError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.name = 'CredentialsProviderError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s3mock
|
||||||
|
.on(GetObjectCommand, {
|
||||||
|
Bucket: 'repobucket',
|
||||||
|
Key: 'org/example/package/maven-metadata.xml',
|
||||||
|
})
|
||||||
|
.rejectsOnce(new CredentialsProviderError());
|
||||||
|
|
||||||
|
const res = await get('org.example:package', baseUrlS3);
|
||||||
|
|
||||||
|
expect(res).toBeNull();
|
||||||
|
expect(logger.logger.debug).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
failedUrl: 's3://repobucket/org/example/package/maven-metadata.xml',
|
||||||
|
},
|
||||||
|
'Dependency lookup authorization failed. Please correct AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY env vars'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for incorrect region', async () => {
|
||||||
|
s3mock
|
||||||
|
.on(GetObjectCommand, {
|
||||||
|
Bucket: 'repobucket',
|
||||||
|
Key: 'org/example/package/maven-metadata.xml',
|
||||||
|
})
|
||||||
|
.rejectsOnce('Region is missing');
|
||||||
|
|
||||||
|
const res = await get('org.example:package', baseUrlS3);
|
||||||
|
|
||||||
|
expect(res).toBeNull();
|
||||||
|
expect(logger.logger.debug).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
failedUrl: 's3://repobucket/org/example/package/maven-metadata.xml',
|
||||||
|
},
|
||||||
|
'Dependency lookup failed. Please a correct AWS_REGION env var'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for NoSuchKey error', async () => {
|
||||||
|
s3mock
|
||||||
|
.on(GetObjectCommand, {
|
||||||
|
Bucket: 'repobucket',
|
||||||
|
Key: 'org/example/package/maven-metadata.xml',
|
||||||
|
})
|
||||||
|
.rejectsOnce('NoSuchKey');
|
||||||
|
|
||||||
|
const res = await get('org.example:package', baseUrlS3);
|
||||||
|
|
||||||
|
expect(res).toBeNull();
|
||||||
|
expect(logger.logger.trace).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
failedUrl: 's3://repobucket/org/example/package/maven-metadata.xml',
|
||||||
|
},
|
||||||
|
'S3 url not found'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for NotFound error', async () => {
|
||||||
|
s3mock
|
||||||
|
.on(GetObjectCommand, {
|
||||||
|
Bucket: 'repobucket',
|
||||||
|
Key: 'org/example/package/maven-metadata.xml',
|
||||||
|
})
|
||||||
|
.rejectsOnce('NotFound');
|
||||||
|
|
||||||
|
const res = await get('org.example:package', baseUrlS3);
|
||||||
|
|
||||||
|
expect(res).toBeNull();
|
||||||
|
expect(logger.logger.trace).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
failedUrl: 's3://repobucket/org/example/package/maven-metadata.xml',
|
||||||
|
},
|
||||||
|
'S3 url not found'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for unknown error', async () => {
|
||||||
|
s3mock
|
||||||
|
.on(GetObjectCommand, {
|
||||||
|
Bucket: 'repobucket',
|
||||||
|
Key: 'org/example/package/maven-metadata.xml',
|
||||||
|
})
|
||||||
|
.rejectsOnce('Unknown error');
|
||||||
|
|
||||||
|
const res = await get('org.example:package', baseUrlS3);
|
||||||
|
|
||||||
|
expect(res).toBeNull();
|
||||||
|
expect(logger.logger.debug).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
failedUrl: 's3://repobucket/org/example/package/maven-metadata.xml',
|
||||||
|
message: 'Unknown error',
|
||||||
|
},
|
||||||
|
'Unknown S3 download error'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
50
lib/modules/datasource/maven/util.spec.ts
Normal file
50
lib/modules/datasource/maven/util.spec.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { parseUrl } from '../../../util/url';
|
||||||
|
import {
|
||||||
|
checkResource,
|
||||||
|
checkS3Resource,
|
||||||
|
downloadMavenXml,
|
||||||
|
downloadS3Protocol,
|
||||||
|
} from './util';
|
||||||
|
|
||||||
|
describe('modules/datasource/maven/util', () => {
|
||||||
|
describe('downloadMavenXml', () => {
|
||||||
|
it('returns empty object for unsupported protocols', async () => {
|
||||||
|
const res = await downloadMavenXml(
|
||||||
|
null,
|
||||||
|
parseUrl('unsupported://server.com/')
|
||||||
|
);
|
||||||
|
expect(res).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty object for invalid URLs', async () => {
|
||||||
|
const res = await downloadMavenXml(null, null);
|
||||||
|
expect(res).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('downloadS3Protocol', () => {
|
||||||
|
it('returns null for non-S3 URLs', async () => {
|
||||||
|
const res = await downloadS3Protocol(parseUrl('http://not-s3.com/'));
|
||||||
|
expect(res).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkResource', () => {
|
||||||
|
it('returns not found for unsupported protocols', async () => {
|
||||||
|
const res = await checkResource(null, 'unsupported://server.com/');
|
||||||
|
expect(res).toBe('not-found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error for invalid URLs', async () => {
|
||||||
|
const res = await checkResource(null, 'not-a-valid-url');
|
||||||
|
expect(res).toBe('error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkS3Resource', () => {
|
||||||
|
it('returns error for non-S3 URLs', async () => {
|
||||||
|
const res = await checkS3Resource(parseUrl('http://not-s3.com/'));
|
||||||
|
expect(res).toBe('error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Blob } from 'buffer';
|
||||||
|
import { Readable } from 'stream';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { XmlDocument } from 'xmldoc';
|
import { XmlDocument } from 'xmldoc';
|
||||||
import { HOST_DISABLED } from '../../../constants/error-messages';
|
import { HOST_DISABLED } from '../../../constants/error-messages';
|
||||||
|
@ -6,9 +8,10 @@ import { ExternalHostError } from '../../../types/errors/external-host-error';
|
||||||
import type { Http } from '../../../util/http';
|
import type { Http } from '../../../util/http';
|
||||||
import type { HttpResponse } from '../../../util/http/types';
|
import type { HttpResponse } from '../../../util/http/types';
|
||||||
import { regEx } from '../../../util/regex';
|
import { regEx } from '../../../util/regex';
|
||||||
|
import { getS3Client, parseS3Url } from '../../../util/s3';
|
||||||
|
import { streamToString } from '../../../util/streams';
|
||||||
import { parseUrl } from '../../../util/url';
|
import { parseUrl } from '../../../util/url';
|
||||||
import { normalizeDate } from '../metadata';
|
import { normalizeDate } from '../metadata';
|
||||||
|
|
||||||
import type { ReleaseResult } from '../types';
|
import type { ReleaseResult } from '../types';
|
||||||
import { MAVEN_REPO } from './common';
|
import { MAVEN_REPO } from './common';
|
||||||
import type {
|
import type {
|
||||||
|
@ -93,15 +96,60 @@ export async function downloadHttpProtocol(
|
||||||
// istanbul ignore next
|
// istanbul ignore next
|
||||||
logger.debug({ failedUrl }, 'Unsupported host');
|
logger.debug({ failedUrl }, 'Unsupported host');
|
||||||
} else {
|
} else {
|
||||||
logger.info({ failedUrl, err }, 'Unknown error');
|
logger.info({ failedUrl, err }, 'Unknown HTTP download error');
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkHttpResource(
|
function isS3NotFound(err: Error): boolean {
|
||||||
|
return err.message === 'NotFound' || err.message === 'NoSuchKey';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadS3Protocol(pkgUrl: URL): Promise<string | null> {
|
||||||
|
logger.trace({ url: pkgUrl.toString() }, `Attempting to load S3 dependency`);
|
||||||
|
try {
|
||||||
|
const s3Url = parseS3Url(pkgUrl);
|
||||||
|
if (s3Url === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { Body: res } = await getS3Client().getObject(s3Url);
|
||||||
|
|
||||||
|
// istanbul ignore if
|
||||||
|
if (res instanceof Blob) {
|
||||||
|
return res.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res instanceof Readable) {
|
||||||
|
return streamToString(res);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const failedUrl = pkgUrl.toString();
|
||||||
|
if (err.name === 'CredentialsProviderError') {
|
||||||
|
logger.debug(
|
||||||
|
{ failedUrl },
|
||||||
|
'Dependency lookup authorization failed. Please correct AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY env vars'
|
||||||
|
);
|
||||||
|
} else if (err.message === 'Region is missing') {
|
||||||
|
logger.debug(
|
||||||
|
{ failedUrl },
|
||||||
|
'Dependency lookup failed. Please a correct AWS_REGION env var'
|
||||||
|
);
|
||||||
|
} else if (isS3NotFound(err)) {
|
||||||
|
logger.trace({ failedUrl }, `S3 url not found`);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
{ failedUrl, message: err.message },
|
||||||
|
'Unknown S3 download error'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkHttpResource(
|
||||||
http: Http,
|
http: Http,
|
||||||
pkgUrl: URL | string
|
pkgUrl: URL
|
||||||
): Promise<HttpResourceCheckResult> {
|
): Promise<HttpResourceCheckResult> {
|
||||||
try {
|
try {
|
||||||
const res = await http.head(pkgUrl.toString());
|
const res = await http.head(pkgUrl.toString());
|
||||||
|
@ -130,6 +178,58 @@ export async function checkHttpResource(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function checkS3Resource(
|
||||||
|
pkgUrl: URL
|
||||||
|
): Promise<HttpResourceCheckResult> {
|
||||||
|
try {
|
||||||
|
const s3Url = parseS3Url(pkgUrl);
|
||||||
|
if (s3Url === null) {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
const response = await getS3Client().headObject(s3Url);
|
||||||
|
if (response.DeleteMarker) {
|
||||||
|
return 'not-found';
|
||||||
|
}
|
||||||
|
if (response.LastModified) {
|
||||||
|
return response.LastModified;
|
||||||
|
}
|
||||||
|
return 'found';
|
||||||
|
} catch (err) {
|
||||||
|
if (isS3NotFound(err)) {
|
||||||
|
return 'not-found';
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
{ pkgUrl, name: err.name, message: err.message },
|
||||||
|
`Can't check S3 resource existence`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkResource(
|
||||||
|
http: Http,
|
||||||
|
pkgUrl: URL | string
|
||||||
|
): Promise<HttpResourceCheckResult> {
|
||||||
|
const parsedUrl = typeof pkgUrl === 'string' ? parseUrl(pkgUrl) : pkgUrl;
|
||||||
|
if (parsedUrl === null) {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
switch (parsedUrl.protocol) {
|
||||||
|
case 'http:':
|
||||||
|
case 'https:':
|
||||||
|
return await checkHttpResource(http, parsedUrl);
|
||||||
|
case 's3:':
|
||||||
|
return await checkS3Resource(parsedUrl);
|
||||||
|
default:
|
||||||
|
logger.debug(
|
||||||
|
{ url: pkgUrl.toString() },
|
||||||
|
`Unsupported Maven protocol in check resource`
|
||||||
|
);
|
||||||
|
return 'not-found';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function containsPlaceholder(str: string): boolean {
|
function containsPlaceholder(str: string): boolean {
|
||||||
return regEx(/\${.*?}/g).test(str);
|
return regEx(/\${.*?}/g).test(str);
|
||||||
}
|
}
|
||||||
|
@ -146,7 +246,6 @@ export async function downloadMavenXml(
|
||||||
http: Http,
|
http: Http,
|
||||||
pkgUrl: URL | null
|
pkgUrl: URL | null
|
||||||
): Promise<MavenXml> {
|
): Promise<MavenXml> {
|
||||||
/* istanbul ignore if */
|
|
||||||
if (!pkgUrl) {
|
if (!pkgUrl) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -166,8 +265,8 @@ export async function downloadMavenXml(
|
||||||
} = await downloadHttpProtocol(http, pkgUrl));
|
} = await downloadHttpProtocol(http, pkgUrl));
|
||||||
break;
|
break;
|
||||||
case 's3:':
|
case 's3:':
|
||||||
logger.debug('Skipping s3 dependency');
|
rawContent = (await downloadS3Protocol(pkgUrl)) ?? undefined;
|
||||||
return {};
|
break;
|
||||||
default:
|
default:
|
||||||
logger.debug({ url: pkgUrl.toString() }, `Unsupported Maven protocol`);
|
logger.debug({ url: pkgUrl.toString() }, `Unsupported Maven protocol`);
|
||||||
return {};
|
return {};
|
||||||
|
|
|
@ -4,12 +4,12 @@ import {
|
||||||
GitRef,
|
GitRef,
|
||||||
} from 'azure-devops-node-api/interfaces/GitInterfaces.js';
|
} from 'azure-devops-node-api/interfaces/GitInterfaces.js';
|
||||||
import { logger } from '../../../logger';
|
import { logger } from '../../../logger';
|
||||||
|
import { streamToString } from '../../../util/streams';
|
||||||
import * as azureApi from './azure-got-wrapper';
|
import * as azureApi from './azure-got-wrapper';
|
||||||
import {
|
import {
|
||||||
getBranchNameWithoutRefsPrefix,
|
getBranchNameWithoutRefsPrefix,
|
||||||
getBranchNameWithoutRefsheadsPrefix,
|
getBranchNameWithoutRefsheadsPrefix,
|
||||||
getNewBranchName,
|
getNewBranchName,
|
||||||
streamToString,
|
|
||||||
} from './util';
|
} from './util';
|
||||||
|
|
||||||
const mergePolicyGuid = 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab'; // Magic GUID for merge strategy policy configurations
|
const mergePolicyGuid = 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab'; // Magic GUID for merge strategy policy configurations
|
||||||
|
|
|
@ -22,6 +22,7 @@ import * as git from '../../../util/git';
|
||||||
import * as hostRules from '../../../util/host-rules';
|
import * as hostRules from '../../../util/host-rules';
|
||||||
import { regEx } from '../../../util/regex';
|
import { regEx } from '../../../util/regex';
|
||||||
import { sanitize } from '../../../util/sanitize';
|
import { sanitize } from '../../../util/sanitize';
|
||||||
|
import { streamToString } from '../../../util/streams';
|
||||||
import { ensureTrailingSlash } from '../../../util/url';
|
import { ensureTrailingSlash } from '../../../util/url';
|
||||||
import type {
|
import type {
|
||||||
BranchStatusConfig,
|
BranchStatusConfig,
|
||||||
|
@ -53,7 +54,6 @@ import {
|
||||||
getRepoByName,
|
getRepoByName,
|
||||||
getStorageExtraCloneOpts,
|
getStorageExtraCloneOpts,
|
||||||
max4000Chars,
|
max4000Chars,
|
||||||
streamToString,
|
|
||||||
} from './util';
|
} from './util';
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
import { streamToString } from '../../../util/streams';
|
||||||
import {
|
import {
|
||||||
getBranchNameWithoutRefsheadsPrefix,
|
getBranchNameWithoutRefsheadsPrefix,
|
||||||
getGitStatusContextCombinedName,
|
getGitStatusContextCombinedName,
|
||||||
|
@ -9,7 +10,6 @@ import {
|
||||||
getRepoByName,
|
getRepoByName,
|
||||||
getStorageExtraCloneOpts,
|
getStorageExtraCloneOpts,
|
||||||
max4000Chars,
|
max4000Chars,
|
||||||
streamToString,
|
|
||||||
} from './util';
|
} from './util';
|
||||||
|
|
||||||
describe('modules/platform/azure/util', () => {
|
describe('modules/platform/azure/util', () => {
|
||||||
|
|
|
@ -118,19 +118,6 @@ export function getRenovatePRFormat(azurePr: GitPullRequest): AzurePr {
|
||||||
} as AzurePr;
|
} as AzurePr;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function streamToString(
|
|
||||||
stream: NodeJS.ReadableStream
|
|
||||||
): Promise<string> {
|
|
||||||
const chunks: Uint8Array[] = [];
|
|
||||||
|
|
||||||
const p = await new Promise<string>((resolve, reject) => {
|
|
||||||
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
||||||
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
||||||
stream.on('error', (err) => reject(err));
|
|
||||||
});
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStorageExtraCloneOpts(config: HostRule): GitOptions {
|
export function getStorageExtraCloneOpts(config: HostRule): GitOptions {
|
||||||
let authType: string;
|
let authType: string;
|
||||||
let authValue: string;
|
let authValue: string;
|
||||||
|
|
24
lib/util/s3.spec.ts
Normal file
24
lib/util/s3.spec.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { getS3Client, parseS3Url } from './s3';
|
||||||
|
|
||||||
|
describe('util/s3', () => {
|
||||||
|
it('parses S3 URLs', () => {
|
||||||
|
expect(parseS3Url('s3://bucket/key/path')).toEqual({
|
||||||
|
Bucket: 'bucket',
|
||||||
|
Key: 'key/path',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for non-S3 URLs', () => {
|
||||||
|
expect(parseS3Url('http://example.com/key/path')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for invalid URLs', () => {
|
||||||
|
expect(parseS3Url('thisisnotaurl')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a singleton S3 client instance', () => {
|
||||||
|
const client1 = getS3Client();
|
||||||
|
const client2 = getS3Client();
|
||||||
|
expect(client1).toBe(client2);
|
||||||
|
});
|
||||||
|
});
|
30
lib/util/s3.ts
Normal file
30
lib/util/s3.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// Singleton S3 instance initialized on-demand.
|
||||||
|
import { S3 } from '@aws-sdk/client-s3';
|
||||||
|
import { parseUrl } from './url';
|
||||||
|
|
||||||
|
let s3Instance: S3 | undefined;
|
||||||
|
export function getS3Client(): S3 {
|
||||||
|
if (!s3Instance) {
|
||||||
|
s3Instance = new S3({});
|
||||||
|
}
|
||||||
|
return s3Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3UrlParts {
|
||||||
|
Bucket: string;
|
||||||
|
Key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseS3Url(rawUrl: URL | string): S3UrlParts | null {
|
||||||
|
const parsedUrl = typeof rawUrl === 'string' ? parseUrl(rawUrl) : rawUrl;
|
||||||
|
if (parsedUrl === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (parsedUrl.protocol !== 's3:') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
Bucket: parsedUrl.host,
|
||||||
|
Key: parsedUrl.pathname.substring(1),
|
||||||
|
};
|
||||||
|
}
|
11
lib/util/streams.spec.ts
Normal file
11
lib/util/streams.spec.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
import { streamToString } from './streams';
|
||||||
|
|
||||||
|
describe('util/streams', () => {
|
||||||
|
describe('streamToString', () => {
|
||||||
|
it('handles Readables', async () => {
|
||||||
|
const res = await streamToString(Readable.from(['abc', 'zxc']));
|
||||||
|
expect(res).toBe('abczxc');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
14
lib/util/streams.ts
Normal file
14
lib/util/streams.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
|
export async function streamToString(
|
||||||
|
stream: NodeJS.ReadableStream
|
||||||
|
): Promise<string> {
|
||||||
|
const readable = Readable.from(stream);
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
const p = await new Promise<string>((resolve, reject) => {
|
||||||
|
readable.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||||
|
readable.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
||||||
|
readable.on('error', (err) => reject(err));
|
||||||
|
});
|
||||||
|
return p;
|
||||||
|
}
|
|
@ -134,6 +134,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-ec2": "3.72.0",
|
"@aws-sdk/client-ec2": "3.72.0",
|
||||||
"@aws-sdk/client-ecr": "3.72.0",
|
"@aws-sdk/client-ecr": "3.72.0",
|
||||||
|
"@aws-sdk/client-s3": "3.72.0",
|
||||||
"@breejs/later": "4.1.0",
|
"@breejs/later": "4.1.0",
|
||||||
"@cheap-glitch/mi-cron": "1.0.1",
|
"@cheap-glitch/mi-cron": "1.0.1",
|
||||||
"@iarna/toml": "2.2.5",
|
"@iarna/toml": "2.2.5",
|
||||||
|
@ -215,7 +216,6 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/core": "1.7.0",
|
"@actions/core": "1.7.0",
|
||||||
"@aws-sdk/client-s3": "3.72.0",
|
|
||||||
"@jest/globals": "27.5.1",
|
"@jest/globals": "27.5.1",
|
||||||
"@jest/reporters": "27.5.1",
|
"@jest/reporters": "27.5.1",
|
||||||
"@jest/test-result": "27.5.1",
|
"@jest/test-result": "27.5.1",
|
||||||
|
|
Loading…
Reference in a new issue