renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
235 lines (234 loc) • 9.46 kB
JavaScript
import { getEnv } from "../../../util/env.js";
import { newlineRegex, regEx } from "../../../util/regex.js";
import { logger } from "../../../logger/index.js";
import { joinUrlParts } from "../../../util/url.js";
import { ExternalHostError } from "../../../types/errors/external-host-error.js";
import { filterMap } from "../../../util/filter-map.js";
import api from "../../versioning/go-mod-directive/index.js";
import { withCache } from "../../../util/cache/package/with-cache.js";
import { RequestError } from "../../../util/http/got.js";
import "../../../util/http/index.js";
import { asTimestamp } from "../../../util/timestamp.js";
import { Datasource } from "../datasource.js";
import { map } from "../../../util/promises.js";
import { BaseGoDatasource } from "./base.js";
import { parseGoproxy, parseNoproxy } from "./goproxy-parser.js";
import { getSourceUrl } from "./common.js";
import { GoDirectDatasource } from "./releases-direct.js";
import { VersionInfo } from "./schema.js";
import { isNonEmptyStringAndNotWhitespace, isTruthy } from "@sindresorhus/is";
//#region lib/modules/datasource/go/releases-goproxy.ts
/** TODO #42566 */
const goVersionRegex = regEx(/^\s*go\s+(?<version>[^\s]+)\s*$/);
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);
function pseudoVersionToRelease(pseudoVersion) {
const match = pseudoVersion.match(pseudoVersionRegex)?.groups;
if (!match) return null;
const { digest: newDigest, timestamp } = match;
return {
version: pseudoVersion,
newDigest,
releaseTimestamp: asTimestamp(timestamp)
};
}
var GoProxyDatasource = class GoProxyDatasource extends Datasource {
static id = "go-proxy";
constructor() {
super(GoProxyDatasource.id);
}
direct = new GoDirectDatasource();
async _getReleases(config) {
const { packageName } = config;
logger.trace(`goproxy.getReleases(${packageName})`);
const goproxy = getEnv().GOPROXY ?? "https://proxy.golang.org,direct";
if (goproxy === "direct") return this.direct.getReleases(config);
const proxyList = parseGoproxy(goproxy);
const noproxy = parseNoproxy();
let result = 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, config.constraintsFiltering);
if (res.releases.length) {
result = res;
break;
}
} catch (err) {
const statusCode = (err instanceof ExternalHostError ? err.err : err)?.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 sourceUrl = getSourceUrl(await BaseGoDatasource.getDatasource(packageName));
if (sourceUrl) result.sourceUrl = sourceUrl;
} catch (err) {
logger.trace({ err }, `Can't get datasource for ${packageName}`);
}
return result;
}
getReleases(config) {
return withCache({
namespace: `datasource-${GoProxyDatasource.id}`,
key: GoProxyDatasource.getCacheKey(config),
fallback: true
}, () => this._getReleases(config));
}
/**
* Avoid ambiguity when serving from case-insensitive file systems.
*
* @see https://golang.org/ref/mod#goproxy-protocol
*/
encodeCase(input) {
return input.replace(regEx(/([A-Z])/g), (x) => `!${x.toLowerCase()}`);
}
async listVersions(baseUrl, packageName) {
const url = joinUrlParts(baseUrl, this.encodeCase(packageName), "@v", "list");
const { body } = await this.http.getText(url);
return filterMap(body.split(newlineRegex), (str) => {
if (!isNonEmptyStringAndNotWhitespace(str)) return null;
const [version, timestamp] = str.trim().split(regEx(/\s+/));
const release = pseudoVersionToRelease(version) ?? { version };
const releaseTimestamp = asTimestamp(timestamp);
if (releaseTimestamp) release.releaseTimestamp = releaseTimestamp;
return release;
});
}
async versionInfo(baseUrl, packageName, version) {
const url = joinUrlParts(baseUrl, this.encodeCase(packageName), "@v", `${version}.info`);
const res = await this.http.getJson(url, VersionInfo);
const result = { version: res.body.Version };
const releaseTimestamp = asTimestamp(res.body.Time);
if (releaseTimestamp) result.releaseTimestamp = releaseTimestamp;
return result;
}
/**
* Retrieve the `go` directive for a given Go Module.
*
* NOTE that this means the `go` directive, not the `toolchain` directive.
*/
async retrieveGoDirectiveForModule(baseUrl, packageName, version) {
return withCache({
namespace: `datasource-${GoProxyDatasource.id}`,
key: GoProxyDatasource.getVersionedCacheKey(packageName, version),
ttlMinutes: 2400 * 60
}, () => this._retrieveGoDirectiveForModule(baseUrl, packageName, version));
}
async _retrieveGoDirectiveForModule(baseUrl, packageName, version) {
const url = joinUrlParts(baseUrl, this.encodeCase(packageName), "@v", `${version}.mod`);
const res = await this.http.getText(url);
let goDirective = void 0;
for (const line of res.body.split("\n")) {
const goVersionMatches = goVersionRegex.exec(line)?.groups;
if (goVersionMatches) {
goDirective = goVersionMatches.version;
break;
}
}
if (!goDirective) return goDirective;
const parts = goDirective.split(".");
if (parts.length === 1) return `${parts[0]}.0.0`;
else if (parts.length === 2) return `${parts[0]}.${parts[1]}.0`;
else return `${parts[0]}.${parts[1]}.${parts[2]}`;
}
async getLatestVersion(baseUrl, packageName) {
try {
const url = joinUrlParts(baseUrl, this.encodeCase(packageName), "@latest");
return (await this.http.getJson(url, VersionInfo)).body.Version;
} catch (err) {
logger.trace({ err }, "Failed to get latest version");
return null;
}
}
async getVersionsWithInfo(baseUrl, packageName, constraintsFiltering) {
const isGopkgin = packageName.startsWith("gopkg.in/");
const majorSuffixSeparator = isGopkgin ? "." : "/";
const modParts = packageName.match(modRegex)?.groups;
const baseMod = modParts?.baseMod ?? packageName;
const packageMajor = parseInt(modParts?.majorVersion ?? "0", 10);
const result = { releases: [] };
for (let major = packageMajor;; major += 1) {
let pkg = `${baseMod}${majorSuffixSeparator}v${major}`;
if (!isGopkgin && major < 2) {
pkg = baseMod;
major += 1;
}
let releases = [];
try {
releases = await map((await this.listVersions(baseUrl, pkg)).filter(({ version }) => {
if (major < 2) return true;
return version.split(regEx(/[^\d]+/)).find(isTruthy) === major.toString();
}), 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 };
}
});
if (constraintsFiltering === "strict") releases = await map(releases, async (rel) => {
try {
const goDirective = await this.retrieveGoDirectiveForModule(baseUrl, pkg, rel.version);
if (goDirective) {
rel.constraints ??= {};
rel.constraints["%goMod"] ??= [];
rel.constraints["%goMod"].push(goDirective);
}
} catch (err) {
logger.trace({ err }, `Can't obtain \`go\` directive from ${baseUrl}`);
}
return rel;
});
result.releases.push(...releases);
} catch (err) {
const potentialHttpError = err instanceof ExternalHostError ? err.err : err;
const status = potentialHttpError.response?.statusCode;
if (potentialHttpError instanceof RequestError && (status === 404 || status === 403) && major !== packageMajor) break;
throw err;
}
const latestVersion = await this.getLatestVersion(baseUrl, pkg);
if (latestVersion) {
result.tags ??= {};
result.tags.latest ??= latestVersion;
if (api.isGreaterThan(latestVersion, result.tags.latest)) result.tags.latest = latestVersion;
if (!result.releases.length) {
const releaseFromLatest = pseudoVersionToRelease(latestVersion);
if (releaseFromLatest) result.releases.push(releaseFromLatest);
}
}
if (!releases.length) break;
}
return result;
}
static getCacheKey({ packageName, constraintsFiltering }) {
const goproxy = getEnv().GOPROXY;
const noproxy = parseNoproxy();
const constraintsFilteringKey = constraintsFiltering && constraintsFiltering !== "none" ? `@@${constraintsFiltering}` : "";
return `${packageName}@@${goproxy}@@${noproxy?.toString()}${constraintsFilteringKey}`;
}
static getVersionedCacheKey(packageName, version) {
return `${packageName}@@${version}@@${getEnv().GOPROXY}@@${parseNoproxy()?.toString()}`;
}
};
//#endregion
export { GoProxyDatasource };
//# sourceMappingURL=releases-goproxy.js.map