mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-30 01:08:06 +00:00
e0cec3dc95
feat(lint): add `types-location` rule and enable it for `lib/modules`
Reviewers consistently ask for exported types to be moved into a
colocated `types.ts`. Add a `renovate/types-location` oxlint rule that
reports `export interface` / `export type` declarations outside such a
file, and turn it on across `lib/modules`.
Re-exports (`export type { X }`, `export type *`), file-local types, and
values (enums, classes, functions) are left alone. `schema.ts` files and
`schema/` directories are excluded via config, matching how
`zod-schema-location` is configured — inferred types belong next to the
schema they are derived from.
Relocating the 87 existing violations means:
- new `types.ts` files for the modules that had none
- `terragrunt/common.ts` folded into its `types.ts`, as it held nothing
else
- `RangeBound` in `versioning/gradle/compare.ts` un-exported, since only
a file-local interface referenced it
- `bazel-module/parser/fragments.ts` split from its inferred types, which
now live in `parser/types.ts` and infer through a namespace import so
each schema keeps its merged name
196 lines
5.4 KiB
TypeScript
196 lines
5.4 KiB
TypeScript
import { logger } from '../../../logger/index.ts';
|
|
import { BitbucketHttp } from '../../../util/http/bitbucket.ts';
|
|
import type {
|
|
EnsureCommentConfig,
|
|
EnsureCommentRemovalConfig,
|
|
} from '../types.ts';
|
|
import type { Account, CommentsConfig, PagedResult } from './types.ts';
|
|
|
|
export const REOPEN_PR_COMMENT_KEYWORD = 'reopen!';
|
|
|
|
const bitbucketHttp = new BitbucketHttp();
|
|
|
|
interface Comment {
|
|
content: { raw: string };
|
|
id: number;
|
|
user: Account;
|
|
}
|
|
|
|
interface EnsureBitbucketCommentConfig extends EnsureCommentConfig {
|
|
config: CommentsConfig;
|
|
}
|
|
|
|
async function getComments(
|
|
config: CommentsConfig,
|
|
prNo: number,
|
|
): Promise<Comment[]> {
|
|
const comments = (
|
|
await bitbucketHttp.getJsonUnchecked<PagedResult<Comment>>(
|
|
`/2.0/repositories/${config.repository}/pullrequests/${prNo}/comments`,
|
|
{
|
|
paginate: true,
|
|
},
|
|
)
|
|
).body.values;
|
|
|
|
logger.debug(`Found ${comments.length} comments`);
|
|
return comments;
|
|
}
|
|
|
|
async function addComment(
|
|
config: CommentsConfig,
|
|
prNo: number,
|
|
raw: string,
|
|
): Promise<void> {
|
|
await bitbucketHttp.postJson(
|
|
`/2.0/repositories/${config.repository}/pullrequests/${prNo}/comments`,
|
|
{
|
|
body: { content: { raw } },
|
|
},
|
|
);
|
|
}
|
|
|
|
async function editComment(
|
|
config: CommentsConfig,
|
|
prNo: number,
|
|
commentId: number,
|
|
raw: string,
|
|
): Promise<void> {
|
|
await bitbucketHttp.putJson(
|
|
`/2.0/repositories/${config.repository}/pullrequests/${prNo}/comments/${commentId}`,
|
|
{
|
|
body: { content: { raw } },
|
|
},
|
|
);
|
|
}
|
|
|
|
async function deleteComment(
|
|
config: CommentsConfig,
|
|
prNo: number,
|
|
commentId: number,
|
|
): Promise<void> {
|
|
await bitbucketHttp.deleteJson(
|
|
`/2.0/repositories/${config.repository}/pullrequests/${prNo}/comments/${commentId}`,
|
|
);
|
|
}
|
|
|
|
export async function ensureComment({
|
|
config,
|
|
number: prNo,
|
|
topic,
|
|
content,
|
|
}: EnsureBitbucketCommentConfig): Promise<boolean> {
|
|
try {
|
|
const comments = await getComments(config, prNo);
|
|
let body: string;
|
|
let commentId: number | undefined;
|
|
let commentNeedsUpdating: boolean | undefined;
|
|
if (topic) {
|
|
logger.debug(`Ensuring comment "${topic}" in #${prNo}`);
|
|
body = `### ${topic}\n\n${content}`;
|
|
comments.forEach((comment) => {
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (comment.content.raw.startsWith(`### ${topic}\n\n`)) {
|
|
commentId = comment.id;
|
|
commentNeedsUpdating = comment.content.raw !== body;
|
|
}
|
|
});
|
|
} else {
|
|
logger.debug(`Ensuring content-only comment in #${prNo}`);
|
|
body = `${content}`;
|
|
comments.forEach((comment) => {
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (comment.content.raw === body) {
|
|
commentId = comment.id;
|
|
commentNeedsUpdating = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// sanitize any language that isn't supported by Bitbucket Cloud
|
|
body = sanitizeCommentBody(body);
|
|
|
|
if (!commentId) {
|
|
await addComment(config, prNo, body);
|
|
logger.info(
|
|
{ repository: config.repository, prNo, topic },
|
|
'Comment added',
|
|
);
|
|
} else if (commentNeedsUpdating) {
|
|
await editComment(config, prNo, commentId, body);
|
|
logger.debug({ repository: config.repository, prNo }, 'Comment updated');
|
|
} else {
|
|
logger.debug('Comment is already up-to-date');
|
|
}
|
|
return true;
|
|
} catch (err) /* v8 ignore next -- defensive: comment API failures are logged and swallowed, not simulated in specs */ {
|
|
logger.warn({ err }, 'Error ensuring comment');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function reopenComments(
|
|
config: CommentsConfig,
|
|
prNo: number,
|
|
): Promise<Comment[]> {
|
|
const comments = await getComments(config, prNo);
|
|
|
|
const reopenComments = comments.filter((comment) =>
|
|
comment.content.raw.startsWith(REOPEN_PR_COMMENT_KEYWORD),
|
|
);
|
|
|
|
return reopenComments;
|
|
}
|
|
|
|
function byTopic(comment: Comment, topic: string): boolean {
|
|
return comment.content.raw.startsWith(`### ${topic}\n\n`);
|
|
}
|
|
|
|
function byContent(comment: Comment, content: string): boolean {
|
|
return comment.content.raw.trim() === content;
|
|
}
|
|
|
|
export async function ensureCommentRemoval(
|
|
config: CommentsConfig,
|
|
deleteConfig: EnsureCommentRemovalConfig,
|
|
): Promise<void> {
|
|
try {
|
|
const { number: prNo } = deleteConfig;
|
|
const key =
|
|
deleteConfig.type === 'by-topic'
|
|
? deleteConfig.topic
|
|
: deleteConfig.content;
|
|
logger.debug(`Ensuring comment "${key}" in #${prNo} is removed`);
|
|
const comments = await getComments(config, prNo);
|
|
|
|
let commentId: number | undefined = undefined;
|
|
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (deleteConfig.type === 'by-topic') {
|
|
const topic = deleteConfig.topic;
|
|
commentId = comments.find((comment) => byTopic(comment, topic))?.id;
|
|
} else if (deleteConfig.type === 'by-content') {
|
|
const content = deleteConfig.content;
|
|
commentId = comments.find((comment) => byContent(comment, content))?.id;
|
|
}
|
|
|
|
if (commentId) {
|
|
await deleteComment(config, prNo, commentId);
|
|
}
|
|
} catch (err) /* v8 ignore next -- defensive: comment API failures are logged and swallowed, not simulated in specs */ {
|
|
logger.warn({ err }, 'Error ensuring comment removal');
|
|
}
|
|
}
|
|
|
|
function sanitizeCommentBody(body: string): string {
|
|
return body
|
|
.replace(
|
|
'checking the rebase/retry box above',
|
|
'renaming this PR to start with "rebase!"',
|
|
)
|
|
.replace(
|
|
'rename this PR to get a fresh replacement',
|
|
'add a comment starting with "reopen!" to get a fresh replacement',
|
|
);
|
|
}
|