From 9ed8b2b30c58a079b9ea5c42a2fe2ce4dfc1bb52 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Thu, 2 Apr 2026 07:43:46 +0200 Subject: [PATCH 01/12] Add `oxlint` to the repo --- .github/workflows/ci.yml | 3 + .oxlintrc.json | 31 +++++++ get-changed-packages.ts | 122 ++++++++++++++++------------ index.ts | 23 +++--- modules.d.ts | 3 + package.json | 6 +- pages/api/webhook.ts | 2 +- test/index.test.ts | 160 ++++++++++++++++++++---------------- yarn.lock | 170 ++++++++++++++++++++++++++++++++++++++- 9 files changed, 384 insertions(+), 136 deletions(-) create mode 100644 .oxlintrc.json create mode 100644 modules.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92c7d3d..51824f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,9 @@ jobs: - name: Run format:check run: yarn format:check + - name: Run lint and typecheck + run: yarn lint + ci-ok: name: CI OK runs-on: ubuntu-latest diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..c768cb0 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,31 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "unicorn", "oxc", "vitest", "import", "promise"], + "categories": { + "correctness": "error", + "suspicious": "error", + "perf": "error", + "pedantic": "error" + }, + "options": { + "typeAware": true, + "typeCheck": true + }, + "rules": { + "func-style": "off", + "max-lines": "off", + "max-lines-per-function": "off", + "no-implicit-coercion": "off", + "no-magic-numbers": "off", + "no-ternary": "off", + "no-warning-comments": "off", + "typescript/array-type": ["error", { "default": "generic", "readonly": "generic" }], + "typescript/consistent-type-imports": "error", + "typescript/no-unsafe-type-assertion": "off", + "typescript/return-await": "off", + "typescript/strict-boolean-expressions": "off" + }, + "env": { + "builtin": true + } +} diff --git a/get-changed-packages.ts b/get-changed-packages.ts index 2443efb..db1f5e3 100644 --- a/get-changed-packages.ts +++ b/get-changed-packages.ts @@ -3,14 +3,19 @@ import nodePath from "path"; import assembleReleasePlan from "@changesets/assemble-release-plan"; import { parse as parseConfig } from "@changesets/config"; import parseChangeset from "@changesets/parse"; -import { PreState, NewChangeset } from "@changesets/types"; -import { Packages, Tool } from "@manypkg/get-packages"; +import type { + NewChangeset, + PreState, + WrittenConfig, + PackageJSON as ChangesetPackageJSON, +} from "@changesets/types"; +import type { Packages, Tool } from "@manypkg/get-packages"; import { safeLoad } from "js-yaml"; import micromatch from "micromatch"; import fetch from "node-fetch"; -import { ProbotOctokit } from "probot"; +import type { ProbotOctokit } from "probot"; -export let getChangedPackages = async ({ +export const getChangedPackages = async ({ owner, repo, ref, @@ -21,12 +26,12 @@ export let getChangedPackages = async ({ owner: string; repo: string; ref: string; - changedFiles: string[] | Promise; + changedFiles: Array | Promise>; octokit: InstanceType; installationToken: string; }) => { let hasErrored = false; - let encodedCredentials = Buffer.from(`x-access-token:${installationToken}`).toString("base64"); + const encodedCredentials = Buffer.from(`x-access-token:${installationToken}`).toString("base64"); function fetchFile(path: string) { return fetch(`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`, { @@ -36,38 +41,45 @@ export let getChangedPackages = async ({ }); } - function fetchJsonFile(path: string) { - return fetchFile(path) - .then((x) => x.json()) - .catch((err) => { - hasErrored = true; - console.error(err); - return {}; - }); + async function fetchJsonFile(path: string): Promise { + try { + const x = await fetchFile(path); + return x.json() as Promise; + } catch (error) { + hasErrored = true; + console.error(error); + return {} as Promise; + } + } + + async function fetchTextFile(path: string): Promise { + try { + const x = await fetchFile(path); + return x.text(); + } catch (err) { + hasErrored = true; + console.error(err); + return ""; + } } - function fetchTextFile(path: string) { - return fetchFile(path) - .then((x) => x.text()) - .catch((err) => { - hasErrored = true; - console.error(err); - return ""; - }); + interface PackageJSON extends ChangesetPackageJSON { + workspaces?: Array | { packages: Array }; + bolt?: { workspaces: Array }; } - async function getPackage(pkgPath: string) { - let jsonContent = await fetchJsonFile(pkgPath + "/package.json"); + async function getPackage(pkgPath: string): Promise<{ dir: string; packageJson: PackageJSON }> { + const jsonContent = await fetchJsonFile(pkgPath + "/package.json"); return { - packageJson: jsonContent, dir: pkgPath, + packageJson: jsonContent as PackageJSON, }; } - let rootPackageJsonContentsPromise = fetchJsonFile("package.json"); - let configPromise: Promise = fetchJsonFile(".changeset/config.json"); + const rootPackageJsonContentsPromise: Promise = fetchJsonFile("package.json"); + const rawConfigPromise: Promise = fetchJsonFile(".changeset/config.json"); - let tree = await octokit.git.getTree({ + const tree = await octokit.git.getTree({ owner, repo, recursive: "1", @@ -75,15 +87,17 @@ export let getChangedPackages = async ({ }); let preStatePromise: Promise | undefined; - let changesetPromises: Promise[] = []; - let potentialWorkspaceDirectories: string[] = []; + const changesetPromises: Array> = []; + const potentialWorkspaceDirectories: Array = []; let isPnpm = false; - let changedFiles = await changedFilesPromise; + const changedFiles = await changedFilesPromise; - for (let item of tree.data.tree) { - if (!item.path) continue; + for (const item of tree.data.tree) { + if (!item.path) { + continue; + } if (item.path.endsWith("/package.json")) { - let dirPath = nodePath.dirname(item.path); + const dirPath = nodePath.dirname(item.path); potentialWorkspaceDirectories.push(dirPath); } else if (item.path === "pnpm-workspace.yaml") { isPnpm = true; @@ -95,56 +109,62 @@ export let getChangedPackages = async ({ item.path.endsWith(".md") && changedFiles.includes(item.path) ) { - let res = /\.changeset\/([^\.]+)\.md/.exec(item.path); + const res = /\.changeset\/([^.]+)\.md/.exec(item.path); if (!res) { throw new Error("could not get name from changeset filename"); } - let id = res[1]; + const id = res[1]; + changesetPromises.push( - fetchTextFile(item.path).then((text) => { - return { ...parseChangeset(text), id }; - }), + fetchTextFile(item.path).then((text) => ({ ...parseChangeset(text), id })), ); } } let tool: | { tool: Tool; - globs: string[]; + globs: Array; } | undefined; if (isPnpm) { + interface PnpmWorkspace { + packages: Array; + } + + const pnpmWorkspaceContent = await fetchTextFile("pnpm-workspace.yaml"); + const pnpmWorkspace = safeLoad(pnpmWorkspaceContent) as PnpmWorkspace; + tool = { + globs: pnpmWorkspace.packages, tool: "pnpm", - globs: safeLoad(await fetchTextFile("pnpm-workspace.yaml")).packages, }; } else { - let rootPackageJsonContent = await rootPackageJsonContentsPromise; + const rootPackageJsonContent = await rootPackageJsonContentsPromise; if (rootPackageJsonContent.workspaces) { - if (!Array.isArray(rootPackageJsonContent.workspaces)) { + if (Array.isArray(rootPackageJsonContent.workspaces)) { tool = { + globs: rootPackageJsonContent.workspaces, tool: "yarn", - globs: rootPackageJsonContent.workspaces.packages, }; } else { tool = { + globs: rootPackageJsonContent.workspaces.packages, tool: "yarn", - globs: rootPackageJsonContent.workspaces, }; } } else if (rootPackageJsonContent.bolt && rootPackageJsonContent.bolt.workspaces) { tool = { - tool: "bolt", globs: rootPackageJsonContent.bolt.workspaces, + tool: "bolt", }; } } - let rootPackageJsonContent = await rootPackageJsonContentsPromise; + const rootPackageJsonContent = await rootPackageJsonContentsPromise; - let packages: Packages = { + const packages: Packages = { root: { dir: "/", packageJson: rootPackageJsonContent, @@ -157,7 +177,7 @@ export let getChangedPackages = async ({ if (!Array.isArray(tool.globs) || !tool.globs.every((x) => typeof x === "string")) { throw new Error("globs are not valid: " + JSON.stringify(tool.globs)); } - let matches = micromatch(potentialWorkspaceDirectories, tool.globs); + const matches = micromatch(potentialWorkspaceDirectories, tool.globs); packages.packages = await Promise.all(matches.map((dir) => getPackage(dir))); } else { @@ -167,10 +187,12 @@ export let getChangedPackages = async ({ throw new Error("an error occurred when fetching files"); } + const rawConfig = await rawConfigPromise; + const releasePlan = assembleReleasePlan( await Promise.all(changesetPromises), packages, - await configPromise.then((rawConfig) => parseConfig(rawConfig, packages)), + parseConfig(rawConfig, packages), await preStatePromise, ); diff --git a/index.ts b/index.ts index 24ce0cf..ee67526 100644 --- a/index.ts +++ b/index.ts @@ -1,11 +1,10 @@ import { ValidationError } from "@changesets/errors"; -import { ReleasePlan, ComprehensiveRelease, VersionType } from "@changesets/types"; -import { EmitterWebhookEvent } from "@octokit/webhooks"; +import type { ReleasePlan, ComprehensiveRelease, VersionType } from "@changesets/types"; +import type { EmitterWebhookEvent } from "@octokit/webhooks"; import { captureException } from "@sentry/node"; -// @ts-ignore import humanId from "human-id"; import markdownTable from "markdown-table"; -import { Probot, Context } from "probot"; +import type { Probot, Context } from "probot"; import { getChangedPackages } from "./get-changed-packages"; @@ -31,7 +30,7 @@ const getReleasePlanMessage = (releasePlan: ReleasePlan | null) => { ]); return `
This PR includes ${ - releasePlan.changesets.length + releasePlan.changesets.length > 0 ? `changesets to release ${ publishableReleases.length === 1 ? "1 package" : `${publishableReleases.length} packages` }` @@ -39,7 +38,7 @@ const getReleasePlanMessage = (releasePlan: ReleasePlan | null) => { } ${ - publishableReleases.length + publishableReleases.length > 0 ? table : "When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types" } @@ -83,7 +82,7 @@ Not sure what this means? [Click here to learn what changesets are](https://git `; -const getNewChangesetTemplate = (changedPackages: string[], title: string) => +const getNewChangesetTemplate = (changedPackages: Array, title: string) => encodeURIComponent(`--- ${changedPackages.map((x) => `"${x}": patch`).join("\n")} --- @@ -120,8 +119,8 @@ const hasChangesetBeenAdded = ( ), ); -export default (app: Probot) => { - app.auth(); +export default (app: Probot): void => { + void app.auth(); app.log("Yay, the app was loaded!"); app.on(["pull_request.opened", "pull_request.synchronize"], async (context) => { @@ -152,13 +151,13 @@ export default (app: Probot) => { // deploying this doesn't cost money context.payload.action === "synchronize" ? getCommentId(context, { ...repo, issue_number: number }) - : undefined, + : Promise.resolve(null), hasChangesetBeenAdded(changedFilesPromise), getChangedPackages({ repo: context.payload.pull_request.head.repo.name, owner: context.payload.pull_request.head.repo.owner.login, ref: context.payload.pull_request.head.ref, - changedFiles: changedFilesPromise.then((x) => x.data.map((x) => x.filename)), + changedFiles: changedFilesPromise.then((x) => x.data.map(({ filename }) => filename)), octokit: context.octokit, installationToken: ( await (await app.auth()).apps.createInstallationAccessToken({ @@ -196,7 +195,7 @@ export default (app: Probot) => { errFromFetchingChangedFiles, }; - if (commentId != null) { + if (commentId !== null) { return context.octokit.issues.updateComment({ ...prComment, comment_id: commentId, diff --git a/modules.d.ts b/modules.d.ts new file mode 100644 index 0000000..1c06ab6 --- /dev/null +++ b/modules.d.ts @@ -0,0 +1,3 @@ +declare module "human-id" { + export default function humanId(options: { separator: string; capitalize: boolean }): string; +} diff --git a/package.json b/package.json index 4bf658f..a74bce3 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "scripts": { "format": "oxfmt", "format:check": "oxfmt --check", + "lint": "oxlint", + "lint:fix": "oxlint --fix", "test": "vitest" }, "dependencies": { @@ -32,11 +34,13 @@ "probot": "^12.2.4", "react": "^18.2.0", "react-dom": "^18.2.0", - "typescript": "^4.7.4" + "typescript": "^6.0.2" }, "devDependencies": { "msw": "^2.12.14", "oxfmt": "^0.42.0", + "oxlint": "^1.58.0", + "oxlint-tsgolint": "^0.19.0", "vite": "^8.0.3", "vitest": "^4.1.2" }, diff --git a/pages/api/webhook.ts b/pages/api/webhook.ts index 1ce3f97..ff16e81 100644 --- a/pages/api/webhook.ts +++ b/pages/api/webhook.ts @@ -2,7 +2,7 @@ import { createNodeMiddleware, createProbot } from "probot"; import app from "../../index"; -// requires: +// Requires: // - APP_ID // - PRIVATE_KEY // - WEBHOOK_SECRET diff --git a/test/index.test.ts b/test/index.test.ts index 2944981..d06e291 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,22 +1,28 @@ import assert from "node:assert/strict"; import { generateKeyPairSync } from "node:crypto"; -import { http, HttpResponse } from "msw"; +import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; import { Probot, ProbotOctokit } from "probot"; import { aroundEach, beforeAll, describe, it } from "vitest"; -import changesetBot, { PRContext } from "../index"; +import type { PRContext } from "../index"; +import changesetBot from "../index"; import pullRequestOpen from "./fixtures/pull_request.opened.json"; import pullRequestSynchronize from "./fixtures/pull_request.synchronize.json"; import releasePullRequestOpen from "./fixtures/release_pull_request.opened.json"; // TODO: it might be possible to remove this if improvements to `Array.isArray` ever land -// related thread: github.com/microsoft/TypeScript/issues/36554 +// Related thread: github.com/microsoft/TypeScript/issues/36554 function isArray( + // oxlint-disable-next-line typescript/ban-types arg: T | {}, -): arg is T extends readonly any[] ? (unknown extends T ? never : readonly any[]) : any[] { +): arg is T extends ReadonlyArray + ? unknown extends T + ? never + : ReadonlyArray + : Array { return Array.isArray(arg); } @@ -35,16 +41,16 @@ function setupMswServer() { const server = setupMswServer(); // Probot validates the privateKey locally -// so we must generate a valid key +// So we must generate a valid key const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048, - publicKeyEncoding: { - type: "spki", - format: "pem", - }, privateKeyEncoding: { + format: "pem", type: "pkcs8", + }, + publicKeyEncoding: { format: "pem", + type: "spki", }, }); @@ -52,7 +58,10 @@ const githubRepoBase = "https://api.github.com/repos/changesets/bot"; const githubAppBase = "https://api.github.com/app/installations"; const normalizeCommentBody = (body: string) => - body.replace(/filename=\.changeset\/[^)&\s]+?\.md/g, "filename=.changeset/.md"); + body.replaceAll( + /filename=\.changeset\/[^)&\s]+?\.md/g, + "filename=.changeset/.md", + ); type ChangedFile = [ { @@ -63,24 +72,24 @@ type ChangedFile = [ type FileState = string | ChangedFile; -type CommentState = { +interface CommentState { id: number; user: { login: string }; -}; +} -type PrState = { +interface PrState { files: Record; - comments?: CommentState[]; -}; + comments?: Array; +} -type RecordedRequest = { +interface RecordedRequest { method: string; path: string; body?: unknown; -}; +} -function usePrState(server: ReturnType, state: PrState) { - const requests: RecordedRequest[] = []; +function usePrState(apiServer: ReturnType, state: PrState) { + const requests: Array = []; const recordRequest = async (request: Request, mapper?: (body: unknown) => unknown) => { let body: unknown; @@ -100,13 +109,13 @@ function usePrState(server: ReturnType, state: PrState) { } requests.push({ + body, method: request.method, path: new URL(request.url).pathname, - body, }); }; - server.use( + apiServer.use( http.post(`${githubAppBase}/:installationId/access_tokens`, async ({ request }) => { await recordRequest(request); return HttpResponse.json({ token: "test" }); @@ -124,11 +133,13 @@ function usePrState(server: ReturnType, state: PrState) { }), http.get(`${githubRepoBase}/pulls/2/files`, async ({ request }) => { await recordRequest(request); - // we only use those 2 fields right now, so we don't bother with the rest of the type - const changedFiles: Pick< - Awaited>["data"][number], - "filename" | "status" - >[] = []; + // We only use those 2 fields right now, so we don't bother with the rest of the type + const changedFiles: Array< + Pick< + Awaited>["data"][number], + "filename" | "status" + > + > = []; for (const [filename, file] of Object.entries(state.files)) { if (typeof file !== "string") { changedFiles.push({ @@ -145,16 +156,17 @@ function usePrState(server: ReturnType, state: PrState) { await recordRequest(request); const path = isArray(params.path) ? params.path.join("/") : params.path; - assert(path); + assert.ok(path); const file = state.files[path]; - if (file == null) { + if (file === null) { return new HttpResponse("Not found", { status: 404 }); } const content = typeof file === "string" ? file : file[1]; if (path.endsWith(".json")) { + // oxlint-disable-next-line typescript/no-unsafe-argument return HttpResponse.json(JSON.parse(content)); } @@ -163,7 +175,7 @@ function usePrState(server: ReturnType, state: PrState) { ), http.post(`${githubRepoBase}/issues/2/comments`, async ({ request }) => { await recordRequest(request, (body) => { - assert( + assert.ok( !!body && typeof body === "object" && "body" in body && typeof body.body === "string", ); return { ...body, body: normalizeCommentBody(body.body) }; @@ -172,7 +184,7 @@ function usePrState(server: ReturnType, state: PrState) { }), http.patch(`${githubRepoBase}/issues/comments/:commentId`, async ({ request }) => { await recordRequest(request, (body) => { - assert( + assert.ok( !!body && typeof body === "object" && "body" in body && typeof body.body === "string", ); return { ...body, body: normalizeCommentBody(body.body) }; @@ -185,14 +197,14 @@ function usePrState(server: ReturnType, state: PrState) { } const baseFiles = { + ".changeset/config.json": JSON.stringify({}), "package.json": JSON.stringify({ name: "test", workspaces: ["packages/*"], }), - ".changeset/config.json": JSON.stringify({}), }; -function setupProbot(testId: string) { +function setupProbot(testId: string): Probot { // Probot reuses some global state for Octokit instances for the same installation id. // That makes MSW mocking unreliable as requests scheduled by one test can be actually dispatched from the context of another test. const TestOctokit = ProbotOctokit.defaults({ @@ -200,7 +212,7 @@ function setupProbot(testId: string) { id: `test-${testId}`, }, }); - const probot = new Probot({ appId: 123, privateKey, Octokit: TestOctokit }); + const probot = new Probot({ Octokit: TestOctokit, appId: 123, privateKey }); changesetBot(probot); return probot; } @@ -209,17 +221,18 @@ describe.concurrent("changeset-bot", () => { it("adds a comment when there is no comment", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { + comments: [], files: { ...baseFiles, ".changeset/something-changed.md": [{ status: "added" }, "---\n---\n"], }, - comments: [], }); await probot.receive({ name: "pull_request", + // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - } as never); + }); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -255,24 +268,27 @@ describe.concurrent("changeset-bot", () => { it("should update a comment when there is a comment", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { - files: { - ...baseFiles, - ".changeset/something/changes.md": [{ status: "added" }, "---\n---\n"], - }, comments: [ { id: 7, user: { login: "changeset-bot[bot]" }, }, ], + files: { + ...baseFiles, + ".changeset/something/changes.md": [{ status: "added" }, "---\n---\n"], + }, }); await probot.receive({ name: "pull_request", + // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestSynchronize, - } as never); + }); const commentRequests = requests.filter( + // https://github.com/oxc-project/oxc/issues/20894 + // oxlint-disable-next-line jest/no-conditional-in-test (request) => request.path.includes("/comments") && request.method === "PATCH", ); @@ -309,17 +325,18 @@ describe.concurrent("changeset-bot", () => { it("should show correct message if there is a changeset", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { + comments: [], files: { ...baseFiles, ".changeset/something/changes.md": [{ status: "added" }, "---\n---\n"], }, - comments: [], }); await probot.receive({ name: "pull_request", + // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - } as never); + }); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -355,17 +372,18 @@ describe.concurrent("changeset-bot", () => { it("should show correct message if there is no changeset", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { + comments: [], files: { ...baseFiles, "index.js": [{ status: "added" }, "console.log('test');"], }, - comments: [], }); await probot.receive({ name: "pull_request", + // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - } as never); + }); const commentRequests = requests.filter((request) => request.path.includes("/comments")); expect(commentRequests).toMatchInlineSnapshot(` @@ -400,20 +418,21 @@ describe.concurrent("changeset-bot", () => { it("uses the root package when no workspace tool is detected", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { + comments: [], files: { + ".changeset/config.json": JSON.stringify({}), "package.json": JSON.stringify({ name: "root-package", }), - ".changeset/config.json": JSON.stringify({}), "src/index.ts": [{ status: "added" }, "export {};"], }, - comments: [], }); await probot.receive({ name: "pull_request", + // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - } as never); + }); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -452,27 +471,28 @@ describe.concurrent("changeset-bot", () => { }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { + comments: [], files: { + ".changeset/config.json": JSON.stringify({}), "package.json": JSON.stringify({ name: "test", workspaces: ["packages/*"], }), - ".changeset/config.json": JSON.stringify({}), + "packages/a/index.ts": [{ status: "added" }, "export const a = true;"], "packages/a/package.json": JSON.stringify({ name: "pkg-a", }), "packages/b/package.json": JSON.stringify({ name: "pkg-b", }), - "packages/a/index.ts": [{ status: "added" }, "export const a = true;"], }, - comments: [], }); await probot.receive({ name: "pull_request", + // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - } as never); + }); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -511,27 +531,28 @@ describe.concurrent("changeset-bot", () => { }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { + comments: [], files: { + ".changeset/config.json": JSON.stringify({}), "package.json": JSON.stringify({ name: "test", workspaces: ["packages/*"], }), - ".changeset/config.json": JSON.stringify({}), "packages/a/package.json": JSON.stringify({ name: "pkg-a", }), + "packages/ab/index.ts": [{ status: "added" }, "export const ab = true;"], "packages/ab/package.json": JSON.stringify({ name: "pkg-ab", }), - "packages/ab/index.ts": [{ status: "added" }, "export const ab = true;"], }, - comments: [], }); await probot.receive({ name: "pull_request", + // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - } as never); + }); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -567,24 +588,25 @@ describe.concurrent("changeset-bot", () => { it("detects pnpm workspaces when building the add-changeset link", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { + comments: [], files: { + ".changeset/config.json": JSON.stringify({}), "package.json": JSON.stringify({ name: "test", }), - ".changeset/config.json": JSON.stringify({}), - "pnpm-workspace.yaml": "packages:\n - packages/*\n", + "packages/a/file.ts": [{ status: "added" }, "export const a = true;"], "packages/a/package.json": JSON.stringify({ name: "pkg-a", }), - "packages/a/file.ts": [{ status: "added" }, "export const a = true;"], + "pnpm-workspace.yaml": "packages:\n - packages/*\n", }, - comments: [], }); await probot.receive({ name: "pull_request", + // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - } as never); + }); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -623,12 +645,9 @@ describe.concurrent("changeset-bot", () => { }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { + comments: [], files: { ...baseFiles, - "packages/a/package.json": JSON.stringify({ - name: "pkg-a", - version: "1.0.0", - }), ".changeset/abc123.md": [ { status: "added", @@ -640,14 +659,18 @@ describe.concurrent("changeset-bot", () => { add feature `, ], + "packages/a/package.json": JSON.stringify({ + name: "pkg-a", + version: "1.0.0", + }), }, - comments: [], }); await probot.receive({ name: "pull_request", + // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - } as never); + }); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -685,14 +708,15 @@ add feature it("shouldn't add a comment to a release pull request", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { - files: baseFiles, comments: [], + files: baseFiles, }); await probot.receive({ name: "pull_request", + // @ts-expect-error fixtures json doesn't match typescript type payload: releasePullRequestOpen, - } as never); + }); expect(requests).toHaveLength(0); }); diff --git a/yarn.lock b/yarn.lock index c7f167c..3fd8f84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -607,6 +607,131 @@ resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.42.0.tgz#e7e455723361ab1253b75407aa90ca043711c9dc" integrity sha512-Wg4TMAfQRL9J9AZevJ/ZNy3uyyDztDYQtGr4P8UyyzIhLhFrdSmz1J/9JT+rv0fiCDLaFOBQnj3f3K3+a5PzDQ== +"@oxlint-tsgolint/darwin-arm64@0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-0.19.0.tgz#9d7039c2c7dbac6930c56ba9dd74c4240d006588" + integrity sha512-FVOIp5Njte8Z6PpINz7sL5blqSro0pAL8VAHYQ+K5Xm4cOrPQ6DGIhH14oXnbRjzn8Kl69qjz8TPteyn8EqwsQ== + +"@oxlint-tsgolint/darwin-x64@0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/darwin-x64/-/darwin-x64-0.19.0.tgz#c4e329852ad4a6e7cc4d1c29c711e3f6202d9e6b" + integrity sha512-GakDTDACePvqOFq3N4oQCl8SyMMa7VBnqV0gDcXPuK50jdWCUqlxM9tgRJarjyIVvmDEJRGYOen+4uBtVwg4Aw== + +"@oxlint-tsgolint/linux-arm64@0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/linux-arm64/-/linux-arm64-0.19.0.tgz#7b930137cc9684ab462181a9745068eca1741661" + integrity sha512-Ya0R7somo+KDhhkPtENJ9Q28Fost+aqA3MPe86pEqgmukHFc/KO65PgShOSbIFjZNptELEQvsWL8gDxYZWhH3w== + +"@oxlint-tsgolint/linux-x64@0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/linux-x64/-/linux-x64-0.19.0.tgz#60d020225f4e92b1faf0c3fee3b212012d2f43b2" + integrity sha512-yFH378jWc1k/oJmpk+TKpWbKvFieJJvsOHxVMSNFc+ukqs44ZSHVt4HFfAhXAt/bzVK2f7EIDTGp8Hm1OjoJ6Q== + +"@oxlint-tsgolint/win32-arm64@0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-arm64/-/win32-arm64-0.19.0.tgz#8f47b04245263cdf02303c6a4197a977de18d196" + integrity sha512-R6NyAtha7OWxh7NGBeFxqDTGAVl1Xj4xLa8Qj39PKbIDqBeVW8BIb+1nEnRp+Mo/VpRoeoFAcqlBsuMcUMd26Q== + +"@oxlint-tsgolint/win32-x64@0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-x64/-/win32-x64-0.19.0.tgz#49826f086a0dac8f559e1d4a79f7a0b42adeb75e" + integrity sha512-2ePvxcbS5tPOmrQvxR8Kc+IqzdTtlrGeMDv+jjTYfkTFPmh2rF9yxVchi/4WM6js3gt2UauQeMV/tfnZNemENQ== + +"@oxlint/binding-android-arm-eabi@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.58.0.tgz#c31d2a92b9ac8b7fef7a8cd747b3fd034b19debc" + integrity sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg== + +"@oxlint/binding-android-arm64@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.58.0.tgz#8cdcbc6033ad849b5950ea876fd532887cbb2c92" + integrity sha512-GryzujxuiRv2YFF7bRy8mKcxlbuAN+euVUtGJt9KKbLT8JBUIosamVhcthLh+VEr6KE6cjeVMAQxKAzJcoN7dg== + +"@oxlint/binding-darwin-arm64@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.58.0.tgz#c675ad8c1058525553375e2a93be98c0a0d7e6d6" + integrity sha512-7/bRSJIwl4GxeZL9rPZ11anNTyUO9epZrfEJH/ZMla3+/gbQ6xZixh9nOhsZ0QwsTW7/5J2A/fHbD1udC5DQQA== + +"@oxlint/binding-darwin-x64@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.58.0.tgz#56f325cdaa2e04fc9310dff6ca1e914536cb8031" + integrity sha512-EqdtJSiHweS2vfILNrpyJ6HUwpEq2g7+4Zx1FPi4hu3Hu7tC3znF6ufbXO8Ub2LD4mGgznjI7kSdku9NDD1Mkg== + +"@oxlint/binding-freebsd-x64@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.58.0.tgz#09b967159fee7a2d508b4967dd5cc6037a6debe4" + integrity sha512-VQt5TH4M42mY20F545G637RKxV/yjwVtKk2vfXuazfReSIiuvWBnv+FVSvIV5fKVTJNjt3GSJibh6JecbhGdBw== + +"@oxlint/binding-linux-arm-gnueabihf@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.58.0.tgz#9ac6de7595663f270d8da00865f4d87f8a53933c" + integrity sha512-fBYcj4ucwpAtjJT3oeBdFBYKvNyjRSK+cyuvBOTQjh0jvKp4yeA4S/D0IsCHus/VPaNG5L48qQkh+Vjy3HL2/Q== + +"@oxlint/binding-linux-arm-musleabihf@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.58.0.tgz#f7d2e7dad5d7401a4f03806d1112b9a377f8ae29" + integrity sha512-0BeuFfwlUHlJ1xpEdSD1YO3vByEFGPg36uLjK1JgFaxFb4W6w17F8ET8sz5cheZ4+x5f2xzdnRrrWv83E3Yd8g== + +"@oxlint/binding-linux-arm64-gnu@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.58.0.tgz#5879e9f0a37aad243a64861cf18725fbeceea6db" + integrity sha512-TXlZgnPTlxrQzxG9ZXU7BNwx1Ilrr17P3GwZY0If2EzrinqRH3zXPc3HrRcBJgcsoZNMuNL5YivtkJYgp467UQ== + +"@oxlint/binding-linux-arm64-musl@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.58.0.tgz#7a587aa4621e865c91ddf1b824cfd94243b1582b" + integrity sha512-zSoYRo5dxHLcUx93Stl2hW3hSNjPt99O70eRVWt5A1zwJ+FPjeCCANCD2a9R4JbHsdcl11TIQOjyigcRVOH2mw== + +"@oxlint/binding-linux-ppc64-gnu@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.58.0.tgz#b716ccd651cf60d61dc8c82fa5f0febebb5a76fd" + integrity sha512-NQ0U/lqxH2/VxBYeAIvMNUK1y0a1bJ3ZicqkF2c6wfakbEciP9jvIE4yNzCFpZaqeIeRYaV7AVGqEO1yrfVPjA== + +"@oxlint/binding-linux-riscv64-gnu@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.58.0.tgz#84eb41337d2f6978ed8cde9b28ae1a7b8ea248eb" + integrity sha512-X9J+kr3gIC9FT8GuZt0ekzpNUtkBVzMVU4KiKDSlocyQuEgi3gBbXYN8UkQiV77FTusLDPsovjo95YedHr+3yg== + +"@oxlint/binding-linux-riscv64-musl@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.58.0.tgz#12d8ece59a14620686f1326d79be5994184db0c9" + integrity sha512-CDze3pi1OO3Wvb/QsXjmLEY4XPKGM6kIo82ssNOgmcl1IdndF9VSGAE38YLhADWmOac7fjqhBw82LozuUVxD0Q== + +"@oxlint/binding-linux-s390x-gnu@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.58.0.tgz#35815bcdd34270bea72b5e04c19f13c3a8821314" + integrity sha512-b/89glbxFaEAcA6Uf1FvCNecBJEgcUTsV1quzrqXM/o4R1M4u+2KCVuyGCayN2UpsRWtGGLb+Ver0tBBpxaPog== + +"@oxlint/binding-linux-x64-gnu@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.58.0.tgz#5935b9600af6daa34157477cfaaf56aaa0ae8e0d" + integrity sha512-0/yYpkq9VJFCEcuRlrViGj8pJUFFvNS4EkEREaN7CB1EcLXJIaVSSa5eCihwBGXtOZxhnblWgxks9juRdNQI7w== + +"@oxlint/binding-linux-x64-musl@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.58.0.tgz#39a9b4d733db354d3a735a659020230332b1a7e7" + integrity sha512-hr6FNvmcAXiH+JxSvaJ4SJ1HofkdqEElXICW9sm3/Rd5eC3t7kzvmLyRAB3NngKO2wzXRCAm4Z/mGWfrsS4X8w== + +"@oxlint/binding-openharmony-arm64@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.58.0.tgz#84b9c4d16a0199eefc989ae1b49e303566184a97" + integrity sha512-R+O368VXgRql1K6Xar+FEo7NEwfo13EibPMoTv3sesYQedRXd6m30Dh/7lZMxnrQVFfeo4EOfYIP4FpcgWQNHg== + +"@oxlint/binding-win32-arm64-msvc@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.58.0.tgz#ae119d7defed3c48e6002465699f72ed4548681f" + integrity sha512-Q0FZiAY/3c4YRj4z3h9K1PgaByrifrfbBoODSeX7gy97UtB7pySPUQfC2B/GbxWU6k7CzQrRy5gME10PltLAFQ== + +"@oxlint/binding-win32-ia32-msvc@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.58.0.tgz#54d2e7a413ed40142ea7d920e531ca50b8347b05" + integrity sha512-Y8FKBABrSPp9H0QkRLHDHOSUgM/309a3IvOVgPcVxYcX70wxJrk608CuTg7w+C6vEd724X5wJoNkBcGYfH7nNQ== + +"@oxlint/binding-win32-x64-msvc@1.58.0": + version "1.58.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.58.0.tgz#692134ba89311a56ea2e5d7002d90535b9333f06" + integrity sha512-bCn5rbiz5My+Bj7M09sDcnqW0QJyINRVxdZ65x1/Y2tGrMwherwK/lpk+HRQCKvXa8pcaQdF5KY5j54VGZLwNg== + "@probot/get-private-key@^1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@probot/get-private-key/-/get-private-key-1.1.1.tgz#12bf61d00a15760d9b0bd713a794f9c4ba4ad5d3" @@ -2801,6 +2926,43 @@ oxfmt@^0.42.0: "@oxfmt/binding-win32-ia32-msvc" "0.42.0" "@oxfmt/binding-win32-x64-msvc" "0.42.0" +oxlint-tsgolint@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/oxlint-tsgolint/-/oxlint-tsgolint-0.19.0.tgz#cd3262266aa0b4c2375a334995d18da8723cb51d" + integrity sha512-pSzUmDjMyjC8iUUZ7fCLo0D1iUaYIfodd/WIQ6Zra11YkjkUQk3BOFoW4I5ec6uZ/0s2FEmxtiZ7hiTXFRp1cg== + optionalDependencies: + "@oxlint-tsgolint/darwin-arm64" "0.19.0" + "@oxlint-tsgolint/darwin-x64" "0.19.0" + "@oxlint-tsgolint/linux-arm64" "0.19.0" + "@oxlint-tsgolint/linux-x64" "0.19.0" + "@oxlint-tsgolint/win32-arm64" "0.19.0" + "@oxlint-tsgolint/win32-x64" "0.19.0" + +oxlint@^1.58.0: + version "1.58.0" + resolved "https://registry.yarnpkg.com/oxlint/-/oxlint-1.58.0.tgz#68980c5404dab1250e868fec63bd1050f411dd81" + integrity sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg== + optionalDependencies: + "@oxlint/binding-android-arm-eabi" "1.58.0" + "@oxlint/binding-android-arm64" "1.58.0" + "@oxlint/binding-darwin-arm64" "1.58.0" + "@oxlint/binding-darwin-x64" "1.58.0" + "@oxlint/binding-freebsd-x64" "1.58.0" + "@oxlint/binding-linux-arm-gnueabihf" "1.58.0" + "@oxlint/binding-linux-arm-musleabihf" "1.58.0" + "@oxlint/binding-linux-arm64-gnu" "1.58.0" + "@oxlint/binding-linux-arm64-musl" "1.58.0" + "@oxlint/binding-linux-ppc64-gnu" "1.58.0" + "@oxlint/binding-linux-riscv64-gnu" "1.58.0" + "@oxlint/binding-linux-riscv64-musl" "1.58.0" + "@oxlint/binding-linux-s390x-gnu" "1.58.0" + "@oxlint/binding-linux-x64-gnu" "1.58.0" + "@oxlint/binding-linux-x64-musl" "1.58.0" + "@oxlint/binding-openharmony-arm64" "1.58.0" + "@oxlint/binding-win32-arm64-msvc" "1.58.0" + "@oxlint/binding-win32-ia32-msvc" "1.58.0" + "@oxlint/binding-win32-x64-msvc" "1.58.0" + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -3688,10 +3850,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^4.7.4: - version "4.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" - integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== +typescript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.2.tgz#0b1bfb15f68c64b97032f3d78abbf98bdbba501f" + integrity sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ== uglify-js@^3.1.4: version "3.6.0" From 604909cfc9771102a44d3dc763f43580548c5835 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 3 Apr 2026 13:04:22 +0200 Subject: [PATCH 02/12] refine banned types related rules --- .oxlintrc.json => .oxlintrc.jsonc | 13 ++++++++----- test/index.test.ts | 1 - 2 files changed, 8 insertions(+), 6 deletions(-) rename .oxlintrc.json => .oxlintrc.jsonc (68%) diff --git a/.oxlintrc.json b/.oxlintrc.jsonc similarity index 68% rename from .oxlintrc.json rename to .oxlintrc.jsonc index c768cb0..db939d1 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.jsonc @@ -5,11 +5,11 @@ "correctness": "error", "suspicious": "error", "perf": "error", - "pedantic": "error" + "pedantic": "error", }, "options": { "typeAware": true, - "typeCheck": true + "typeCheck": true, }, "rules": { "func-style": "off", @@ -20,12 +20,15 @@ "no-ternary": "off", "no-warning-comments": "off", "typescript/array-type": ["error", { "default": "generic", "readonly": "generic" }], + "typescript/ban-types": "off", // deprecated, replaced by no-unsafe-function-type and no-wrapper-object-types "typescript/consistent-type-imports": "error", "typescript/no-unsafe-type-assertion": "off", + "typescript/no-unsafe-function-type": "error", + "typescript/no-wrapper-object-types": "error", "typescript/return-await": "off", - "typescript/strict-boolean-expressions": "off" + "typescript/strict-boolean-expressions": "off", }, "env": { - "builtin": true - } + "builtin": true, + }, } diff --git a/test/index.test.ts b/test/index.test.ts index d06e291..e38d426 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -16,7 +16,6 @@ import releasePullRequestOpen from "./fixtures/release_pull_request.opened.json" // TODO: it might be possible to remove this if improvements to `Array.isArray` ever land // Related thread: github.com/microsoft/TypeScript/issues/36554 function isArray( - // oxlint-disable-next-line typescript/ban-types arg: T | {}, ): arg is T extends ReadonlyArray ? unknown extends T From bb5045c1986245126f033127ba3193004aebd0e7 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 3 Apr 2026 13:10:54 +0200 Subject: [PATCH 03/12] refine PRSttate --- test/index.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/index.test.ts b/test/index.test.ts index e38d426..f75341c 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -77,7 +77,7 @@ interface CommentState { } interface PrState { - files: Record; + files: Partial>; comments?: Array; } @@ -143,7 +143,7 @@ function usePrState(apiServer: ReturnType, state: PrState) { if (typeof file !== "string") { changedFiles.push({ filename, - status: file[0].status, + status: file![0].status, }); } } @@ -158,7 +158,7 @@ function usePrState(apiServer: ReturnType, state: PrState) { assert.ok(path); const file = state.files[path]; - if (file === null) { + if (file === undefined) { return new HttpResponse("Not found", { status: 404 }); } From 0cc8930f447096e8a8980476f32163c7e4a6ac1b Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 3 Apr 2026 13:58:24 +0200 Subject: [PATCH 04/12] refine CI --- .github/workflows/ci.yml | 7 +++++-- package.json | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51824f2..c5e497a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,8 +44,11 @@ jobs: - name: Run format:check run: yarn format:check - - name: Run lint and typecheck - run: yarn lint + - name: Run lint + run: yarn lint:ci + + - name: Run typecheck + run: yarn typecheck ci-ok: name: CI OK diff --git a/package.json b/package.json index a74bce3..7adf701 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "format:check": "oxfmt --check", "lint": "oxlint", "lint:fix": "oxlint --fix", + "lint:ci": "oxlint --type-check false", + "typecheck": "tsc --noEmit", "test": "vitest" }, "dependencies": { From 0248bdba75f6ab74c427b2cc4f6f7c3aeeb38188 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 3 Apr 2026 13:58:47 +0200 Subject: [PATCH 05/12] no explicit return type rules --- .oxlintrc.jsonc | 2 ++ test/index.test.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index db939d1..d3afb4b 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -22,6 +22,8 @@ "typescript/array-type": ["error", { "default": "generic", "readonly": "generic" }], "typescript/ban-types": "off", // deprecated, replaced by no-unsafe-function-type and no-wrapper-object-types "typescript/consistent-type-imports": "error", + "typescript/explicit-function-return-type": "off", + "typescript/explicit-module-boundary-types": "off", "typescript/no-unsafe-type-assertion": "off", "typescript/no-unsafe-function-type": "error", "typescript/no-wrapper-object-types": "error", diff --git a/test/index.test.ts b/test/index.test.ts index f75341c..bc18b79 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -203,7 +203,7 @@ const baseFiles = { }), }; -function setupProbot(testId: string): Probot { +function setupProbot(testId: string) { // Probot reuses some global state for Octokit instances for the same installation id. // That makes MSW mocking unreliable as requests scheduled by one test can be actually dispatched from the context of another test. const TestOctokit = ProbotOctokit.defaults({ From 06d0118f73332115bf85e63f9e0f875a034e78ff Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 3 Apr 2026 13:59:13 +0200 Subject: [PATCH 06/12] remove ts-expect error directives --- index.ts | 2 +- test/index.test.ts | 32 +++++++++++--------------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/index.ts b/index.ts index ee67526..0696936 100644 --- a/index.ts +++ b/index.ts @@ -145,7 +145,7 @@ export default (app: Probot): void => { }); const [commentId, hasChangeset, { changedPackages, releasePlan }] = await Promise.all([ - // we know the comment won't exist on opened events + // We know the comment won't exist on opened events // ok, well like technically that's wrong // but reducing time is nice here so that // deploying this doesn't cost money diff --git a/test/index.test.ts b/test/index.test.ts index bc18b79..f988466 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -14,7 +14,7 @@ import pullRequestSynchronize from "./fixtures/pull_request.synchronize.json"; import releasePullRequestOpen from "./fixtures/release_pull_request.opened.json"; // TODO: it might be possible to remove this if improvements to `Array.isArray` ever land -// Related thread: github.com/microsoft/TypeScript/issues/36554 +// related thread: github.com/microsoft/TypeScript/issues/36554 function isArray( arg: T | {}, ): arg is T extends ReadonlyArray @@ -229,9 +229,8 @@ describe.concurrent("changeset-bot", () => { await probot.receive({ name: "pull_request", - // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - }); + } as never); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -281,9 +280,8 @@ describe.concurrent("changeset-bot", () => { await probot.receive({ name: "pull_request", - // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestSynchronize, - }); + } as never); const commentRequests = requests.filter( // https://github.com/oxc-project/oxc/issues/20894 @@ -333,9 +331,8 @@ describe.concurrent("changeset-bot", () => { await probot.receive({ name: "pull_request", - // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - }); + } as never); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -380,9 +377,8 @@ describe.concurrent("changeset-bot", () => { await probot.receive({ name: "pull_request", - // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - }); + } as never); const commentRequests = requests.filter((request) => request.path.includes("/comments")); expect(commentRequests).toMatchInlineSnapshot(` @@ -429,9 +425,8 @@ describe.concurrent("changeset-bot", () => { await probot.receive({ name: "pull_request", - // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - }); + } as never); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -489,9 +484,8 @@ describe.concurrent("changeset-bot", () => { await probot.receive({ name: "pull_request", - // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - }); + } as never); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -549,9 +543,8 @@ describe.concurrent("changeset-bot", () => { await probot.receive({ name: "pull_request", - // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - }); + } as never); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -603,9 +596,8 @@ describe.concurrent("changeset-bot", () => { await probot.receive({ name: "pull_request", - // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - }); + } as never); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -667,9 +659,8 @@ add feature await probot.receive({ name: "pull_request", - // @ts-expect-error fixtures json doesn't match typescript type payload: pullRequestOpen, - }); + } as never); const commentRequests = requests.filter((request) => request.path.includes("/comments")); @@ -713,9 +704,8 @@ add feature await probot.receive({ name: "pull_request", - // @ts-expect-error fixtures json doesn't match typescript type payload: releasePullRequestOpen, - }); + } as never); expect(requests).toHaveLength(0); }); From 94aa66234fa886234b49bb449ecfdbc80feaed34 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 3 Apr 2026 14:01:22 +0200 Subject: [PATCH 07/12] avoid object keys sorting --- get-changed-packages.ts | 8 ++++---- test/index.test.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/get-changed-packages.ts b/get-changed-packages.ts index db1f5e3..295dfa6 100644 --- a/get-changed-packages.ts +++ b/get-changed-packages.ts @@ -136,8 +136,8 @@ export const getChangedPackages = async ({ const pnpmWorkspace = safeLoad(pnpmWorkspaceContent) as PnpmWorkspace; tool = { - globs: pnpmWorkspace.packages, tool: "pnpm", + globs: pnpmWorkspace.packages, }; } else { const rootPackageJsonContent = await rootPackageJsonContentsPromise; @@ -145,19 +145,19 @@ export const getChangedPackages = async ({ if (rootPackageJsonContent.workspaces) { if (Array.isArray(rootPackageJsonContent.workspaces)) { tool = { - globs: rootPackageJsonContent.workspaces, tool: "yarn", + globs: rootPackageJsonContent.workspaces, }; } else { tool = { - globs: rootPackageJsonContent.workspaces.packages, tool: "yarn", + globs: rootPackageJsonContent.workspaces.packages, }; } } else if (rootPackageJsonContent.bolt && rootPackageJsonContent.bolt.workspaces) { tool = { - globs: rootPackageJsonContent.bolt.workspaces, tool: "bolt", + globs: rootPackageJsonContent.bolt.workspaces, }; } } diff --git a/test/index.test.ts b/test/index.test.ts index f988466..d627951 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -220,11 +220,11 @@ describe.concurrent("changeset-bot", () => { it("adds a comment when there is no comment", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { - comments: [], files: { ...baseFiles, ".changeset/something-changed.md": [{ status: "added" }, "---\n---\n"], }, + comments: [], }); await probot.receive({ From 44cc05069a373217cf0d2a250c81f085bd4303f2 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 3 Apr 2026 14:03:53 +0200 Subject: [PATCH 08/12] remove redundant variable --- get-changed-packages.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/get-changed-packages.ts b/get-changed-packages.ts index 295dfa6..8aaaef3 100644 --- a/get-changed-packages.ts +++ b/get-changed-packages.ts @@ -187,12 +187,10 @@ export const getChangedPackages = async ({ throw new Error("an error occurred when fetching files"); } - const rawConfig = await rawConfigPromise; - const releasePlan = assembleReleasePlan( await Promise.all(changesetPromises), packages, - parseConfig(rawConfig, packages), + parseConfig(await rawConfigPromise, packages), await preStatePromise, ); From 1b6d461b6ea7439c3b37abcf3b484fb82964fd86 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 3 Apr 2026 14:07:09 +0200 Subject: [PATCH 09/12] disable jest/no-conditional-in-test --- .oxlintrc.jsonc | 1 + test/index.test.ts | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index d3afb4b..e412d17 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -19,6 +19,7 @@ "no-magic-numbers": "off", "no-ternary": "off", "no-warning-comments": "off", + "jest/no-conditional-in-test": "off", "typescript/array-type": ["error", { "default": "generic", "readonly": "generic" }], "typescript/ban-types": "off", // deprecated, replaced by no-unsafe-function-type and no-wrapper-object-types "typescript/consistent-type-imports": "error", diff --git a/test/index.test.ts b/test/index.test.ts index d627951..f4f7e43 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -284,8 +284,6 @@ describe.concurrent("changeset-bot", () => { } as never); const commentRequests = requests.filter( - // https://github.com/oxc-project/oxc/issues/20894 - // oxlint-disable-next-line jest/no-conditional-in-test (request) => request.path.includes("/comments") && request.method === "PATCH", ); From 8901416e9cd24d69559b7064ad87764e272ea554 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 3 Apr 2026 15:54:16 +0200 Subject: [PATCH 10/12] disable oxlint typecheck on both local and CI env --- .oxlintrc.jsonc | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index e412d17..85edda8 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -9,7 +9,7 @@ }, "options": { "typeAware": true, - "typeCheck": true, + "typeCheck": false, }, "rules": { "func-style": "off", diff --git a/package.json b/package.json index 7adf701..2319677 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "format:check": "oxfmt --check", "lint": "oxlint", "lint:fix": "oxlint --fix", - "lint:ci": "oxlint --type-check false", + "lint:ci": "oxlint --format=github", "typecheck": "tsc --noEmit", "test": "vitest" }, From eaf4e8fd97a13fbe482e63e192640869bc0f96b2 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 3 Apr 2026 16:02:31 +0200 Subject: [PATCH 11/12] consistent step names between jobs --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5e497a..09ef407 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,11 +35,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1 + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1 with: persist-credentials: false - - uses: ./.github/actions/ci-setup + - name: Setup dependencies + uses: ./.github/actions/ci-setup - name: Run format:check run: yarn format:check From 65721432120e53a3eff32c46c5680d3fbd25b7e1 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Sat, 4 Apr 2026 17:52:20 +0200 Subject: [PATCH 12/12] sort keys and comment refinements to match review feedback --- index.ts | 2 +- test/index.test.ts | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/index.ts b/index.ts index 0696936..f2e10fa 100644 --- a/index.ts +++ b/index.ts @@ -119,7 +119,7 @@ const hasChangesetBeenAdded = ( ), ); -export default (app: Probot): void => { +export default (app: Probot) => { void app.auth(); app.log("Yay, the app was loaded!"); diff --git a/test/index.test.ts b/test/index.test.ts index f4f7e43..32a6285 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -40,16 +40,16 @@ function setupMswServer() { const server = setupMswServer(); // Probot validates the privateKey locally -// So we must generate a valid key +// so we must generate a valid key const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048, privateKeyEncoding: { - format: "pem", type: "pkcs8", + format: "pem", }, publicKeyEncoding: { - format: "pem", type: "spki", + format: "pem", }, }); @@ -266,16 +266,16 @@ describe.concurrent("changeset-bot", () => { it("should update a comment when there is a comment", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { + files: { + ...baseFiles, + ".changeset/something/changes.md": [{ status: "added" }, "---\n---\n"], + }, comments: [ { id: 7, user: { login: "changeset-bot[bot]" }, }, ], - files: { - ...baseFiles, - ".changeset/something/changes.md": [{ status: "added" }, "---\n---\n"], - }, }); await probot.receive({ @@ -320,11 +320,11 @@ describe.concurrent("changeset-bot", () => { it("should show correct message if there is a changeset", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { - comments: [], files: { ...baseFiles, ".changeset/something/changes.md": [{ status: "added" }, "---\n---\n"], }, + comments: [], }); await probot.receive({ @@ -366,11 +366,11 @@ describe.concurrent("changeset-bot", () => { it("should show correct message if there is no changeset", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { - comments: [], files: { ...baseFiles, "index.js": [{ status: "added" }, "console.log('test');"], }, + comments: [], }); await probot.receive({ @@ -411,7 +411,6 @@ describe.concurrent("changeset-bot", () => { it("uses the root package when no workspace tool is detected", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { - comments: [], files: { ".changeset/config.json": JSON.stringify({}), "package.json": JSON.stringify({ @@ -419,6 +418,7 @@ describe.concurrent("changeset-bot", () => { }), "src/index.ts": [{ status: "added" }, "export {};"], }, + comments: [], }); await probot.receive({ @@ -463,7 +463,6 @@ describe.concurrent("changeset-bot", () => { }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { - comments: [], files: { ".changeset/config.json": JSON.stringify({}), "package.json": JSON.stringify({ @@ -478,6 +477,7 @@ describe.concurrent("changeset-bot", () => { name: "pkg-b", }), }, + comments: [], }); await probot.receive({ @@ -522,7 +522,6 @@ describe.concurrent("changeset-bot", () => { }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { - comments: [], files: { ".changeset/config.json": JSON.stringify({}), "package.json": JSON.stringify({ @@ -537,6 +536,7 @@ describe.concurrent("changeset-bot", () => { name: "pkg-ab", }), }, + comments: [], }); await probot.receive({ @@ -578,7 +578,6 @@ describe.concurrent("changeset-bot", () => { it("detects pnpm workspaces when building the add-changeset link", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { - comments: [], files: { ".changeset/config.json": JSON.stringify({}), "package.json": JSON.stringify({ @@ -590,6 +589,7 @@ describe.concurrent("changeset-bot", () => { }), "pnpm-workspace.yaml": "packages:\n - packages/*\n", }, + comments: [], }); await probot.receive({ @@ -634,7 +634,6 @@ describe.concurrent("changeset-bot", () => { }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { - comments: [], files: { ...baseFiles, ".changeset/abc123.md": [ @@ -653,6 +652,7 @@ add feature version: "1.0.0", }), }, + comments: [], }); await probot.receive({ @@ -696,8 +696,8 @@ add feature it("shouldn't add a comment to a release pull request", async ({ expect, task }) => { const probot = setupProbot(task.id); const { requests } = usePrState(server, { - comments: [], files: baseFiles, + comments: [], }); await probot.receive({