import { ExternalHostError } from '../../../types/errors/external-host-error.ts'; import { withCache } from '../../../util/cache/package/with-cache.ts'; import { regEx } from '../../../util/regex.ts'; import { asTimestamp } from '../../../util/timestamp.ts'; import { joinUrlParts } from '../../../util/url.ts'; import { isVersion, id as semverVersioningId, } from '../../versioning/semver/index.ts'; import { Datasource } from '../datasource.ts'; import type { GetReleasesConfig, Release, ReleaseResult } from '../types.ts'; const lineTerminationRegex = regEx(`\r?\n`); const releaseBeginningChar = '\t{'; const releaseTerminationChar = '\t},'; const releaseDateRegex = regEx( `Date\\{(?\\d+),\\s+(?\\d+),\\s+(?\\d+)\\}`, ); const releaseVersionRegex = regEx( `Version\\{(?\\d+),\\s+(?\\d+),\\s+(?\\d+)\\}`, ); const releaseFutureRegex = regEx(`Future:\\s+true`); export class GolangVersionDatasource extends Datasource { static readonly id = 'golang-version'; constructor() { super(GolangVersionDatasource.id); } override readonly defaultRegistryUrls = [ 'https://raw.githubusercontent.com/golang/website', ]; override readonly customRegistrySupport = true; override readonly defaultVersioning = semverVersioningId; override readonly releaseTimestampSupport = true; override readonly releaseTimestampNote = 'The release timestamp is determined from the `Date` field in the results.'; override readonly sourceUrlSupport = 'package'; override readonly sourceUrlNote = 'We use the URL: https://github.com/golang/go.'; private async _getReleases({ registryUrl, }: GetReleasesConfig): Promise { /* v8 ignore next -- should never happen */ if (!registryUrl) { return null; } const res: ReleaseResult = { homepage: 'https://go.dev/', sourceUrl: 'https://github.com/golang/go', releases: [], }; const golangVersionsUrl = joinUrlParts( registryUrl, '/HEAD/internal/history/release.go', ); const response = await this.http.getText(golangVersionsUrl); const lines = response.body.split(lineTerminationRegex); const startOfReleases = lines.indexOf('var Releases = []*Release{'); if (startOfReleases === -1) { throw new ExternalHostError( new Error('Invalid file - could not find the Releases section'), ); } // Remove part before releases lines.splice(0, startOfReleases + 1); // Parse the release list let release: Omit & { version?: string | undefined } = { version: undefined, }; let skipFutureRelease = false; while (lines.length !== 0) { const line = lines.shift()!; if (line === releaseBeginningChar) { if (release.version !== undefined) { throw new ExternalHostError( new Error( 'Invalid file - unexpected error while parsing a release', ), ); } } else if (line === releaseTerminationChar) { if (skipFutureRelease) { skipFutureRelease = false; } else { if (release.version === undefined) { throw new ExternalHostError( new Error('Invalid file - release has empty version'), ); } res.releases.push(release as Release); } release = { version: undefined }; } else { const isFutureRelease = releaseFutureRegex.test(line); if (isFutureRelease) { skipFutureRelease = true; } const releaseDateMatch = releaseDateRegex.exec(line); if (releaseDateMatch?.groups) { // Make a valid UTC timestamp const year = releaseDateMatch.groups.year.padStart(4, '0'); const month = releaseDateMatch.groups.month.padStart(2, '0'); const day = releaseDateMatch.groups.day.padStart(2, '0'); release.releaseTimestamp = asTimestamp( `${year}-${month}-${day}T00:00:00.000Z`, ); } const releaseVersionMatch = releaseVersionRegex.exec(line); if (releaseVersionMatch?.groups) { release.version = `${releaseVersionMatch.groups.versionMajor}.${releaseVersionMatch.groups.versionMinor}.${releaseVersionMatch.groups.patch}`; if (!isVersion(release.version)) { throw new ExternalHostError( new Error(`Version ${release.version} is not a valid semver`), ); } } } } if (res.releases.length === 0) { throw new ExternalHostError( new Error(`Invalid file - zero releases extracted`), ); } return res; } getReleases(config: GetReleasesConfig): Promise { return withCache( { namespace: `datasource-${GolangVersionDatasource.id}`, key: 'all', fallback: true, }, () => this._getReleases(config), ); } }