chore: resolve no-explicit-any lint warnings in plugins (#12237)

* chore: resolve no-explicit-any lint warnings in plugins

Replaces uses of `any` in the plugins directory with concrete types,
`unknown`, or structured type assertions, addressing the remaining
typescript-eslint(no-explicit-any) warnings flagged by oxlint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: address review feedback in GitLabIssueProvider

Drop trailing semicolon from log string and add early return in
`destroyNamespace` when neither `user_id` nor `full_path` is present
to avoid an unnecessary full-scan transaction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-05-01 08:29:58 -04:00
committed by GitHub
parent e2c28f4b9f
commit 1f097b0fdd
21 changed files with 202 additions and 98 deletions
+72 -25
View File
@@ -5,7 +5,26 @@ import { Integration, IntegrationAuthentication } from "@server/models";
import { BaseIssueProvider } from "@server/utils/BaseIssueProvider";
import { GitLab } from "./gitlab";
import { sequelize } from "@server/storage/database";
import { Op } from "sequelize";
import { Op, type WhereOptions } from "sequelize";
interface GitLabWebhookPayload {
event_name?: string;
old_full_path?: string;
old_username?: string;
full_path?: string;
username?: string;
group_id?: string;
user_id?: string;
project_id?: string | number;
project_namespace_id?: string | number;
name?: string;
path_with_namespace?: string;
changes?: { before: string }[];
project?: {
name: string;
path_with_namespace: string;
};
}
export class GitLabIssueProvider extends BaseIssueProvider {
constructor() {
@@ -64,7 +83,8 @@ export class GitLabIssueProvider extends BaseIssueProvider {
headers: Record<string, unknown>;
}) {
const hookId = headers["x-gitlab-webhook-uuid"] as string;
const eventName = payload.event_name as string;
const typedPayload = payload as GitLabWebhookPayload;
const eventName = typedPayload.event_name;
if (!eventName) {
Logger.warn(
@@ -77,28 +97,28 @@ export class GitLabIssueProvider extends BaseIssueProvider {
case "project_update":
case "project_transfer":
case "project_rename":
await this.updateProject(payload);
await this.updateProject(typedPayload);
break;
case "repository_update":
await this.createProject(payload);
await this.createProject(typedPayload);
break;
case "project_destroy":
await this.destroyProject(payload);
await this.destroyProject(typedPayload);
break;
case "group_rename":
case "user_rename":
await this.updateNamespace(payload);
await this.updateNamespace(typedPayload);
break;
case "user_destroy":
case "group_destroy":
await this.destroyNamespace(payload);
await this.destroyNamespace(typedPayload);
break;
default:
break;
}
}
private async updateNamespace(payload: Record<string, any>) {
private async updateNamespace(payload: GitLabWebhookPayload) {
const name = payload.old_full_path ?? payload.old_username;
const where = {
service: IntegrationService.GitLab,
@@ -119,6 +139,12 @@ export class GitLabIssueProvider extends BaseIssueProvider {
return;
}
const newName = payload.full_path ?? payload.username;
if (!newName) {
Logger.warn(`GitLab namespace_update event without new name`);
return;
}
const sources = integration.issueSources ?? [];
const updatedSources = sources.map((source) => {
if (source.owner.name === name) {
@@ -126,7 +152,7 @@ export class GitLabIssueProvider extends BaseIssueProvider {
...source,
owner: {
id: payload.group_id || source.owner.id,
name: payload.full_path ?? payload.username,
name: newName,
},
};
}
@@ -139,19 +165,29 @@ export class GitLabIssueProvider extends BaseIssueProvider {
});
}
private async destroyNamespace(payload: Record<string, any>) {
private async destroyNamespace(payload: GitLabWebhookPayload) {
if (!payload.user_id && !payload.full_path) {
Logger.warn(
`GitLab namespace_destroy event without user_id or full_path`
);
return;
}
let replacements = {};
const whereCondition: any = {
const whereCondition: WhereOptions = {
service: IntegrationService.GitLab,
...(payload.user_id && {
"settings.gitlab.installation.account.id": payload.user_id,
}),
...(!payload.user_id &&
payload.full_path && {
[Op.and]: sequelize.literal(
`"issueSources"::jsonb @> :jsonCondition`
),
}),
};
if (payload.user_id) {
whereCondition["settings.gitlab.installation.account.id"] =
payload.user_id;
} else if (payload.full_path) {
whereCondition[Op.and] = sequelize.literal(
`"issueSources"::jsonb @> :jsonCondition`
);
if (!payload.user_id && payload.full_path) {
replacements = {
jsonCondition: JSON.stringify([{ owner: { name: payload.full_path } }]),
};
@@ -187,7 +223,7 @@ export class GitLabIssueProvider extends BaseIssueProvider {
});
}
private async destroyProject(payload: Record<string, any>) {
private async destroyProject(payload: GitLabWebhookPayload) {
await sequelize.transaction(async (transaction) => {
const integrations = await Integration.findAll({
where: {
@@ -223,14 +259,20 @@ export class GitLabIssueProvider extends BaseIssueProvider {
});
}
private async createProject(payload: Record<string, any>) {
const createEvent = payload.changes.some((p: { before: string }) =>
private async createProject(payload: GitLabWebhookPayload) {
const createEvent = payload.changes?.some((p: { before: string }) =>
/^0{40}$/.test(p.before)
);
if (!createEvent) {
return;
}
const project = payload.project;
if (!project || !payload.project_id) {
return;
}
await sequelize.transaction(async (transaction) => {
const integration = (await Integration.findOne({
where: {
@@ -245,7 +287,6 @@ export class GitLabIssueProvider extends BaseIssueProvider {
return;
}
const project = payload.project;
const owner = {
id: "", // namespace.id is not provided in this webhook payload
name: project.path_with_namespace.split("/").slice(0, -1).join("/"),
@@ -264,7 +305,13 @@ export class GitLabIssueProvider extends BaseIssueProvider {
});
}
private async updateProject(payload: Record<string, any>) {
private async updateProject(payload: GitLabWebhookPayload) {
if (!payload.name || !payload.path_with_namespace) {
return;
}
const newName = payload.name;
const pathWithNamespace = payload.path_with_namespace;
await sequelize.transaction(async (transaction) => {
const integrations = await Integration.findAll({
where: {
@@ -293,8 +340,8 @@ export class GitLabIssueProvider extends BaseIssueProvider {
);
if (source) {
source.name = payload.name;
source.owner.name = payload.path_with_namespace
source.name = newName;
source.owner.name = pathWithNamespace
.split("/")
.slice(0, -1)
.join("/");
+8 -1
View File
@@ -46,7 +46,14 @@ class Iframely {
return { error: data.error } as UnfurlError; // In addition to our custom UnfurlError, sometimes iframely returns error in the response body.
}
const parsedData = data as Record<string, any>;
const parsedData = data as {
url: string;
meta: { title: string; description: string; site: string };
links: {
thumbnail?: { href: string }[];
icon?: { href: string }[];
};
};
return {
type: UnfurlResourceType.URL,
+8 -2
View File
@@ -1,7 +1,13 @@
import type { Request } from "express";
import { HttpsProxyAgent } from "https-proxy-agent";
import type OAuth2Strategy from "passport-oauth2";
import { Strategy } from "passport-oauth2";
interface AuthenticateOptions {
originalQuery?: Request["query"];
[key: string]: unknown;
}
export class OIDCStrategy extends Strategy {
constructor(
options: OAuth2Strategy.StrategyOptionsWithRequest,
@@ -15,12 +21,12 @@ export class OIDCStrategy extends Strategy {
}
}
authenticate(req: any, options: any) {
authenticate(req: Request, options: AuthenticateOptions) {
options.originalQuery = req.query;
super.authenticate(req, options);
}
authorizationParams(options: any) {
authorizationParams(options: AuthenticateOptions) {
return {
...options.originalQuery,
...super.authorizationParams?.(options),
+8 -3
View File
@@ -1,4 +1,5 @@
import { startRegistration } from "@simplewebauthn/browser";
import type { JSONObject } from "@shared/types";
import { observer } from "mobx-react";
import { KeyIcon, PlusIcon } from "outline-icons";
import * as React from "react";
@@ -66,9 +67,13 @@ function PasskeysSettings() {
}
);
const attResp = await startRegistration(resp.data);
await client.post("/passkeys.verifyRegistration", attResp as any, {
baseUrl: "/auth",
});
await client.post(
"/passkeys.verifyRegistration",
attResp as unknown as JSONObject,
{
baseUrl: "/auth",
}
);
toast.success(t("Passkey added successfully"));
await loadPasskeys();
} catch (err) {
+14 -13
View File
@@ -8,19 +8,20 @@ describe("getExpectedOrigin", () => {
hostname: string;
host: string;
forwardedPort?: string;
}): APIContext => ({
protocol: options.protocol,
request: {
hostname: options.hostname,
host: options.host,
get: (header: string) => {
if (header === "X-Forwarded-Port" && options.forwardedPort) {
return options.forwardedPort;
}
return undefined;
},
} as unknown,
}) as unknown as APIContext;
}): APIContext =>
({
protocol: options.protocol,
request: {
hostname: options.hostname,
host: options.host,
get: (header: string) => {
if (header === "X-Forwarded-Port" && options.forwardedPort) {
return options.forwardedPort;
}
return undefined;
},
} as unknown,
}) as unknown as APIContext;
it("should construct origin with non-standard HTTPS port from X-Forwarded-Port", () => {
const ctx = createMockContext({
@@ -236,13 +236,13 @@ export default class PostgresSearchProvider extends BaseSearchProvider {
where,
limit,
offset,
}) as any as Promise<RankedDocument[]>;
}) as unknown as Promise<RankedDocument[]>;
const countQuery = Document.unscoped().count({
// @ts-expect-error Types are incorrect for count
replacements: findOptions.replacements,
where,
}) as any as Promise<number>;
}) as unknown as Promise<number>;
const [results, count] = await Promise.all([resultsQuery, countQuery]);
// Final query to get associated document data
@@ -428,7 +428,7 @@ export default class PostgresSearchProvider extends BaseSearchProvider {
where,
limit,
offset,
})) as any as RankedDocument[];
})) as unknown as RankedDocument[];
const countQuery = Document.unscoped().count({
// @ts-expect-error Types are incorrect for count
@@ -436,7 +436,7 @@ export default class PostgresSearchProvider extends BaseSearchProvider {
include,
replacements: findOptions.replacements,
where,
}) as any as Promise<number>;
}) as unknown as Promise<number>;
// Final query to get associated document data
const [documents, count] = await Promise.all([
+2 -1
View File
@@ -238,7 +238,8 @@ router.post(
return;
}
const { results, total } = await SearchProviderManager.getProvider().searchForUser(user, options);
const { results, total } =
await SearchProviderManager.getProvider().searchForUser(user, options);
await SearchQuery.create({
userId: user ? user.id : null,
+9 -3
View File
@@ -1,4 +1,4 @@
import querystring from "node:querystring";
import querystring, { type ParsedUrlQueryInput } from "node:querystring";
import { InvalidRequestError } from "@server/errors";
import fetch from "@server/utils/fetch";
import { SlackUtils } from "../shared/SlackUtils";
@@ -13,7 +13,10 @@ const SLACK_API_URL = "https://slack.com/api";
* @param body - the request body containing token and other parameters.
* @returns the parsed JSON response from Slack.
*/
export async function post(endpoint: string, body: Record<string, any>) {
export async function post(
endpoint: string,
body: { token: string } & Record<string, unknown>
) {
let data;
const { token, ...bodyWithoutToken } = body;
@@ -44,7 +47,10 @@ export async function post(endpoint: string, body: Record<string, any>) {
* @param body - the request parameters.
* @returns the parsed JSON response from Slack.
*/
export async function request(endpoint: string, body: Record<string, any>) {
export async function request(
endpoint: string,
body: { client_id?: string; client_secret?: string } & ParsedUrlQueryInput
) {
let data;
const { client_id, client_secret, ...params } = body;
+1 -1
View File
@@ -16,7 +16,7 @@ export class SlackUtils {
static createState(
teamId: string,
type: IntegrationType,
data?: Record<string, any>
data?: Record<string, unknown>
) {
return JSON.stringify({ type, teamId, ...data });
}