Files
renovate/lib/modules/platform/forgejo/forgejo-helper.ts
Sebastian Poxhofer 58771bb183 refactor(linters): enabled oxlints func-style (#44691)
* refactor(oxlint): enable func-style

Enable the built-in func-style rule as ["warn", "declaration"] so that
named functions are written as function declarations instead of arrow or
function expressions assigned to variables, matching the convention in
docs/development/best-practices.md (Functions section). Inline arrow
callbacks are unaffected; prefer-arrow-callback continues to cover the
callback direction.

Severity is "warn" instead of "error" as a fallback: a repo-wide run
found 196 pre-existing violations (70 in spec files, 126 in production
code), exceeding the 80-violation fix threshold for this change, so no
violations were fixed here. Follow-up work can convert the offenders
(largest clusters: lib/modules/versioning/ruby, lib/util/cache/package,
lib/modules/versioning/maven, lib/modules/versioning/gradle) and then
raise the rule to "error". Note oxlint 1.72 also supports an
allowTypeAnnotation option if typed consts should remain arrows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(linter): enforce func-style at error

* refactor(func-style): convert const-arrow functions to declarations in with-cache spec and ruby versioning

* refactor(func-style): convert const-arrow functions to declarations in maven/gradle modules

* refactor(func-style): convert const-arrow functions to declarations in remaining versioning modules

* refactor(func-style): convert const-arrow functions to declarations in platform modules

Discriminated-union closures (ensureCommentRemoval byTopic/byContent
helpers in github, gitlab, bitbucket, bitbucket-server) needed the
narrowed field passed explicitly as a parameter, since function
declarations are hoisted and lose the outer if-narrowing that inline
arrow-function closures previously relied on.

* refactor(func-style): convert const-arrow functions to declarations across util/manager/datasource modules

One case (logger/renovate-logger.ts logOnceFn) is kept as an arrow
function with a documented oxlint-disable: it is stored on
this.logger.once[level] and relies on lexically-bound this, which a
hoisted function declaration would not preserve.

* refactor(func-style): convert remaining const-arrow test helpers to declarations

get-updated.spec.ts: hoisted expectPipCompilePackageAndLockFile to
module scope (it only closes over the module-level pipCompile mock)
and added a documented oxlint-disable for vitest/expect-expect on its
caller test, since oxlint's expect-expect check does not trace
assertions through an externally-defined helper function.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:14:22 +00:00

535 lines
12 KiB
TypeScript

import { isBoolean } from '@sindresorhus/is';
import { z } from 'zod/v4';
import { logger } from '../../../logger/index.ts';
import type { BranchStatus } from '../../../types/index.ts';
import { getCache } from '../../../util/cache/repository/index.ts';
import type { ForgejoHttpOptions } from '../../../util/http/forgejo.ts';
import { ForgejoHttp } from '../../../util/http/forgejo.ts';
import { getQueryString } from '../../../util/url.ts';
import {
Branch,
Comment,
CommitStatus,
type CommitStatusType,
Issue,
Label,
PR,
Repo,
RepoContents,
RepoSearchResults,
User,
Version,
} from './schema.ts';
import type {
CombinedCommitStatus,
CommentCreateParams,
CommitStatusCreateParams,
IssueCreateParams,
IssueSearchParams,
IssueUpdateLabelsParams,
IssueUpdateParams,
PRCreateParams,
PRMergeParams,
PRUpdateParams,
PrReviewersParams,
RepoSearchParams,
} from './types.ts';
import { API_PATH } from './utils.ts';
export const forgejoHttp = new ForgejoHttp();
function urlEscape(raw: string): string {
return encodeURIComponent(raw);
}
const commitStatusStates: CommitStatusType[] = [
'unknown',
'success',
'pending',
'warning',
'failure',
'error',
];
export async function getCurrentUser(
options: ForgejoHttpOptions = {},
): Promise<User> {
const url = `${API_PATH}/user`;
const res = await forgejoHttp.getJson(url, options, User);
return res.body;
}
export async function getVersion(
options: ForgejoHttpOptions = {},
): Promise<string> {
const url = `${API_PATH}/version`;
const res = await forgejoHttp.getJson(url, options, Version);
return res.body.version;
}
export async function isOrg(organization: string): Promise<boolean> {
const repoCache = getCache();
repoCache.platform ??= {};
repoCache.platform.forgejo ??= {};
repoCache.platform.forgejo.orgs ??= {};
const cached = repoCache.platform.forgejo.orgs[organization];
if (isBoolean(cached)) {
return cached;
}
try {
const url = `${API_PATH}/orgs/${organization}`;
const res = await forgejoHttp.getJsonUnchecked(url);
repoCache.platform.forgejo.orgs[organization] = res.statusCode === 200;
return res.statusCode === 200;
} catch (err) {
if (err.statusCode === 404) {
return false;
}
// throw other errors
throw err;
}
}
export async function searchRepos(
params: RepoSearchParams,
options?: ForgejoHttpOptions,
): Promise<Repo[]> {
const query = getQueryString(params);
const url = `${API_PATH}/repos/search?${query}`;
const res = await forgejoHttp.getJson(
url,
{
...options,
paginate: true,
},
RepoSearchResults,
);
if (!res.body.ok) {
throw new Error(
'Unable to search for repositories, ok flag has not been set',
);
}
return res.body.data;
}
export async function orgListRepos(
organization: string,
options?: ForgejoHttpOptions,
): Promise<Repo[]> {
const url = `${API_PATH}/orgs/${organization}/repos`;
const res = await forgejoHttp.getJson(
url,
{
...options,
paginate: true,
},
z.array(Repo),
);
return res.body;
}
export async function getRepo(
repoPath: string,
options: ForgejoHttpOptions = {},
): Promise<Repo> {
const url = `${API_PATH}/repos/${repoPath}`;
const res = await forgejoHttp.getJson(url, options, Repo);
return res.body;
}
export async function getRepoContents(
repoPath: string,
filePath: string,
ref?: string | null,
options: ForgejoHttpOptions = {},
): Promise<RepoContents> {
const query = getQueryString(ref ? { ref } : {});
const url = `${API_PATH}/repos/${repoPath}/contents/${urlEscape(
filePath,
)}?${query}`;
const res = await forgejoHttp.getJson(url, options, RepoContents);
return res.body;
}
export async function createPR(
repoPath: string,
params: PRCreateParams,
options?: ForgejoHttpOptions,
): Promise<PR> {
const url = `${API_PATH}/repos/${repoPath}/pulls`;
const res = await forgejoHttp.postJson(
url,
{
...options,
body: params,
},
PR,
);
return res.body;
}
export async function updatePR(
repoPath: string,
idx: number,
params: PRUpdateParams,
options?: ForgejoHttpOptions,
): Promise<PR> {
const url = `${API_PATH}/repos/${repoPath}/pulls/${idx}`;
const res = await forgejoHttp.patchJson(
url,
{
...options,
body: params,
},
PR,
);
return res.body;
}
export async function closePR(
repoPath: string,
idx: number,
options?: ForgejoHttpOptions,
): Promise<void> {
await updatePR(repoPath, idx, {
...options,
state: 'closed',
});
}
export async function mergePR(
repoPath: string,
idx: number,
params: PRMergeParams,
options?: ForgejoHttpOptions,
): Promise<void> {
const url = `${API_PATH}/repos/${repoPath}/pulls/${idx}/merge`;
await forgejoHttp.postJson(url, {
...options,
body: params,
});
}
export async function getPR(
repoPath: string,
idx: number,
options: ForgejoHttpOptions = {},
): Promise<PR> {
const url = `${API_PATH}/repos/${repoPath}/pulls/${idx}`;
const res = await forgejoHttp.getJson(url, options, PR);
return res.body;
}
export async function getPRByBranch(
repoPath: string,
base: string,
head: string,
options: ForgejoHttpOptions = {},
): Promise<PR | null> {
const url = `${API_PATH}/repos/${repoPath}/pulls/${base}/${head}`;
try {
const res = await forgejoHttp.getJson(url, options, PR);
return res.body;
} catch (err) {
logger.trace({ err }, 'Error while fetching PR');
if (err.statusCode !== 404) {
logger.debug({ err }, 'Error while fetching PR');
}
return null;
}
}
export async function requestPrReviewers(
repoPath: string,
idx: number,
params: PrReviewersParams,
options?: ForgejoHttpOptions,
): Promise<void> {
const url = `${API_PATH}/repos/${repoPath}/pulls/${idx}/requested_reviewers`;
await forgejoHttp.postJson(url, {
...options,
body: params,
});
}
export async function createIssue(
repoPath: string,
params: IssueCreateParams,
options?: ForgejoHttpOptions,
): Promise<Issue> {
const url = `${API_PATH}/repos/${repoPath}/issues`;
const res = await forgejoHttp.postJson(
url,
{
...options,
body: params,
},
Issue,
);
return res.body;
}
export async function updateIssue(
repoPath: string,
idx: number,
params: IssueUpdateParams,
options?: ForgejoHttpOptions,
): Promise<Issue> {
const url = `${API_PATH}/repos/${repoPath}/issues/${idx}`;
const res = await forgejoHttp.patchJson(
url,
{
...options,
body: params,
},
Issue,
);
return res.body;
}
export async function updateIssueLabels(
repoPath: string,
idx: number,
params: IssueUpdateLabelsParams,
options?: ForgejoHttpOptions,
): Promise<Label[]> {
const url = `${API_PATH}/repos/${repoPath}/issues/${idx}/labels`;
const res = await forgejoHttp.putJson(
url,
{
...options,
body: params,
},
z.array(Label),
);
return res.body;
}
export async function closeIssue(
repoPath: string,
idx: number,
options?: ForgejoHttpOptions,
): Promise<void> {
await updateIssue(repoPath, idx, {
...options,
state: 'closed',
});
}
export async function searchIssues(
repoPath: string,
params: IssueSearchParams,
options?: ForgejoHttpOptions,
): Promise<Issue[]> {
const query = getQueryString({ ...params, type: 'issues' });
const url = `${API_PATH}/repos/${repoPath}/issues?${query}`;
const res = await forgejoHttp.getJson(
url,
{
...options,
paginate: true,
},
z.array(Issue),
);
return res.body;
}
export async function getIssue(
repoPath: string,
idx: number,
options: ForgejoHttpOptions = {},
): Promise<Issue> {
const url = `${API_PATH}/repos/${repoPath}/issues/${idx}`;
const res = await forgejoHttp.getJson(url, options, Issue);
return res.body;
}
export async function getRepoLabels(
repoPath: string,
options: ForgejoHttpOptions = {},
): Promise<Label[]> {
const url = `${API_PATH}/repos/${repoPath}/labels`;
const res = await forgejoHttp.getJson(url, options, z.array(Label));
return res.body;
}
export async function getOrgLabels(
orgName: string,
options: ForgejoHttpOptions = {},
): Promise<Label[]> {
const url = `${API_PATH}/orgs/${orgName}/labels`;
const res = await forgejoHttp.getJson(url, options, z.array(Label));
return res.body;
}
export async function unassignLabel(
repoPath: string,
issue: number,
label: number,
options?: ForgejoHttpOptions,
): Promise<void> {
const url = `${API_PATH}/repos/${repoPath}/issues/${issue}/labels/${label}`;
await forgejoHttp.deleteJson(url, options);
}
export async function createComment(
repoPath: string,
issue: number,
body: string,
options?: ForgejoHttpOptions,
): Promise<Comment> {
const params: CommentCreateParams = { body };
const url = `${API_PATH}/repos/${repoPath}/issues/${issue}/comments`;
const res = await forgejoHttp.postJson(
url,
{
...options,
body: params,
},
Comment,
);
return res.body;
}
export async function updateComment(
repoPath: string,
idx: number,
body: string,
options?: ForgejoHttpOptions,
): Promise<Comment> {
const params: CommentCreateParams = { body };
const url = `${API_PATH}/repos/${repoPath}/issues/comments/${idx}`;
const res = await forgejoHttp.patchJson(
url,
{
...options,
body: params,
},
Comment,
);
return res.body;
}
export async function deleteComment(
repoPath: string,
idx: number,
options?: ForgejoHttpOptions,
): Promise<void> {
const url = `${API_PATH}/repos/${repoPath}/issues/comments/${idx}`;
await forgejoHttp.deleteJson(url, options);
}
export async function getComments(
repoPath: string,
issue: number,
options: ForgejoHttpOptions = {},
): Promise<Comment[]> {
const url = `${API_PATH}/repos/${repoPath}/issues/${issue}/comments`;
const res = await forgejoHttp.getJson(url, options, z.array(Comment));
return res.body;
}
export async function createCommitStatus(
repoPath: string,
branchCommit: string,
params: CommitStatusCreateParams,
options?: ForgejoHttpOptions,
): Promise<CommitStatus> {
const url = `${API_PATH}/repos/${repoPath}/statuses/${branchCommit}`;
const res = await forgejoHttp.postJson(
url,
{
...options,
body: params,
},
CommitStatus,
);
return res.body;
}
export const forgejoToRenovateStatusMapping: Record<
CommitStatusType,
BranchStatus
> = {
unknown: 'yellow',
success: 'green',
pending: 'yellow',
warning: 'red',
failure: 'red',
error: 'red',
};
export const renovateToForgejoStatusMapping: Record<
BranchStatus,
CommitStatusType
> = {
green: 'success',
yellow: 'pending',
red: 'failure',
};
function filterStatus(data: CommitStatus[]): CommitStatus[] {
const ret: Record<string, CommitStatus> = {};
for (const i of data) {
if (!ret[i.context] || ret[i.context].id < i.id) {
ret[i.context] = i;
}
}
return Object.values(ret);
}
export async function getCombinedCommitStatus(
repoPath: string,
branchName: string,
options?: ForgejoHttpOptions,
): Promise<CombinedCommitStatus> {
const url = `${API_PATH}/repos/${repoPath}/commits/${urlEscape(
branchName,
)}/statuses`;
const res = await forgejoHttp.getJson(
url,
{
...options,
paginate: true,
},
z.array(CommitStatus),
);
let worstState = 0;
const statuses = filterStatus(res.body);
for (const cs of statuses) {
worstState = Math.max(worstState, commitStatusStates.indexOf(cs.status));
}
return {
worstStatus: commitStatusStates[worstState],
statuses,
};
}
export async function getBranch(
repoPath: string,
branchName: string,
options: ForgejoHttpOptions = {},
): Promise<Branch> {
const url = `${API_PATH}/repos/${repoPath}/branches/${urlEscape(branchName)}`;
const res = await forgejoHttp.getJson(url, options, Branch);
return res.body;
}