diff --git a/.eslintrc.js b/.eslintrc.js index 6ece359..e67f6d3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,20 +29,30 @@ module.exports = { }, { - files: ['src/cli.ts'], + files: ['src/cli.ts', '.github/scripts/**/*.ts'], parserOptions: { sourceType: 'script', }, rules: { - // It's okay if this file has a shebang; it's meant to be executed - // directly. + // These are scripts and are meant to have shebangs. 'n/shebang': 'off', }, }, + + { + files: ['.github/scripts/**/*.ts'], + parserOptions: { + sourceType: 'script', + }, + rules: { + 'n/no-process-env': 'off', + }, + }, ], ignorePatterns: [ '!.eslintrc.js', + '!.github/', '!.prettierrc.js', '.yarn/', 'dist/', diff --git a/.github/scripts/determine-initial-slack-payload.ts b/.github/scripts/determine-initial-slack-payload.ts new file mode 100755 index 0000000..ed87b9b --- /dev/null +++ b/.github/scripts/determine-initial-slack-payload.ts @@ -0,0 +1,265 @@ +#!/usr/bin/env tsx + +import { setOutput } from '@actions/core'; +import fs from 'fs'; +import path from 'path'; + +type Inputs = { + githubRepository: string; + githubRunId: string; + moduleLintRunsDirectory: string; + channelId: string; + isRunningOnCi: boolean; +}; + +/** + * Obtains the inputs for this script from environment variables. + * + * @returns The inputs for this script. + */ +function getInputs(): Inputs { + return { + githubRepository: requireEnvironmentVariable('GITHUB_REPOSITORY'), + githubRunId: requireEnvironmentVariable('GITHUB_RUN_ID'), + moduleLintRunsDirectory: requireEnvironmentVariable( + 'MODULE_LINT_RUNS_DIRECTORY', + ), + channelId: requireEnvironmentVariable('SLACK_CHANNEL_ID'), + isRunningOnCi: process.env.CI !== undefined, + }; +} + +/** + * Obtains the given environment variable, throwing if it has not been set. + * + * @param variableName - The name of the desired environment variable. + * @returns The value of the given environment variable. + * @throws if the given environment variable has not been set. + */ +function requireEnvironmentVariable(variableName: string): string { + const value = process.env[variableName]; + + if (value === undefined || value === '') { + throw new Error(`Missing environment variable ${variableName}.`); + } + + return value; +} + +/** + * Reads the exit code files produced from previous `module-lint` runs. + * + * @param moduleLintRunsDirectory - The directory that holds the exit code + * files. + * @returns An array of exit codes. + */ +async function readModuleLintExitCodeFiles(moduleLintRunsDirectory: string) { + const entryNames = (await fs.promises.readdir(moduleLintRunsDirectory)).map( + (entryName) => path.join(moduleLintRunsDirectory, entryName), + ); + const exitCodeFileNames = entryNames.filter((entry) => + entry.endsWith('--exitcode.txt'), + ); + return Promise.all( + exitCodeFileNames.map(async (exitCodeFileName) => { + const content = ( + await fs.promises.readFile(exitCodeFileName, 'utf8') + ).trim(); + const exitCode = Number(content); + if (Number.isNaN(exitCode)) { + throw new Error(`Could not parse '${content}' as exit code`); + } + return exitCode; + }), + ); +} + +/** + * Generates the Slack message that will appear when all `module-lint` runs are + * successful. + * + * @returns The Slack payload blocks. + */ +function constructSlackPayloadBlocksForSuccessfulRun() { + return [ + { + type: 'rich_text', + elements: [ + { + type: 'rich_text_section', + elements: [ + { + type: 'emoji', + name: 'package', + }, + { + type: 'text', + text: ' ', + }, + { + type: 'text', + text: 'A new package standardization report is available.', + style: { + bold: true, + }, + }, + { + type: 'text', + text: '\n\nGreat work! Your team has ', + }, + { + type: 'text', + text: '5 repositories', + style: { + bold: true, + }, + }, + { + type: 'text', + text: ' that fully align with the module template.\n\n', + }, + { + type: 'text', + text: 'Open this thread to view more details:', + }, + { + type: 'emoji', + name: 'point_right', + }, + ], + }, + ], + }, + ]; +} + +/** + * Generates the Slack message that will appear when some `module-lint` runs are + * unsuccessful. + * + * @param inputs - The inputs to this script. + * @returns The Slack payload blocks. + */ +function constructSlackPayloadBlocksForUnSuccessfulRun(inputs: Inputs) { + return [ + { + type: 'rich_text', + elements: [ + { + type: 'rich_text_section', + elements: [ + { + type: 'emoji', + name: 'package', + }, + { + type: 'text', + text: ' ', + }, + { + type: 'text', + text: 'A new package standardization report is available.', + style: { + bold: true, + }, + }, + { + type: 'text', + text: '\n\nYour team has ', + }, + { + type: 'text', + text: '4 repositories', + style: { + bold: true, + }, + }, + { + type: 'text', + text: ' that require maintenance in order to align with the module template. This is important for maintaining conventions across MetaMask and adhering to our security principles.\n\n', + }, + { + type: 'link', + text: 'View this run', + url: `https://github.com/${inputs.githubRepository}/actions/runs/${inputs.githubRunId}`, + }, + { + type: 'text', + text: ', or open this thread to view more details:', + }, + { + type: 'emoji', + name: 'point_right', + }, + ], + }, + ], + }, + ]; +} + +/** + * Constructs the payload that will be used to post a message in Slack + * containing information about previous `module-lint` runs. + * + * @param inputs - The inputs to this script. + * @param allModuleLintRunsSuccessful - Whether all of the previously linted projects passed + * lint. + * @returns The Slack payload. + */ +function constructSlackPayload( + inputs: Inputs, + allModuleLintRunsSuccessful: boolean, +) { + const text = + 'A new package standardization report is available. Open this thread to view more details.'; + + const blocks = allModuleLintRunsSuccessful + ? constructSlackPayloadBlocksForSuccessfulRun() + : constructSlackPayloadBlocksForUnSuccessfulRun(inputs); + + if (inputs.isRunningOnCi) { + return { + text, + blocks, + // The Slack API dictates use of this property. + // eslint-disable-next-line @typescript-eslint/naming-convention + icon_url: + 'https://raw.githubusercontent.com/MetaMask/action-npm-publish/main/robo.png', + username: 'MetaMask Bot', + channel: inputs.channelId, + }; + } + + return { blocks }; +} + +/** + * The entrypoint for this script. + */ +async function main() { + const inputs = getInputs(); + + const exitCodes = await readModuleLintExitCodeFiles( + inputs.moduleLintRunsDirectory, + ); + const allModuleLintRunsSuccessful = exitCodes.every( + (exitCode) => exitCode === 0, + ); + + const slackPayload = constructSlackPayload( + inputs, + allModuleLintRunsSuccessful, + ); + + // Offer two different ways of outputting the Slack payload so that this + // script can be run locally and the output can be fed into Slack's Block Kit + // Builder + if (inputs.isRunningOnCi) { + setOutput('SLACK_PAYLOAD', JSON.stringify(slackPayload)); + } else { + console.log(JSON.stringify(slackPayload, null, ' ')); + } +} + +main().catch(console.error); diff --git a/.github/scripts/determine-slack-payload-for-package.ts b/.github/scripts/determine-slack-payload-for-package.ts new file mode 100755 index 0000000..7e1c364 --- /dev/null +++ b/.github/scripts/determine-slack-payload-for-package.ts @@ -0,0 +1,203 @@ +#!/usr/bin/env tsx + +import { setOutput } from '@actions/core'; +import fs from 'fs'; +import path from 'path'; + +type Inputs = { + projectName: string; + moduleLintRunsDirectory: string; + channelId: string; + threadTs: string; + isRunningOnCi: boolean; +}; + +type ParsedModuleLintOutput = { + passed: number; + failed: number; + errored: number; + total: number; + durationWithUnit: string; + percentage: number; +}; + +/** + * Obtains the inputs for this script from environment variables. + * + * @returns The inputs for this script. + */ +function getInputs(): Inputs { + return { + projectName: requireEnvironmentVariable('PROJECT_NAME'), + moduleLintRunsDirectory: requireEnvironmentVariable( + 'MODULE_LINT_RUNS_DIRECTORY', + ), + channelId: requireEnvironmentVariable('SLACK_CHANNEL_ID'), + threadTs: requireEnvironmentVariable('SLACK_THREAD_TS'), + isRunningOnCi: process.env.CI !== undefined, + }; +} + +/** + * Obtains the given environment variable, throwing if it has not been set. + * + * @param variableName - The name of the desired environment variable. + * @returns The value of the given environment variable. + * @throws if the given environment variable has not been set. + */ +function requireEnvironmentVariable(variableName: string): string { + const value = process.env[variableName]; + + if (value === undefined || value === '') { + throw new Error(`Missing environment variable ${variableName}.`); + } + + return value; +} + +/** + * Reads the output file produced by a previous `module-lint` run for the + * project in question. + * + * @param projectName - The name of the project previously run. + * @param moduleLintRunsDirectory - The directory where the output file was + * stored. + * @returns The content of the file (with leading and trailing whitespace + * removed). + */ +async function readModuleLintOutputFile( + projectName: string, + moduleLintRunsDirectory: string, +) { + const content = await fs.promises.readFile( + path.join(moduleLintRunsDirectory, `${projectName}--output.txt`), + 'utf8', + ); + return content.trim(); +} + +/** + * Parses the output from a previous `module-lint` run into parts that can be + * used to construct a Slack message. + * + * @param moduleLintOutput - The output from a previous `module-lint` run. + * @returns The pieces of the `module-lint` output. + */ +function parseModuleLintOutput( + moduleLintOutput: string, +): ParsedModuleLintOutput { + const moduleLintOutputSections = moduleLintOutput.split('\n\n'); + + const summarySection = + moduleLintOutputSections[moduleLintOutputSections.length - 1]; + if (summarySection === undefined) { + throw new Error("Couldn't parse module-lint report output"); + } + + const [reportSummaryLine, elapsedTimeLine] = summarySection.split('\n'); + if (reportSummaryLine === undefined || elapsedTimeLine === undefined) { + throw new Error("Couldn't parse module-lint report output"); + } + + const reportSummary = reportSummaryLine.replace(/^Results:[ ]+/u, ''); + const reportSummaryMatch = reportSummary.match( + /(\d+) passed, (\d+) failed, (\d+) errored, (\d+) total/u, + ); + if (!reportSummaryMatch) { + throw new Error("Couldn't parse module-lint report output"); + } + + const passed = Number(reportSummaryMatch[1]); + const failed = Number(reportSummaryMatch[2]); + const errored = Number(reportSummaryMatch[3]); + const total = Number(reportSummaryMatch[4]); + const durationWithUnit = elapsedTimeLine.replace(/^Elapsed time:[ ]+/u, ''); + + const percentage = Math.round((passed / total) * 1000) / 10; + + return { passed, failed, errored, total, durationWithUnit, percentage }; +} + +/** + * Constructs the payload that will be used to post a message in Slack + * containing information about the `module-lint` run for the package in + * question. + * + * @param inputs - The inputs to this script. + * @param moduleLintOutput - The output from a previous `module-lint` run. + * @returns The Slack payload. + */ +function constructSlackPayload(inputs: Inputs, moduleLintOutput: string) { + const { passed, total, percentage } = parseModuleLintOutput(moduleLintOutput); + + const text = `Report for MetaMask/${inputs.projectName}`; + + const blocks = [ + { + type: 'rich_text', + elements: [ + { + type: 'rich_text_section', + elements: [ + { + type: 'text', + text: `Results for MetaMask/${inputs.projectName}:`, + style: { + bold: true, + }, + }, + { + type: 'text', + text: '\n\n', + }, + { + type: 'emoji', + name: passed === total ? 'white_check_mark' : 'x', + }, + { + type: 'text', + text: ` ${passed}/${total} rules passed (${percentage}% alignment with template).\n\n`, + }, + ], + }, + ], + }, + ]; + + if (inputs.isRunningOnCi) { + return { + text, + blocks, + channel: inputs.channelId, + // The Slack API dictates use of this property. + // eslint-disable-next-line @typescript-eslint/naming-convention + thread_ts: inputs.threadTs, + }; + } + return { blocks }; +} + +/** + * The entrypoint for this script. + */ +async function main() { + const inputs = getInputs(); + + const moduleLintOutput = await readModuleLintOutputFile( + inputs.projectName, + inputs.moduleLintRunsDirectory, + ); + + const slackPayload = constructSlackPayload(inputs, moduleLintOutput); + + // Offer two different ways of outputting the Slack payload so that this + // script can be run locally and the output can be fed into Slack's Block Kit + // Builder + if (inputs.isRunningOnCi) { + setOutput('SLACK_PAYLOAD', JSON.stringify(slackPayload)); + } else { + console.log(JSON.stringify(slackPayload, null, ' ')); + } +} + +main().catch(console.error); diff --git a/.github/scripts/run-module-lint.sh b/.github/scripts/run-module-lint.sh new file mode 100755 index 0000000..fd25c71 --- /dev/null +++ b/.github/scripts/run-module-lint.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +if [[ -z "$PROJECT_NAME" ]]; then + echo "Missing PROJECT_NAME." + exit 1 +fi + +if [[ -z "$MODULE_LINT_RUNS_DIRECTORY" ]]; then + echo "Missing MODULE_LINT_RUNS_DIRECTORY." + exit 1 +fi + +mkdir -p "$MODULE_LINT_RUNS_DIRECTORY" + +yarn run-tool "$PROJECT_NAME" > "$MODULE_LINT_RUNS_DIRECTORY/$PROJECT_NAME--output.txt" + +exitcode=$? + +echo $exitcode > "$MODULE_LINT_RUNS_DIRECTORY/$PROJECT_NAME--exitcode.txt" + +cat "$MODULE_LINT_RUNS_DIRECTORY/$PROJECT_NAME--output.txt" + +if [[ $exitcode -ne 0 && $exitcode -ne 100 ]]; then + exit 1 +else + exit 0 +fi diff --git a/.github/workflows/generate-periodic-report.yml b/.github/workflows/generate-periodic-report.yml new file mode 100644 index 0000000..dab1ef8 --- /dev/null +++ b/.github/workflows/generate-periodic-report.yml @@ -0,0 +1,119 @@ +name: Periodically run module-lint and report results + +# Runs at minute 10 of the 1st hour of every Sunday +on: + schedule: + - cron: '10 1 * * 0' + workflow_dispatch: + +jobs: + run-module-lint: + name: Run module-lint + runs-on: ubuntu-latest + strategy: + matrix: + project: + - auto-changelog + - create-release-branch + - module-lint + - utils + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install dependencies + run: yarn --immutable + - name: Run module-lint + id: module-lint + run: .github/scripts/run-module-lint.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PROJECT_NAME: ${{ matrix.project }} + MODULE_LINT_RUNS_DIRECTORY: /tmp/module-lint-runs + - name: Save report + uses: actions/upload-artifact@v4 + with: + name: module-lint-report--${{ matrix.project }} + path: /tmp/module-lint-runs + post-initial-message-to-slack: + name: Post initial message to Slack + needs: run-module-lint + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install dependencies + run: yarn --immutable + - name: Load results for previous module-lint runs + uses: actions/download-artifact@v4 + with: + path: /tmp/module-lint-runs + pattern: module-lint-report--* + merge-multiple: true + - name: Determine Slack payload + id: slack-payload + run: yarn tsx ./.github/scripts/determine-initial-slack-payload.ts + env: + MODULE_LINT_RUNS_DIRECTORY: /tmp/module-lint-runs + SLACK_CHANNEL_ID: C074PNYH0R4 # Channel: #wallet-framework-automations + - name: Post to Slack + id: initial-slack-message + uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844 + with: + channel-id: C074PNYH0R4 # Channel: #wallet-framework-automations + payload: ${{ steps.slack-payload.outputs.SLACK_PAYLOAD }} + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + outputs: + slack-thread-ts: ${{ steps.initial-slack-message.outputs.ts }} + post-threaded-message-to-slack: + name: Post threaded message to Slack + needs: post-initial-message-to-slack + runs-on: ubuntu-latest + strategy: + matrix: + project: + - auto-changelog + - create-release-branch + - module-lint + - utils + steps: + - name: Load report for previous module-lint-run + id: load-report + uses: actions/download-artifact@v4 + with: + name: module-lint-report--${{ matrix.project }} + path: /tmp/module-lint-runs + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install dependencies + run: yarn --immutable + - name: Determine Slack payload + id: slack-payload + run: yarn tsx ./.github/scripts/determine-slack-payload-for-package.ts + env: + PROJECT_NAME: ${{ matrix.project }} + MODULE_LINT_RUNS_DIRECTORY: ${{ steps.load-report.outputs.download-path }} + SLACK_CHANNEL_ID: C074PNYH0R4 # Channel: #wallet-framework-automations + SLACK_THREAD_TS: ${{ needs.post-initial-message-to-slack.outputs.slack-thread-ts }} + - name: Post to Slack + uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844 + with: + channel-id: C074PNYH0R4 # Channel: #wallet-framework-automations + payload: ${{ steps.slack-payload.outputs.SLACK_PAYLOAD }} + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.github/workflows/module-lint.yml b/.github/workflows/module-lint.yml deleted file mode 100644 index 3878714..0000000 --- a/.github/workflows/module-lint.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Module Lint - -# runs at minute 10 of the 1st hour of every Sunday -on: - schedule: - - cron: '10 1 * * 0' - -jobs: - module-lint: - name: Module Lint - runs-on: ubuntu-latest - strategy: - matrix: - projects: - [ - { name: 'auto-changelog', team: 'S051YKC9350' }, - { name: 'create-release-branch', team: 'S051YKC9350' }, - { name: 'module-lint', team: 'S051YKC9350' }, - { name: 'utils', team: 'S051YKC9350' }, - ] - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version-file: '.nvmrc' - cache: 'yarn' - - run: yarn --immutable - - name: Run module-lint - id: module-lint - run: yarn run-tool ${{ matrix.projects.name }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Require clean working directory - shell: bash - run: | - if ! git diff --exit-code; then - echo "Working tree dirty at end of job" - exit 1 - fi - - name: Post module lint report to a slack - id: slack - if: ${{ failure() && steps.module-lint.outcome == 'failure' }} - uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844 - with: - payload: | - { - "text": " Module Lint failed for the `${{ matrix.projects.name }}` repository 😱.\n for details!", - "icon_url": "https://raw.githubusercontent.com/MetaMask/action-npm-publish/main/robo.png", - "username": "MetaMask bot", - "channel": "#temp-test-module-lint" - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK diff --git a/package.json b/package.json index dc18a1a..0eec5bb 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "yargs": "^17.7.2" }, "devDependencies": { + "@actions/core": "^1.10.1", "@lavamoat/allow-scripts": "^2.3.1", "@lavamoat/preinstall-always-fail": "^1.0.0", "@metamask/auto-changelog": "^3.1.0", diff --git a/src/cli.ts b/src/cli.ts index f038910..fc6d0f5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,7 +17,7 @@ main({ }) .then((isSuccessful) => { if (!isSuccessful) { - process.exitCode = 1; + process.exitCode = 100; } }) .catch((error) => { diff --git a/tsconfig.json b/tsconfig.json index f2a40ef..1629ef5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,6 @@ "strict": true, "target": "es2020" }, - "exclude": ["./dist/**/*"] + "include": [".github/scripts/**/*.ts", "./src/**/*.ts"], + "exclude": ["./dist"] } diff --git a/yarn.lock b/yarn.lock index 55b0959..cacd197 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,26 @@ __metadata: languageName: node linkType: hard +"@actions/core@npm:^1.10.1": + version: 1.10.1 + resolution: "@actions/core@npm:1.10.1" + dependencies: + "@actions/http-client": ^2.0.1 + uuid: ^8.3.2 + checksum: 96524c2725e70e3c3176b4e4d93a1358a86f3c5ca777db9a2f65eadfa672f00877db359bf60fffc416c33838ffb4743db93bcc5bf53e76199dd28bf7f7ff8e80 + languageName: node + linkType: hard + +"@actions/http-client@npm:^2.0.1": + version: 2.2.1 + resolution: "@actions/http-client@npm:2.2.1" + dependencies: + tunnel: ^0.0.6 + undici: ^5.25.4 + checksum: c51c003cd697289136c0e81c0f9b8e57a9bb1a038dc7c9a91a71c02f4ae5e27ef7d3e305aefa7c815604049209d114c06e9991a5c5eaa055508519329267f962 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0": version: 2.2.1 resolution: "@ampproject/remapping@npm:2.2.1" @@ -669,6 +689,13 @@ __metadata: languageName: node linkType: hard +"@fastify/busboy@npm:^2.0.0": + version: 2.1.1 + resolution: "@fastify/busboy@npm:2.1.1" + checksum: 42c32ef75e906c9a4809c1e1930a5ca6d4ddc8d138e1a8c8ba5ea07f997db32210617d23b2e4a85fe376316a41a1a0439fc6ff2dedf5126d96f45a9d80754fb2 + languageName: node + linkType: hard + "@gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -1134,6 +1161,7 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/module-lint@workspace:." dependencies: + "@actions/core": ^1.10.1 "@lavamoat/allow-scripts": ^2.3.1 "@lavamoat/preinstall-always-fail": ^1.0.0 "@metamask/auto-changelog": ^3.1.0 @@ -7056,6 +7084,13 @@ __metadata: languageName: node linkType: hard +"tunnel@npm:^0.0.6": + version: 0.0.6 + resolution: "tunnel@npm:0.0.6" + checksum: c362948df9ad34b649b5585e54ce2838fa583aa3037091aaed66793c65b423a264e5229f0d7e9a95513a795ac2bd4cb72cda7e89a74313f182c1e9ae0b0994fa + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -7145,6 +7180,15 @@ __metadata: languageName: node linkType: hard +"undici@npm:^5.25.4": + version: 5.28.4 + resolution: "undici@npm:5.28.4" + dependencies: + "@fastify/busboy": ^2.0.0 + checksum: a8193132d84540e4dc1895ecc8dbaa176e8a49d26084d6fbe48a292e28397cd19ec5d13bc13e604484e76f94f6e334b2bdc740d5f06a6e50c44072818d0c19f9 + languageName: node + linkType: hard + "unique-filename@npm:^1.1.1": version: 1.1.1 resolution: "unique-filename@npm:1.1.1" @@ -7193,6 +7237,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^8.3.2": + version: 8.3.2 + resolution: "uuid@npm:8.3.2" + bin: + uuid: dist/bin/uuid + checksum: 5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df + languageName: node + linkType: hard + "uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1"