mirror of https://github.com/renovatebot/renovate
258 lines
7.9 KiB
TypeScript
258 lines
7.9 KiB
TypeScript
import is from '@sindresorhus/is';
|
|
import { DateTime } from 'luxon';
|
|
import { logger } from '../../../logger';
|
|
import { ExternalHostError } from '../../../types/errors/external-host-error';
|
|
import { cache } from '../../../util/cache/package/decorator';
|
|
import { filterMap } from '../../../util/filter-map';
|
|
import { HttpError } from '../../../util/http';
|
|
import * as p from '../../../util/promises';
|
|
import { newlineRegex, regEx } from '../../../util/regex';
|
|
import goVersioning from '../../versioning/go-mod-directive';
|
|
import { Datasource } from '../datasource';
|
|
import type { GetReleasesConfig, Release, ReleaseResult } from '../types';
|
|
import { BaseGoDatasource } from './base';
|
|
import { getSourceUrl } from './common';
|
|
import { parseGoproxy, parseNoproxy } from './goproxy-parser';
|
|
import { GoDirectDatasource } from './releases-direct';
|
|
import type { VersionInfo } from './types';
|
|
|
|
const modRegex = regEx(/^(?<baseMod>.*?)(?:[./]v(?<majorVersion>\d+))?$/);
|
|
|
|
/**
|
|
* @see https://go.dev/ref/mod#pseudo-versions
|
|
*/
|
|
const pseudoVersionRegex = regEx(
|
|
/v\d+\.\d+\.\d+-(?:\w+\.)?(?:0\.)?(?<timestamp>\d{14})-(?<digest>[a-f0-9]{12})/i,
|
|
);
|
|
|
|
export function pseudoVersionToRelease(pseudoVersion: string): Release | null {
|
|
const match = pseudoVersion.match(pseudoVersionRegex)?.groups;
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const { digest: newDigest, timestamp } = match;
|
|
const releaseTimestamp = DateTime.fromFormat(timestamp, 'yyyyMMddHHmmss', {
|
|
zone: 'UTC',
|
|
}).toISO({ suppressMilliseconds: true });
|
|
|
|
return {
|
|
version: pseudoVersion,
|
|
newDigest,
|
|
releaseTimestamp,
|
|
};
|
|
}
|
|
|
|
export class GoProxyDatasource extends Datasource {
|
|
static readonly id = 'go-proxy';
|
|
|
|
constructor() {
|
|
super(GoProxyDatasource.id);
|
|
}
|
|
|
|
readonly direct = new GoDirectDatasource();
|
|
|
|
@cache({
|
|
namespace: `datasource-${GoProxyDatasource.id}`,
|
|
key: (config: GetReleasesConfig) => GoProxyDatasource.getCacheKey(config),
|
|
})
|
|
async getReleases(config: GetReleasesConfig): Promise<ReleaseResult | null> {
|
|
const { packageName } = config;
|
|
logger.trace(`goproxy.getReleases(${packageName})`);
|
|
const goproxy = process.env.GOPROXY ?? 'https://proxy.golang.org,direct';
|
|
if (goproxy === 'direct') {
|
|
return this.direct.getReleases(config);
|
|
}
|
|
const proxyList = parseGoproxy(goproxy);
|
|
const noproxy = parseNoproxy();
|
|
|
|
let result: ReleaseResult | null = null;
|
|
|
|
if (noproxy?.test(packageName)) {
|
|
logger.debug(`Fetching ${packageName} via GONOPROXY match`);
|
|
result = await this.direct.getReleases(config);
|
|
return result;
|
|
}
|
|
|
|
for (const { url, fallback } of proxyList) {
|
|
try {
|
|
if (url === 'off') {
|
|
break;
|
|
} else if (url === 'direct') {
|
|
result = await this.direct.getReleases(config);
|
|
break;
|
|
}
|
|
|
|
const res = await this.getVersionsWithInfo(url, packageName);
|
|
if (res.releases.length) {
|
|
result = res;
|
|
break;
|
|
}
|
|
} catch (err) {
|
|
const potentialHttpError =
|
|
err instanceof ExternalHostError ? err.err : err;
|
|
const statusCode = potentialHttpError?.response?.statusCode;
|
|
const canFallback =
|
|
fallback === '|' ? true : statusCode === 404 || statusCode === 410;
|
|
const msg = canFallback
|
|
? 'Goproxy error: trying next URL provided with GOPROXY'
|
|
: 'Goproxy error: skipping other URLs provided with GOPROXY';
|
|
logger.debug({ err }, msg);
|
|
if (!canFallback) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (result && !result.sourceUrl) {
|
|
try {
|
|
const datasource = await BaseGoDatasource.getDatasource(packageName);
|
|
const sourceUrl = getSourceUrl(datasource);
|
|
if (sourceUrl) {
|
|
result.sourceUrl = sourceUrl;
|
|
}
|
|
} catch (err) {
|
|
logger.trace({ err }, `Can't get datasource for ${packageName}`);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Avoid ambiguity when serving from case-insensitive file systems.
|
|
*
|
|
* @see https://golang.org/ref/mod#goproxy-protocol
|
|
*/
|
|
encodeCase(input: string): string {
|
|
return input.replace(regEx(/([A-Z])/g), (x) => `!${x.toLowerCase()}`);
|
|
}
|
|
|
|
async listVersions(baseUrl: string, packageName: string): Promise<Release[]> {
|
|
const url = `${baseUrl}/${this.encodeCase(packageName)}/@v/list`;
|
|
const { body } = await this.http.get(url);
|
|
return filterMap(body.split(newlineRegex), (str) => {
|
|
if (!is.nonEmptyStringAndNotWhitespace(str)) {
|
|
return null;
|
|
}
|
|
|
|
const [version, releaseTimestamp] = str.trim().split(regEx(/\s+/));
|
|
const release: Release = pseudoVersionToRelease(version) ?? { version };
|
|
|
|
if (releaseTimestamp) {
|
|
release.releaseTimestamp = releaseTimestamp;
|
|
}
|
|
|
|
return release;
|
|
});
|
|
}
|
|
|
|
async versionInfo(
|
|
baseUrl: string,
|
|
packageName: string,
|
|
version: string,
|
|
): Promise<Release> {
|
|
const url = `${baseUrl}/${this.encodeCase(packageName)}/@v/${version}.info`;
|
|
const res = await this.http.getJson<VersionInfo>(url);
|
|
|
|
const result: Release = {
|
|
version: res.body.Version,
|
|
};
|
|
|
|
if (res.body.Time) {
|
|
result.releaseTimestamp = res.body.Time;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async getLatestVersion(
|
|
baseUrl: string,
|
|
packageName: string,
|
|
): Promise<string | null> {
|
|
try {
|
|
const url = `${baseUrl}/${this.encodeCase(packageName)}/@latest`;
|
|
const res = await this.http.getJson<VersionInfo>(url);
|
|
return res.body.Version;
|
|
} catch (err) {
|
|
logger.trace({ err }, 'Failed to get latest version');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async getVersionsWithInfo(
|
|
baseUrl: string,
|
|
packageName: string,
|
|
): Promise<ReleaseResult> {
|
|
const isGopkgin = packageName.startsWith('gopkg.in/');
|
|
const majorSuffixSeparator = isGopkgin ? '.' : '/';
|
|
const modParts = packageName.match(modRegex)?.groups;
|
|
const baseMod = modParts?.baseMod ?? /* istanbul ignore next */ packageName;
|
|
const packageMajor = parseInt(modParts?.majorVersion ?? '0');
|
|
|
|
const result: ReleaseResult = { releases: [] };
|
|
for (let major = packageMajor; ; major += 1) {
|
|
let pkg = `${baseMod}${majorSuffixSeparator}v${major}`;
|
|
if (!isGopkgin && major < 2) {
|
|
pkg = baseMod;
|
|
major += 1; // v0 and v1 are the same module
|
|
}
|
|
|
|
try {
|
|
const res = await this.listVersions(baseUrl, pkg);
|
|
const releases = await p.map(res, async (versionInfo) => {
|
|
const { version, newDigest, releaseTimestamp } = versionInfo;
|
|
|
|
if (releaseTimestamp) {
|
|
return { version, newDigest, releaseTimestamp };
|
|
}
|
|
|
|
try {
|
|
return await this.versionInfo(baseUrl, pkg, version);
|
|
} catch (err) {
|
|
logger.trace({ err }, `Can't obtain data from ${baseUrl}`);
|
|
return { version };
|
|
}
|
|
});
|
|
result.releases.push(...releases);
|
|
} catch (err) {
|
|
const potentialHttpError =
|
|
err instanceof ExternalHostError ? err.err : err;
|
|
if (
|
|
potentialHttpError instanceof HttpError &&
|
|
potentialHttpError.response?.statusCode === 404 &&
|
|
major !== packageMajor
|
|
) {
|
|
break;
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
|
|
const latestVersion = await this.getLatestVersion(baseUrl, pkg);
|
|
if (latestVersion) {
|
|
result.tags ??= {};
|
|
result.tags.latest ??= latestVersion;
|
|
if (goVersioning.isGreaterThan(latestVersion, result.tags.latest)) {
|
|
result.tags.latest = latestVersion;
|
|
}
|
|
if (!result.releases.length) {
|
|
const releaseFromLatest = pseudoVersionToRelease(latestVersion);
|
|
if (releaseFromLatest) {
|
|
result.releases.push(releaseFromLatest);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static getCacheKey({ packageName }: GetReleasesConfig): string {
|
|
const goproxy = process.env.GOPROXY;
|
|
const noproxy = parseNoproxy();
|
|
// TODO: types (#22198)
|
|
return `${packageName}@@${goproxy}@@${noproxy?.toString()}`;
|
|
}
|
|
}
|