Skip to content

Commit

Permalink
refactor(env): load env values directly in config verification to gue…
Browse files Browse the repository at this point in the history
…ss platform and allow some overrides via config
  • Loading branch information
kilianpaquier committed May 4, 2024
1 parent 8a0b16a commit a245976
Show file tree
Hide file tree
Showing 14 changed files with 648 additions and 458 deletions.
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/download-artifact@v4
with:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ A [semantic-release](https://github.com/semantic-release/semantic-release) plugi

| Step | Description |
| ------------------ | --------------------------------------------------------------------------------------------- |
| `verifyConditions` | verify the presence of specific plugin configuration alongside required environment variables |
| `success` | apply backmerge for the appropriate target branches |
| `verifyConditions` | Verify the presence of specific plugin configuration alongside required environment variables |
| `success` | Apply backmerge for the appropriate target branches |

- [How to ?](#how-to-)
- [Usage](#usage)
Expand Down
Binary file modified bun.lockb
Binary file not shown.
41 changes: 14 additions & 27 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,31 @@ import { type SuccessContext, type VerifyConditionsContext } from 'semantic-rele

import AggregateError from "aggregate-error"

import { type BackmergeConfig } from "./lib/models/config"
import { type BackmergeConfig, type RepositoryInfo } from "./lib/models/config"
import { backmerge } from "./lib/backmerge"
import { repositoryInfo } from "./lib/repository-info"
import { verifyConfig } from "./lib/verify-config"
import { ensureDefault, verifyConfig } from "./lib/verify-config"

// eslint-disable-next-line @typescript-eslint/require-await
export const verifyConditions = async (partialConfig: Partial<BackmergeConfig>, context: VerifyConditionsContext) => {
const verifyResult = verifyConfig(partialConfig)
if (verifyResult.errors.length > 0) {
throw new AggregateError(verifyResult.errors)
export const verifyConditions = async (partialConfig: Partial<BackmergeConfig>, context: VerifyConditionsContext): Promise<[BackmergeConfig, RepositoryInfo]> => {
const config = ensureDefault(partialConfig)

const configErrors = verifyConfig(config)
if (configErrors.length > 0) {
throw new AggregateError(configErrors)
}

const repositoryResult = repositoryInfo(context, verifyResult.config)
if (repositoryResult.errors.length > 0) {
throw new AggregateError(repositoryResult.errors)
}

if (verifyResult.config.debug) {
context.logger.log(`Plugin configuration is ${JSON.stringify(verifyResult.config)}.`)
const [info, infoErrors] = repositoryInfo(config, context.env)
if (infoErrors.length > 0) {
throw new AggregateError(infoErrors)
}
return [config, info!]
}

export const success = async (partialConfig: Partial<BackmergeConfig>, context: SuccessContext) => {
const verifyResult = verifyConfig(partialConfig)
if (verifyResult.errors.length > 0) {
throw new AggregateError(verifyResult.errors)
}

const repositoryResult = repositoryInfo(context, verifyResult.config)
if (repositoryResult.errors.length > 0) {
throw new AggregateError(repositoryResult.errors)
}

if (verifyResult.config.debug) {
context.logger.log(`Plugin configuration is ${JSON.stringify(verifyResult.config)}.`)
}
const [config, info] = await verifyConditions(partialConfig, context)

const errors = await backmerge(context, verifyResult.config, repositoryResult.info!)
const errors = await backmerge(context, config, info)
if (errors.length > 0) {
throw new AggregateError(errors)
}
Expand Down
125 changes: 42 additions & 83 deletions lib/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,96 +9,55 @@ interface PartialError {
details?: string
}

const configErrors: { [k in keyof BackmergeConfig]: PartialError } = {
const configErrors: { [k in keyof BackmergeConfig]: (value?: any) => PartialError } = {
apiPathPrefix: (value: any) => ({
details: `[API Path Prefix](${linkify("shared-configuration")}) must be a string. Provided value is ${JSON.stringify(value)}.`,
message: `Invalid 'apiPathPrefix' configuration.`,
}),
baseUrl: (value: any) => ({
details: `[Base URL](${linkify("shared-configuration")}) must be a non empty string. Provided value is ${JSON.stringify(value)}.`,
message: `Invalid 'baseUrl' configuration.`,
}),
// shouldn't happen since it comes from semantic-release config
ci: {
message: "Invalid `ci` configuration (coming from semantic-release options).",
},
commit: {
details: `[Commit](${linkify("shared-configuration")}) must be a string.`,
message: "Invalid `commit` configuration.",
},
ci: () => ({
message: "Invalid 'ci' configuration (coming from semantic-release options).",
}),
commit: (value: any) => ({
details: `[Commit](${linkify("shared-configuration")}) must be a string. Provided value is ${JSON.stringify(value)}.`,
message: `Invalid 'commit' configuration.`,
}),
// shouldn't happen since it comes from semantic-release config
debug: {
message: "Invalid `debug` configuration (coming from semantic-release options).",
},
debug: () => ({
message: "Invalid 'debug' configuration (coming from semantic-release options).",
}),
// shouldn't happen since it comes from semantic-release config
dryRun: {
message: "Invalid `dryRun` configuration (coming from semantic-release options).",
},
platform: {
details: `[Platform](${linkify("shared-configuration")}) must be one of 'github', 'gitlab'.`,
message: "Invalid `platform` configuration.",
},
dryRun: () => ({
message: "Invalid 'dryRun' configuration (coming from semantic-release options).",
}),
platform: (value: any) => ({
details: `[Platform](${linkify("shared-configuration")}) must be one of 'bitbucket', 'bitbucket-cloud', 'gitea', 'github', 'gitlab'. Provided value is ${JSON.stringify(value)}.`,
message: `Invalid 'platform' configuration.`,
}),
// shouldn't happen since it comes from semantic-release config
repositoryUrl: {
message: "Invalid `repositoryUrl` configuration (coming from semantic-release options).",
},
targets: {
details: `[Targets](${linkify("shared-configuration")}) must be a valid array of targets ({ from: "...", to: "..." }).`,
message: "Invalid `targets` configuration.",
},
title: {
details: `[Title](${linkify("shared-configuration")}) must be a string.`,
message: "Invalid `title` configuration.",
},
repositoryUrl: () => ({
message: "Invalid 'repositoryUrl' configuration (coming from semantic-release options).",
}),
targets: (value: any) => ({
details: `[Targets](${linkify("shared-configuration")}) must be a valid array of targets ({ from: "...", to: "..." }). Provided value is ${JSON.stringify(value)}.`,
message: `Invalid 'targets' configuration.`,
}),
title: (value: any) => ({
details: `[Title](${linkify("shared-configuration")}) must be a non empty string. Provided value is ${JSON.stringify(value)}.`,
message: `Invalid 'title' configuration.`,
}),
token: () => ({
details: `[Token](${linkify("shared-configuration")}) must be a non empty string.`,
message: "Invalid 'token' configuration.",
})
}

export const getConfigError = (option: keyof BackmergeConfig, value?: any): SemanticReleaseError => {
const code = `EINVALID${option.toUpperCase()}`

const error = configErrors[option]
if (error.details && value) {
error.details += `Provided value is '${JSON.stringify(value)}'.`
}
const error = configErrors[option](value)
return new SemanticReleaseError(error.message, code, error.details)
}

export interface Envs {
BITBUCKET_API_URL: string,
BITBUCKET_TOKEN: string,
GITHUB_API_URL: string,
GITHUB_TOKEN: string,
GITLAB_API_URL: string,
GITLAB_TOKEN: string,
GITLAB_URL: string,
}

const envErrors: { [k in keyof Envs]: PartialError } = {
BITBUCKET_API_URL: {
details: `[Bitbucket](${linkify("bitbucket")}) section must be followed when backmerge configured on 'bitbucket'.`,
message: "Missing `BITBUCKET_API_URL` environment variable.",
},
BITBUCKET_TOKEN: {
details: `[Bitbucket](${linkify("bitbucket")}) section must be followed when backmerge configured on 'bitbucket'.`,
message: "Missing `BITBUCKET_TOKEN` environment variable.",
},

GITHUB_API_URL: {
details: `[github](${linkify("github")}) section must be followed when backmerge configured on 'github'.`,
message: "Missing `GITHUB_API_URL` environment variable.",
},
GITHUB_TOKEN: {
details: `[Github](${linkify("github")}) section must be followed when backmerge configured on 'github'.`,
message: "Missing `GITHUB_TOKEN` or `GH_TOKEN` environment variable.",
},

GITLAB_API_URL: {
details: `[Gitlab](${linkify("gitlab")}) section must be followed when backmerge configured on 'gitlab'.`,
message: "Missing `GITLAB_API_URL` or `CI_API_V4_URL` environment variable.",
},
GITLAB_TOKEN: {
details: `[Gitlab](${linkify("gitlab")}) section must be followed when backmerge configured on 'gitlab'.`,
message: "Missing `GITLAB_TOKEN` or `GL_TOKEN` environment variable.",
},
GITLAB_URL: {
details: `[Gitlab](${linkify("gitlab")}) section must be followed when backmerge configured on 'gitlab'.`,
message: "Missing `GITLAB_URL` or `CI_SERVER_URL` environment variable.",
},
}

export const getEnvError = (envName: keyof Envs): SemanticReleaseError => {
const code = `EINVALID${envName.replaceAll("_", "").toUpperCase()}`
const { message, details } = envErrors[envName]
return new SemanticReleaseError(message, code, details)
}
67 changes: 56 additions & 11 deletions lib/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,50 @@ export const branchesByGlob = async (context: Partial<VerifyConditionsContext>,
}
}


export const createPullRequest = async (config: BackmergeConfig, info: RepositoryInfo, from: string, to: string): Promise<SemanticReleaseError | void> => {
const apiUrl = config.baseUrl + config.apiPathPrefix
switch (config.platform) {
case Platform.BITBUCKET:
case Platform.BITBUCKET_CLOUD:
try {
await fetch(`${info.apiUrl}/2.0/repositories/${info.owner}/${info.repo}/pullrequests`, {
await fetch(`${apiUrl}/repositories/${info.owner}/${info.repo}/pullrequests`, {
body: JSON.stringify({
destination: { branch: { name: to } },
source: { branch: { name: from } },
title: config.title,
}),
headers: { Authorization: `Bearer ${info.token}` },
headers: { Authorization: `Bearer ${config.token}` },
method: "POST",
})
} catch (error) {
return new SemanticReleaseError(`Failed to create pull request from '${remote}/${from}' to '${remote}/${to}'`, "EPULLREQUEST", String(error))
}
break
case Platform.BITBUCKET:
try {
await fetch(`${apiUrl}/projects/${info.owner}/repos/${info.repo}/pull-requests`, {
body: JSON.stringify({
fromRef: { id: `refs/heads/${from}` },
open: true,
state: "OPEN",
title: config.title,
toRef: { id: `refs/heads/${to}` },
}),
headers: { Authorization: `Bearer ${config.token}` },
method: "POST",
})
} catch (error) {
return new SemanticReleaseError(`Failed to create pull request from '${remote}/${from}' to '${remote}/${to}'`, "EPULLREQUEST", String(error))
}
break
case Platform.GITEA:
try {
await fetch(`${apiUrl}/repos/${info.owner}/${info.repo}/pulls`, {
body: JSON.stringify({
base: to,
head: from,
title: config.title,
}),
headers: { Authorization: `Bearer ${config.token}` },
method: "POST",
})
} catch (error) {
Expand All @@ -51,10 +83,10 @@ export const createPullRequest = async (config: BackmergeConfig, info: Repositor
break
case Platform.GITHUB:
try {
await new Octokit({ auth: info.token, request: { fetch } }).
await new Octokit({ auth: config.token, request: { fetch } }).
request("POST /repos/{owner}/{repo}/pulls", {
base: to,
baseUrl: info.apiUrl,
baseUrl: apiUrl,
head: from,
owner: info.owner!,
repo: info.repo!,
Expand All @@ -66,13 +98,13 @@ export const createPullRequest = async (config: BackmergeConfig, info: Repositor
break
case Platform.GITLAB:
try {
await fetch(`${info.apiUrl}/projects/${encodeURIComponent(info.repo!)}/merge_requests`, {
await fetch(`${apiUrl}/projects/${encodeURIComponent(info.repo!)}/merge_requests`, {
body: JSON.stringify({
source_branch: from,
target_branch: to,
title: config.title,
}),
headers: { Authorization: `Bearer ${info.token}` },
headers: { Authorization: `Bearer ${config.token}` },
method: "POST",
})
} catch (error) {
Expand Down Expand Up @@ -101,9 +133,9 @@ export const mergeBranch = async (context: Partial<VerifyConditionsContext>, con
context.logger?.log("Merge conflicts detected! Creating a pull request.")

try {
await git(["merge", "--abort"])
await git(["merge", "--abort"], options)
} catch (error) {
return new SemanticReleaseError("Failed to abort merge before creating pull request", "EMERGEABORT", String(error))
return new SemanticReleaseError("Failed to abort merge before creating pull request.", "EMERGEABORT", String(error))
}

if (config.dryRun) {
Expand All @@ -122,7 +154,20 @@ export const mergeBranch = async (context: Partial<VerifyConditionsContext>, con
try {
await git(push, options)
} catch (error) {
return new SemanticReleaseError(`Failed to push branch ${to} to '${remote}/${to}'.`, "EPUSH", String(error))
context.logger?.log(`Failed to backmerge '${from}' to '${to}' with a push, opening pull request.`)

try {
// reset hard in case a merge commit had been done just previous steps
await git(["reset", "--hard", `${remote}/${from}`], options)
} catch (error) {
return new SemanticReleaseError(`Failed to reset branch ${from} to ${remote} state before opening pull request.`, "ERESETHARD", String(error))
}

if (config.dryRun) {
context.logger?.log(`Running with --dry-run, created pull request would have been from '${from}' to '${to}' with title '${commit}'.`)
return Promise.resolve()
}
return await createPullRequest(config, info, from, to)
}
return Promise.resolve()
}
12 changes: 8 additions & 4 deletions lib/models/config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// eslint-disable-next-line no-shadow
export enum Platform {
BITBUCKET = "bitbucket",
BITBUCKET_CLOUD = "bitbucket-cloud",
GITEA = "gitea",
GITHUB = "github",
GITLAB = "gitlab",
NULL = "",
}

export interface Target {
Expand All @@ -11,6 +14,8 @@ export interface Target {
}

export interface BackmergeConfig {
apiPathPrefix: string
baseUrl: string
ci: boolean // comes from semantic-release config
commit: string
debug: boolean // comes from semantic-release config
Expand All @@ -19,13 +24,12 @@ export interface BackmergeConfig {
repositoryUrl: string // comes from semantic-release config
targets: Target[]
title: string
token: string
}

export interface RepositoryInfo {
apiUrl?: string
owner?: string
repo?: string
token?: string
owner: string
repo: string
}

export const defaultTitle = "Automatic merge failure"
Expand Down
Loading

0 comments on commit a245976

Please sign in to comment.