From 35ab8ffef7b755c899d9e19d61277a4fceb760d1 Mon Sep 17 00:00:00 2001 From: lauren Date: Wed, 16 Apr 2025 17:48:38 -0400 Subject: [PATCH 1/6] [mcp] Add inspect script (#32928) Uses https://github.com/modelcontextprotocol/inspector to inspect and debug the mcp server. `yarn workspace react-mcp-server dev` will build the server in watch mode and launch the inspector. Default address is http://127.0.0.1:6274. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32928). * #32932 * #32931 * #32930 * #32929 * __->__ #32928 --- compiler/packages/react-mcp-server/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compiler/packages/react-mcp-server/package.json b/compiler/packages/react-mcp-server/package.json index 49e5a542d6..d4c270513a 100644 --- a/compiler/packages/react-mcp-server/package.json +++ b/compiler/packages/react-mcp-server/package.json @@ -8,6 +8,8 @@ "scripts": { "build": "rimraf dist && tsup", "test": "echo 'no tests'", + "dev": "concurrently --kill-others -n build,inspect \"yarn run watch\" \"wait-on dist/index.js && yarn run inspect\"", + "inspect": "npx @modelcontextprotocol/inspector node dist/index.js", "watch": "yarn build --watch" }, "dependencies": { From fc21d5a7db4f714825111e365825804f478e093a Mon Sep 17 00:00:00 2001 From: lauren Date: Wed, 16 Apr 2025 17:48:53 -0400 Subject: [PATCH 2/6] [mcp] Dedupe docs (#32929) Previously the resource would return a bunch of dupes because the algolia results would return multiple hashes (headings) for the same url. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32929). * #32932 * #32931 * #32930 * __->__ #32929 * #32928 --- .../packages/react-mcp-server/src/index.ts | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/compiler/packages/react-mcp-server/src/index.ts b/compiler/packages/react-mcp-server/src/index.ts index 9f81de88ce..fbe5f58f66 100644 --- a/compiler/packages/react-mcp-server/src/index.ts +++ b/compiler/packages/react-mcp-server/src/index.ts @@ -40,14 +40,30 @@ const server = new McpServer({ version: '0.0.0', }); +function slugify(heading: string): string { + return heading + .split(' ') + .map(w => w.toLowerCase()) + .join('-'); +} + // TODO: how to verify this works? server.resource( 'docs', new ResourceTemplate('docs://{message}', {list: undefined}), - async (uri, {message}) => { + async (_uri, {message}) => { const hits = await queryAlgolia(message); + const deduped = new Map(); + for (const hit of hits) { + // drop hashes to dedupe properly + const u = new URL(hit.url); + if (deduped.has(u.pathname)) { + continue; + } + deduped.set(u.pathname, hit); + } const pages: Array = await Promise.all( - hits.map(hit => { + Array.from(deduped.values()).map(hit => { return fetch(hit.url, { headers: { 'User-Agent': @@ -70,16 +86,17 @@ server.resource( .filter(html => html !== null) .map(html => { const $ = cheerio.load(html); + const title = encodeURIComponent(slugify($('h1').text())); // react.dev should always have at least one
with the main content const article = $('article').html(); if (article != null) { return { - uri: uri.href, + uri: `docs://${title}`, text: turndownService.turndown(article), }; } else { return { - uri: uri.href, + uri: `docs://${title}`, // Fallback to converting the whole page to markdown text: turndownService.turndown($.html()), }; From 3e04b2a214cdc962dd5acde412c7107321ec7a56 Mon Sep 17 00:00:00 2001 From: lauren Date: Wed, 16 Apr 2025 17:49:04 -0400 Subject: [PATCH 3/6] [mcp] Refine passes returned (#32930) Adds some new options to request the HIR, ReactiveFunction passes --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32930). * #32932 * #32931 * __->__ #32930 * #32929 * #32928 --- .../react-mcp-server/src/compiler/index.ts | 10 ++ .../packages/react-mcp-server/src/index.ts | 91 ++++++++++++++++--- .../src/utils/assertExhaustive.ts | 13 +++ 3 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 compiler/packages/react-mcp-server/src/utils/assertExhaustive.ts diff --git a/compiler/packages/react-mcp-server/src/compiler/index.ts b/compiler/packages/react-mcp-server/src/compiler/index.ts index 8b8e494ccc..0da206bd02 100644 --- a/compiler/packages/react-mcp-server/src/compiler/index.ts +++ b/compiler/packages/react-mcp-server/src/compiler/index.ts @@ -14,6 +14,16 @@ import * as prettier from 'prettier'; export let lastResult: BabelCore.BabelFileResult | null = null; +export type PrintedCompilerPipelineValue = + | { + kind: 'hir'; + name: string; + fnName: string | null; + value: string; + } + | {kind: 'reactive'; name: string; fnName: string | null; value: string} + | {kind: 'debug'; name: string; fnName: string | null; value: string}; + type CompileOptions = { text: string; file: string; diff --git a/compiler/packages/react-mcp-server/src/index.ts b/compiler/packages/react-mcp-server/src/index.ts index fbe5f58f66..345e25881e 100644 --- a/compiler/packages/react-mcp-server/src/index.ts +++ b/compiler/packages/react-mcp-server/src/index.ts @@ -11,7 +11,7 @@ import { } from '@modelcontextprotocol/sdk/server/mcp.js'; import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; import {z} from 'zod'; -import {compile} from './compiler'; +import {compile, type PrintedCompilerPipelineValue} from './compiler'; import { CompilerPipelineValue, printReactiveFunctionWithOutlined, @@ -22,19 +22,9 @@ import { import * as cheerio from 'cheerio'; import TurndownService from 'turndown'; import {queryAlgolia} from './utils/algolia'; +import assertExhaustive from './utils/assertExhaustive'; const turndownService = new TurndownService(); - -export type PrintedCompilerPipelineValue = - | { - kind: 'hir'; - name: string; - fnName: string | null; - value: string; - } - | {kind: 'reactive'; name: string; fnName: string | null; value: string} - | {kind: 'debug'; name: string; fnName: string | null; value: string}; - const server = new McpServer({ name: 'React', version: '0.0.0', @@ -114,7 +104,7 @@ server.tool( 'Compile code with React Compiler. Optionally, for debugging provide a pass name like "HIR" to see more information.', { text: z.string(), - passName: z.string().optional(), + passName: z.enum(['HIR', 'ReactiveFunction', 'All', '@DEBUG']).optional(), }, async ({text, passName}) => { const pipelinePasses = new Map< @@ -164,8 +154,7 @@ server.tool( break; } default: { - const _: never = result; - throw new Error(`Unhandled result ${result}`); + assertExhaustive(result, `Unhandled result ${result}`); } } }; @@ -205,6 +194,78 @@ server.tool( } const requestedPasses: Array<{type: 'text'; text: string}> = []; if (passName != null) { + switch (passName) { + case 'All': { + const hir = pipelinePasses.get('PropagateScopeDependenciesHIR'); + if (hir !== undefined) { + for (const pipelineValue of hir) { + requestedPasses.push({ + type: 'text' as const, + text: pipelineValue.value, + }); + } + } + const reactiveFunc = pipelinePasses.get('PruneHoistedContexts'); + if (reactiveFunc !== undefined) { + for (const pipelineValue of reactiveFunc) { + requestedPasses.push({ + type: 'text' as const, + text: pipelineValue.value, + }); + } + } + break; + } + case 'HIR': { + // Last pass before HIR -> ReactiveFunction + const requestedPass = pipelinePasses.get( + 'PropagateScopeDependenciesHIR', + ); + if (requestedPass !== undefined) { + for (const pipelineValue of requestedPass) { + requestedPasses.push({ + type: 'text' as const, + text: pipelineValue.value, + }); + } + } else { + console.error(`Could not find requested pass ${passName}`); + } + break; + } + case 'ReactiveFunction': { + // Last pass + const requestedPass = pipelinePasses.get('PruneHoistedContexts'); + if (requestedPass !== undefined) { + for (const pipelineValue of requestedPass) { + requestedPasses.push({ + type: 'text' as const, + text: pipelineValue.value, + }); + } + } else { + console.error(`Could not find requested pass ${passName}`); + } + break; + } + case '@DEBUG': { + for (const [, pipelinePass] of pipelinePasses) { + for (const pass of pipelinePass) { + requestedPasses.push({ + type: 'text' as const, + text: `${pass.name}\n\n${pass.value}`, + }); + } + } + break; + } + default: { + assertExhaustive( + passName, + `Unhandled passName option: ${passName}`, + ); + } + } const requestedPass = pipelinePasses.get(passName); if (requestedPass !== undefined) { for (const pipelineValue of requestedPass) { diff --git a/compiler/packages/react-mcp-server/src/utils/assertExhaustive.ts b/compiler/packages/react-mcp-server/src/utils/assertExhaustive.ts new file mode 100644 index 0000000000..2adfffa8d7 --- /dev/null +++ b/compiler/packages/react-mcp-server/src/utils/assertExhaustive.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Trigger an exhaustiveness check in TypeScript and throw at runtime. + */ +export default function assertExhaustive(_: never, errorMsg: string): never { + throw new Error(errorMsg); +} From 3c75bf21dd2fca130635c5b67b5361f4759a7d29 Mon Sep 17 00:00:00 2001 From: lauren Date: Wed, 16 Apr 2025 17:49:15 -0400 Subject: [PATCH 4/6] [mcp] Fix bailout loc (#32931) Use the correct loc line numbers and not [Object:object] --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32931). * #32932 * __->__ #32931 * #32930 * #32929 * #32928 --- compiler/packages/react-mcp-server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/packages/react-mcp-server/src/index.ts b/compiler/packages/react-mcp-server/src/index.ts index 345e25881e..e577128f86 100644 --- a/compiler/packages/react-mcp-server/src/index.ts +++ b/compiler/packages/react-mcp-server/src/index.ts @@ -283,7 +283,7 @@ server.tool( if (typeof err.loc !== 'symbol') { return { type: 'text' as const, - text: `React Compiler bailed out: ${err.message}@${err.loc.start}:${err.loc.end}`, + text: `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`, }; } return null; From 95ff37f5f5ee7c756f844aea2947e961e7151ac9 Mon Sep 17 00:00:00 2001 From: lauren Date: Wed, 16 Apr 2025 17:49:25 -0400 Subject: [PATCH 5/6] [mcp] Iterate on prompt (#32932) v2 --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32932). * __->__ #32932 * #32931 * #32930 * #32929 * #32928 --- .../packages/react-mcp-server/src/index.ts | 98 ++++++------------- 1 file changed, 32 insertions(+), 66 deletions(-) diff --git a/compiler/packages/react-mcp-server/src/index.ts b/compiler/packages/react-mcp-server/src/index.ts index e577128f86..864b8242eb 100644 --- a/compiler/packages/react-mcp-server/src/index.ts +++ b/compiler/packages/react-mcp-server/src/index.ts @@ -307,19 +307,45 @@ server.tool( }, ); -server.prompt('review-code', {code: z.string()}, ({code}) => ({ +server.prompt('review-react-code', () => ({ messages: [ { role: 'assistant', content: { type: 'text', - text: `# React Expert Assistant - + text: ` ## Role -You are a React expert assistant that helps users write more efficient and optimizable React code. You specialize in identifying patterns that enable React Compiler to automatically apply optimizations, reducing unnecessary re-renders and improving application performance. Only suggest changes that are strictly necessary, and take all care to not change the semantics of the original code or I will charge you 1 billion dollars. +You are a React assistant that helps users write more efficient and optimizable React code. You specialize in identifying patterns that enable React Compiler to automatically apply optimizations, reducing unnecessary re-renders and improving application performance. + +## Follow these guidelines in all code you produce and suggest +Use functional components with Hooks: Do not generate class components or use old lifecycle methods. Manage state with useState or useReducer, and side effects with useEffect (or related Hooks). Always prefer functions and Hooks for any new component logic. + +Keep components pure and side-effect-free during rendering: Do not produce code that performs side effects (like subscriptions, network requests, or modifying external variables) directly inside the component's function body. Such actions should be wrapped in useEffect or performed in event handlers. Ensure your render logic is a pure function of props and state. + +Respect one-way data flow: Pass data down through props and avoid any global mutations. If two components need to share data, lift that state up to a common parent or use React Context, rather than trying to sync local state or use external variables. + +Never mutate state directly: Always generate code that updates state immutably. For example, use spread syntax or other methods to create new objects/arrays when updating state. Do not use assignments like state.someValue = ... or array mutations like array.push() on state variables. Use the state setter (setState from useState, etc.) to update state. + +Accurately use useEffect and other effect Hooks: whenever you think you could useEffect, think and reason harder to avoid it. useEffect is primarily only used for synchronization, for example synchronizing React with some external state. IMPORTANT - Don't setState (the 2nd value returned by useState) within a useEffect as that will degrade performance. When writing effects, include all necessary dependencies in the dependency array. Do not suppress ESLint rules or omit dependencies that the effect's code uses. Structure the effect callbacks to handle changing values properly (e.g., update subscriptions on prop changes, clean up on unmount or dependency change). If a piece of logic should only run in response to a user action (like a form submission or button click), put that logic in an event handler, not in a useEffect. Where possible, useEffects should return a cleanup function. + +Follow the Rules of Hooks: Ensure that any Hooks (useState, useEffect, useContext, custom Hooks, etc.) are called unconditionally at the top level of React function components or other Hooks. Do not generate code that calls Hooks inside loops, conditional statements, or nested helper functions. Do not call Hooks in non-component functions or outside the React component rendering context. + +Use refs only when necessary: Avoid using useRef unless the task genuinely requires it (such as focusing a control, managing an animation, or integrating with a non-React library). Do not use refs to store application state that should be reactive. If you do use refs, never write to or read from ref.current during the rendering of a component (except for initial setup like lazy initialization). Any ref usage should not affect the rendered output directly. + +Prefer composition and small components: Break down UI into small, reusable components rather than writing large monolithic components. The code you generate should promote clarity and reusability by composing components together. Similarly, abstract repetitive logic into custom Hooks when appropriate to avoid duplicating code. + +Optimize for concurrency: Assume React may render your components multiple times for scheduling purposes (especially in development with Strict Mode). Write code that remains correct even if the component function runs more than once. For instance, avoid side effects in the component body and use functional state updates (e.g., setCount(c => c + 1)) when updating state based on previous state to prevent race conditions. Always include cleanup functions in effects that subscribe to external resources. Don't write useEffects for "do this when this changes" side-effects. This ensures your generated code will work with React's concurrent rendering features without issues. + +Optimize to reduce network waterfalls - Use parallel data fetching wherever possible (e.g., start multiple requests at once rather than one after another). Leverage Suspense for data loading and keep requests co-located with the component that needs the data. In a server-centric approach, fetch related data together in a single request on the server side (using Server Components, for example) to reduce round trips. Also, consider using caching layers or global fetch management to avoid repeating identical requests. + +Rely on React Compiler - useMemo, useCallback, and React.memo can be omitted if React Compiler is enabled. Avoid premature optimization with manual memoization. Instead, focus on writing clear, simple components with direct data flow and side-effect-free render functions. Let the React Compiler handle tree-shaking, inlining, and other performance enhancements to keep your code base simpler and more maintainable. + +Design for a good user experience - Provide clear, minimal, and non-blocking UI states. When data is loading, show lightweight placeholders (e.g., skeleton screens) rather than intrusive spinners everywhere. Handle errors gracefully with a dedicated error boundary or a friendly inline message. Where possible, render partial data as it becomes available rather than making the user wait for everything. Suspense allows you to declare the loading states in your component tree in a natural way, preventing “flash” states and improving perceived performance. + +Server Components - Shift data-heavy logic to the server whenever possible. Break up the more static parts of the app into server components. Break up data fetching into server components. Only client components (denoted by the 'use client' top level directive) need interactivity. By rendering parts of your UI on the server, you reduce the client-side JavaScript needed and avoid sending unnecessary data over the wire. Use Server Components to prefetch and pre-render data, allowing faster initial loads and smaller bundle sizes. This also helps manage or eliminate certain waterfalls by resolving data on the server before streaming the HTML (and partial React tree) to the client. ## Available Resources -- 'docs': Look up documentation from React.dev. Returns markdown as a string. +- 'docs': Look up documentation from docs://{query}. Returns markdown as a string. ## Available Tools - 'compile': Run the user's code through React Compiler. Returns optimized JS/TS code with potential diagnostics. @@ -329,7 +355,7 @@ You are a React expert assistant that helps users write more efficient and optim - Check for React anti-patterns that prevent compiler optimization - Identify unnecessary manual optimizations (useMemo, useCallback, React.memo) that the compiler can handle - Look for component structure issues that limit compiler effectiveness - - Consult React.dev docs using the 'docs' resource when necessary + - Think about each suggestion you are making and consult React docs using the docs://{query} resource for best practices 2. Use React Compiler to verify optimization potential: - Run the code through the compiler and analyze the output @@ -356,66 +382,6 @@ You are a React expert assistant that helps users write more efficient and optim - When suggesting changes, try to increase or decrease the number of cached expressions (visible in const $ = _c(n)) - Increase: more memoization coverage - Decrease: if there are unnecessary dependencies, less dependencies mean less re-rendering - -As an example: - -\`\`\` -export default function MyApp() { - return
Hello World
; -} -\`\`\` - -Results in: - -\`\`\` -import { c as _c } from "react/compiler-runtime"; -export default function MyApp() { - const $ = _c(1); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 =
Hello World
; - $[0] = t0; - } else { - t0 = $[0]; - } - return t0; -} -\`\`\` - -The code above was memoized successfully by the compiler as you can see from the injected import { c as _c } from "react/compiler-runtime"; statement. The cache size is initialized at 1 slot. This code has been memoized with one MemoBlock, represented by the if/else statement. Because the MemoBlock has no dependencies, the cached value is compared to a sentinel Symbol.for("react.memo_cache_sentinel") value once and then cached forever. - -Here's an example of code that results in a MemoBlock with one dependency, as you can see by the comparison against the name prop: - -\`\`\`js -export default function MyApp({name}) { - return
Hello World, {name}
; -} -\`\`\` - -\`\`\`js -import { c as _c } from "react/compiler-runtime"; -export default function MyApp(t0) { - const $ = _c(2); - const { name } = t0; - let t1; - if ($[0] !== name) { - t1 =
Hello World, {name}
; - $[0] = name; - $[1] = t1; - } else { - t1 = $[1]; - } - return t1; -} -\`\`\` - -## Example 1: - -## Example 2: - -Review the following code: - -${code} `, }, }, From 984650ce764d9eab2ca5e202cf425c996e8e1fe6 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Wed, 16 Apr 2025 17:50:59 -0400 Subject: [PATCH 6/6] [ci] Fix check_access again I can see the value being output and set correctly but not sure why it's skipping the 2nd job. --- .github/workflows/compiler_discord_notify.yml | 10 +++++----- .github/workflows/runtime_discord_notify.yml | 10 +++++----- .github/workflows/shared_label_core_team_prs.yml | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/compiler_discord_notify.yml b/.github/workflows/compiler_discord_notify.yml index 21202a8004..71aea56e84 100644 --- a/.github/workflows/compiler_discord_notify.yml +++ b/.github/workflows/compiler_discord_notify.yml @@ -13,15 +13,15 @@ jobs: check_access: runs-on: ubuntu-latest outputs: - is_member_or_collaborator: ${{ steps.check_access.outputs.is_member_or_collaborator }} + is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }} steps: - - name: Check access - id: check_access + - name: Check is member or collaborator + id: check_is_member_or_collaborator if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }} - run: echo "is_member_or_collaborator='true'" >> "$GITHUB_OUTPUT" + run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT" check_maintainer: - if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' }} + if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }} needs: [check_access] uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main permissions: diff --git a/.github/workflows/runtime_discord_notify.yml b/.github/workflows/runtime_discord_notify.yml index c3240ef88e..44775fbe78 100644 --- a/.github/workflows/runtime_discord_notify.yml +++ b/.github/workflows/runtime_discord_notify.yml @@ -13,15 +13,15 @@ jobs: check_access: runs-on: ubuntu-latest outputs: - is_member_or_collaborator: ${{ steps.check_access.outputs.is_member_or_collaborator }} + is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }} steps: - - name: Check access - id: check_access + - name: Check is member or collaborator + id: check_is_member_or_collaborator if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }} - run: echo "is_member_or_collaborator='true'" >> "$GITHUB_OUTPUT" + run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT" check_maintainer: - if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' }} + if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }} needs: [check_access] uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main permissions: diff --git a/.github/workflows/shared_label_core_team_prs.yml b/.github/workflows/shared_label_core_team_prs.yml index df75ff5042..73e7261f56 100644 --- a/.github/workflows/shared_label_core_team_prs.yml +++ b/.github/workflows/shared_label_core_team_prs.yml @@ -14,15 +14,15 @@ jobs: check_access: runs-on: ubuntu-latest outputs: - is_member_or_collaborator: ${{ steps.check_access.outputs.is_member_or_collaborator }} + is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }} steps: - - name: Check access - id: check_access + - name: Check is member or collaborator + id: check_is_member_or_collaborator if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }} - run: echo "is_member_or_collaborator='true'" >> "$GITHUB_OUTPUT" + run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT" check_maintainer: - if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' }} + if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }} needs: [check_access] uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main permissions: