diff --git a/packages/std/extra/live_update/from_gitea_releases.bri b/packages/std/extra/live_update/from_gitea_releases.bri new file mode 100644 index 000000000..3a3063909 --- /dev/null +++ b/packages/std/extra/live_update/from_gitea_releases.bri @@ -0,0 +1,140 @@ +import * as std from "/core"; +import { + DEFAULT_LIVE_UPDATE_REGEX_VERSION_MATCH, + validateMatchTag, +} from "./index.bri"; +import type {} from "nushell"; + +// HACK: The `import type` line above is a workaround for this issue: +// https://github.com/brioche-dev/brioche/issues/242 + +/** + * Additional options for the project to update. + * + * @param versionDash - The version of the project (converted in dash format). + * @param versionUnderscore - The version of the project (converted in underscore format). + */ +interface LiveUpdateFromGiteaReleasesProjectExtraOptions { + versionDash?: string; + versionUnderscore?: string; + [key: string]: unknown; // Ignore unknown properties +} + +/** + * Options for the live update from Gitea releases. + * + * @param project - The project export that should be updated. Must include a + * `repository` property containing a Gitea repository URL. + * @param matchTag - A regex value (`/.../`) to extract the version number from + * a tag name. The regex must include a group named "version". If not + * provided, an optional "v" prefix will be stripped and the rest of the + * tag will be checked if it's a semver or semver-like version number. + * @param normalizeVersion - Whether to normalize the version number to + * a semver-like version number. When enabled, the dashes and underscores + * will be replaced with dots. + */ +interface LiveUpdateFromGiteaReleasesOptions { + project: { + version: string; + readonly repository: string; + extra?: LiveUpdateFromGiteaReleasesProjectExtraOptions; + }; + readonly matchTag?: RegExp; + readonly normalizeVersion?: boolean; +} + +/** + * Return a runnable recipe to live-update a project based on the latest release + * tag from a Gitea repository. The project's version will be set based on a + * regex match against the latest tag name. The repository is inferred from the + * `repository` field of the project. + * + * @param options - Options for the live update from Gitea releases. + * + * @returns A runnable recipe to live-update the project + * + * @example + * ```typescript + * export const project = { + * name: "gitea_docs", + * version: "0.1.0", + * repository: "https://gitea.com/gitea/docs.git", + * }; + * + * export function liveUpdate(): std.Recipe { + * return std.liveUpdateFromGiteaReleases({ + * project, + * matchTag: /^v(?.+)$/, // Strip "v" prefix from tags + * }); + * } + * ``` + */ +export function liveUpdateFromGiteaReleases( + options: LiveUpdateFromGiteaReleasesOptions, +): std.Recipe { + const { repoOwner, repoName } = parseGiteaRepo(options.project.repository); + const matchTag = options.matchTag ?? DEFAULT_LIVE_UPDATE_REGEX_VERSION_MATCH; + const normalizeVersion = options.normalizeVersion ?? false; + + validateMatchTag(matchTag); + + return std.recipe(async () => { + const { nushellRunnable } = await import("nushell"); + + return nushellRunnable( + Brioche.includeFile("./scripts/live_update_from_gitea_releases.nu"), + ).env({ + project: JSON.stringify(options.project), + repoOwner, + repoName, + matchTag: matchTag.source, + normalizeVersion: normalizeVersion.toString(), + }); + }); +} + +/** + * Interface representing the parsed Gitea repository information. + */ +interface GiteaRepoInfo { + readonly repoOwner: string; + readonly repoName: string; +} + +function tryParseGiteaRepo(repo: string): GiteaRepoInfo | null { + const match = repo.match( + /^(?:https?:\/\/)?(www\.)?(?:gitea\.com\/)?(?[\w\.-]+)\/(?[\w\.-]+)\/?$/, + ); + + const { repoOwner, repoName: matchedRepoName } = match?.groups ?? {}; + if (repoOwner == null || matchedRepoName == null) { + return null; + } + + let repoName = matchedRepoName; + if (repoName.endsWith(".git")) { + repoName = repoName.slice(0, -4); + } + + return { repoOwner, repoName }; +} + +/** + * Parse a Gitea repository URL to extract the repository owner and name. + * + * @param repo - The Gitea repository URL to parse. + * + * @returns An object containing the repository owner and name. + * + * @throws If the repository URL cannot be parsed. + */ +function parseGiteaRepo(repo: string): GiteaRepoInfo { + const info = tryParseGiteaRepo(repo); + if (info == null) { + throw new Error( + `Could not parse repo name and owner from ${JSON.stringify(repo)}`, + ); + } + + return info; +} diff --git a/packages/std/extra/live_update/index.bri b/packages/std/extra/live_update/index.bri index fd4c2d7f5..9e83c61e4 100644 --- a/packages/std/extra/live_update/index.bri +++ b/packages/std/extra/live_update/index.bri @@ -1,3 +1,4 @@ +export * from "./from_gitea_releases.bri"; export * from "./from_github_releases.bri"; export * from "./from_github_tags.bri"; export * from "./from_gitlab_releases.bri"; diff --git a/packages/std/extra/live_update/scripts/live_update_from_gitea_releases.nu b/packages/std/extra/live_update/scripts/live_update_from_gitea_releases.nu new file mode 100644 index 000000000..9e450084f --- /dev/null +++ b/packages/std/extra/live_update/scripts/live_update_from_gitea_releases.nu @@ -0,0 +1,57 @@ +# Get project metadata +mut project = $env.project + | from json + +# Retrieve the latest release information from Gitea +let releases = http get $'https://gitea.com/api/v1/repos/($env.repoOwner)/($env.repoName)/releases' + +# Extract the version(s) +let releasesInfo = $releases + | each {|release| + let parsedTag = $release.tag_name + | parse --regex $env.matchTag + + # If no tag is matched, a nil value will be returned + # and this value will be ignored by 'each' + if ($parsedTag | length) != 0 { + { version: $parsedTag.0.version } + } + } + | sort-by --natural version + +if ($releasesInfo | length) == 0 { + error make { msg: $'No tag did match regex ($env.matchTag)' } +} + +let latestReleaseInfo = $releasesInfo + | last + +mut version = $latestReleaseInfo.version + +if $env.normalizeVersion == "true" { + $version = $version + | str replace --all --regex "(-|_)" "." +} + +$project = $project + | update version $version + +if ($project | get extra?.versionDash?) != null { + let $versionDash = $version + | str replace --all "." "-" + + $project = $project + | update extra.versionDash $versionDash +} + +if ($project | get extra?.versionUnderscore?) != null { + let $versionUnderscore = $version + | str replace --all "." "_" + + $project = $project + | update extra.versionUnderscore $versionUnderscore +} + +# Return back the project metadata encoded as JSON +$project + | to json