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(rubygems): support GitHub Packages #11107

Merged
merged 29 commits into from
Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2c2ad4b
feat(rubygems): support GitHub Packages
qnighy Aug 5, 2021
a96b073
Separate tests by a line
qnighy Aug 6, 2021
92a75a9
Add types for /api/v1/dependencies responses
qnighy Aug 6, 2021
3b7a784
Fix type error
qnighy Aug 6, 2021
9e39abb
Merge branch 'main' into bundler-old-api
qnighy Aug 10, 2021
12c51d3
Use marshal instead of @qnighy/marshal
qnighy Aug 10, 2021
8fa668d
Remove unnecessary branch
qnighy Aug 10, 2021
75326ce
Merge branch 'main' into bundler-old-api
qnighy Aug 12, 2021
9973ce6
Use export assignment to model CJS correctly
qnighy Aug 12, 2021
7340cbe
Remove extra generics from requestBuffer
qnighy Aug 12, 2021
0db6f34
Give better typing for responseType
qnighy Aug 12, 2021
90056f3
Remove unused generality from the API
qnighy Aug 12, 2021
45f5693
Merge branch 'main' into bundler-old-api
qnighy Aug 13, 2021
eb9b852
Remove trace snapshots as they're not needed
qnighy Aug 13, 2021
3039b09
Switch endpoints by hostname
qnighy Aug 13, 2021
6370be8
Apply suggestions from code review
viceice Aug 13, 2021
c4aad36
Apply suggestions from code review
viceice Aug 13, 2021
8b38b96
Update lib/datasource/rubygems/get-github-packages.ts
viceice Aug 13, 2021
1f905b8
Merge branch 'main' into bundler-old-api
viceice Aug 13, 2021
d3053b4
chore: fix types
viceice Aug 13, 2021
a0d9214
test: update snapshots
viceice Aug 13, 2021
9a38a2b
chore: refactor
viceice Aug 13, 2021
6e00568
feat: add gitlab to known fallback hosts
viceice Aug 13, 2021
9cdff1a
Merge branch 'main' into bundler-old-api
rarkins Aug 18, 2021
d9055c8
Merge branch 'main' into bundler-old-api
JamieMagee Aug 18, 2021
b9ade79
Apply suggestions from code review
viceice Aug 19, 2021
88906e5
Merge branch 'main' into bundler-old-api
viceice Aug 19, 2021
bcfaae1
Update lib/datasource/rubygems/get.ts
viceice Aug 19, 2021
606a0ed
Merge branch 'main' into bundler-old-api
rarkins Aug 20, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
1,373 changes: 1,364 additions & 9 deletions lib/datasource/rubygems/__snapshots__/index.spec.ts.snap

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions lib/datasource/rubygems/common.ts
Original file line number Diff line number Diff line change
@@ -1 +1,40 @@
import Marshal from 'marshal';
import urlJoin from 'url-join';
import { logger } from '../../logger';
import { Http } from '../../util/http';
import { getQueryString } from '../../util/url';

export const id = 'rubygems';
export const http = new Http(id);

export const knownFallbackHosts = ['rubygems.pkg.github.com', 'gitlab.com'];

export async function fetchJson<T>(
dependency: string,
registry: string,
path: string
): Promise<T> {
const url = urlJoin(registry, path, `${dependency}.json`);

logger.trace({ registry, dependency, url }, `RubyGems lookup request`);
const response = (await http.getJson<T>(url)) || {
body: undefined,
};

return response.body;
}

export async function fetchBuffer<T>(
dependency: string,
registry: string,
path: string
): Promise<T> {
const url = `${urlJoin(registry, path)}?${getQueryString({
gems: dependency,
})}`;

logger.trace({ registry, dependency, url }, `RubyGems lookup request`);
const response = await http.getBuffer(url);

return new Marshal(response.body).parsed as T;
}
5 changes: 1 addition & 4 deletions lib/datasource/rubygems/get-rubygems-org.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { logger } from '../../logger';
import { ExternalHostError } from '../../types/errors/external-host-error';
import { getElapsedMinutes } from '../../util/date';
import { Http } from '../../util/http';
import type { ReleaseResult } from '../types';
import { id } from './common';

const http = new Http(id);
import { http } from './common';

let lastSync = new Date('2000-01-01');
let packageReleases: Record<string, string[]> = Object.create(null); // Because we might need a "constructor" key
Expand Down
83 changes: 55 additions & 28 deletions lib/datasource/rubygems/get.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,65 @@
import urlJoin from 'url-join';
import { logger } from '../../logger';
import { Http } from '../../util/http';
import type { OutgoingHttpHeaders } from '../../util/http/types';
import type { ReleaseResult } from '../types';
import { id } from './common';

const http = new Http(id);
import { HttpError } from '../../util/http/types';
import type { Release, ReleaseResult } from '../types';
import { fetchBuffer, fetchJson } from './common';
import type {
JsonGemVersions,
JsonGemsInfo,
MarshalledVersionInfo,
} from './types';

const INFO_PATH = '/api/v1/gems';
const VERSIONS_PATH = '/api/v1/versions';
const DEPENDENCIES_PATH = '/api/v1/dependencies';

const getHeaders = (): OutgoingHttpHeaders => ({ hostType: id });

export async function fetch(
export async function getDependencyFallback(
dependency: string,
registry: string,
path: string
): Promise<any> {
const headers = getHeaders();

const url = urlJoin(registry, path, `${dependency}.json`);

logger.trace({ dependency }, `RubyGems lookup request: ${String(url)}`);
const response = (await http.getJson(url, { headers })) || {
body: undefined,
registry: string
): Promise<ReleaseResult | null> {
logger.debug(
{ dependency, api: DEPENDENCIES_PATH },
'RubyGems lookup for dependency'
);
const info = await fetchBuffer<MarshalledVersionInfo[]>(
dependency,
registry,
DEPENDENCIES_PATH
);
if (!info || info.length === 0) {
return null;
}
const releases = info.map(({ number: version, platform: rubyPlatform }) => ({
version,
rubyPlatform,
}));
return {
releases,
homepage: null,
sourceUrl: null,
changelogUrl: null,
};

return response.body;
}

export async function getDependency(
dependency: string,
registry: string
): Promise<ReleaseResult | null> {
logger.debug({ dependency }, 'RubyGems lookup for dependency');
const info = await fetch(dependency, registry, INFO_PATH);
logger.debug(
{ dependency, api: INFO_PATH },
'RubyGems lookup for dependency'
);
let info: JsonGemsInfo;

try {
info = await fetchJson(dependency, registry, INFO_PATH);
} catch (error) {
// fallback to deps api on 404
if (error instanceof HttpError && error.response?.statusCode === 404) {
return await getDependencyFallback(dependency, registry);
}
throw error;
}

if (!info) {
logger.debug({ dependency }, 'RubyGems package not found.');
return null;
Expand All @@ -48,10 +73,10 @@ export async function getDependency(
return null;
}

let versions = [];
let releases = [];
let versions: JsonGemVersions[] = [];
let releases: Release[] = [];
try {
versions = await fetch(dependency, registry, VERSIONS_PATH);
versions = await fetchJson(dependency, registry, VERSIONS_PATH);
} catch (err) {
if (err.statusCode === 400 || err.statusCode === 404) {
logger.debug(
Expand All @@ -63,13 +88,15 @@ export async function getDependency(
}
}

// TODO: invalid properties for `Release` see #11312

if (versions.length === 0 && info.version) {
logger.warn('falling back to the version from the info endpoint');
releases = [
{
version: info.version,
rubyPlatform: info.platform,
},
} as Release,
];
} else {
releases = versions.map(
Expand Down
43 changes: 42 additions & 1 deletion lib/datasource/rubygems/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { getPkgReleases } from '..';
import * as httpMock from '../../../test/http-mock';
import { loadFixture, loadJsonFixture } from '../../../test/util';
import {
loadBinaryFixture,
loadFixture,
loadJsonFixture,
} from '../../../test/util';
import * as rubyVersioning from '../../versioning/ruby';
import { resetCache } from './get-rubygems-org';
import * as rubygems from '.';

const rubygemsOrgVersions = loadFixture('rubygems-org.txt');
const railsInfo = loadJsonFixture('rails/info.json');
const railsVersions = loadJsonFixture('rails/versions.json');
const railsDependencies = loadBinaryFixture('dependencies-rails.dat');
const emptyMarshalArray = Buffer.from([4, 8, 91, 0]);

describe('datasource/rubygems/index', () => {
describe('getReleases', () => {
Expand Down Expand Up @@ -149,6 +155,7 @@ describe('datasource/rubygems/index', () => {
expect(await getPkgReleases(params)).toBeNull();
expect(httpMock.getTrace()).toMatchSnapshot();
});

it('falls back to info when version request fails', async () => {
httpMock
.scope('https://thirdparty.com/')
Expand All @@ -173,5 +180,39 @@ describe('datasource/rubygems/index', () => {
.reply(500);
expect(await getPkgReleases(params)).toBeNull();
});

it('falls back to dependencies api', async () => {
httpMock
.scope('https://thirdparty.com/')
.get('/api/v1/gems/rails.json')
.reply(404, railsInfo)
.get('/api/v1/dependencies?gems=rails')
.reply(200, railsDependencies);

const res = await getPkgReleases(params);
expect(res?.releases).toHaveLength(339);
});

it('returns null for GitHub Packages package miss', async () => {
const newparams = { ...params };
newparams.registryUrls = ['https://rubygems.pkg.github.com/example'];
httpMock
.scope('https://rubygems.pkg.github.com/example')
.get('/api/v1/dependencies?gems=rails')
.reply(200, emptyMarshalArray);
expect(await getPkgReleases(newparams)).toBeNull();
});

it('returns a dep for GitHub Packages package hit', async () => {
const newparams = { ...params };
newparams.registryUrls = ['https://rubygems.pkg.github.com/example'];
httpMock
.scope('https://rubygems.pkg.github.com/example')
.get('/api/v1/dependencies?gems=rails')
.reply(200, railsDependencies);
const res = await getPkgReleases(newparams);
expect(res.releases).toHaveLength(339);
expect(res).toMatchSnapshot();
});
});
});
14 changes: 9 additions & 5 deletions lib/datasource/rubygems/releases.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { parseUrl } from '../../util/url';
import type { GetReleasesConfig, ReleaseResult } from '../types';
viceice marked this conversation as resolved.
Show resolved Hide resolved
import { getDependency } from './get';
import { knownFallbackHosts } from './common';
import { getDependency, getDependencyFallback } from './get';
import { getRubygemsOrgDependency } from './get-rubygems-org';

export function getReleases({
lookupName,
registryUrl,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
// prettier-ignore
if (registryUrl.endsWith('rubygems.org')) { // lgtm [js/incomplete-url-substring-sanitization]
return getRubygemsOrgDependency(lookupName);
}
if (parseUrl(registryUrl)?.hostname === 'rubygems.org') {
return getRubygemsOrgDependency(lookupName);
}
if (knownFallbackHosts.includes(parseUrl(registryUrl)?.hostname)) {
return getDependencyFallback(lookupName, registryUrl);
}
return getDependency(lookupName, registryUrl);
}
44 changes: 44 additions & 0 deletions lib/datasource/rubygems/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* see https://guides.rubygems.org/rubygems-org-api/#get---apiv1dependenciesgemscomma-delimited-gem-names
*/
export interface MarshalledVersionInfo {
name: string;
number: string;
platform: string;
dependencies: MarshalledDependency[];
}

export type MarshalledDependency = [name: string, version: string];

export interface JsonGemDependency {
name: string;
requirements: string;
}

/**
* see https://guides.rubygems.org/rubygems-org-api/#get---apiv1gemsgem-namejsonyaml
*/
export interface JsonGemsInfo {
// FIXME: This property doesn't exist in api
changelog_uri: string;
dependencies: {
development: JsonGemDependency;
runtime: JsonGemDependency;
};
homepage_uri: string;
name: string;
platform?: string;
source_code_uri: string;
version?: string;
}

/**
* see https://guides.rubygems.org/rubygems-org-api/#get---apiv1versionsgem-namejsonyaml
*/
export interface JsonGemVersions {
created_at: string;
number: string;
platform: string;
rubygems_version: string;
ruby_version: string;
}
17 changes: 17 additions & 0 deletions lib/types/marshal.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
declare module 'marshal' {
class Marshal {
public parsed?: unknown;

constructor();
constructor(buffer: Buffer);
constructor(buffer: string, encoding: BufferEncoding);

public load(buffer: Buffer): this;
public load(buffer: string, encoding: BufferEncoding): this;

public toString(encoding?: BufferEncoding): string;
public toJSON(): unknown;
}

export = Marshal;
}
Loading