diff --git a/.github/scripts/create-flaky-test-report.mjs b/.github/scripts/create-flaky-test-report.mjs new file mode 100644 index 00000000..1c8d03c9 --- /dev/null +++ b/.github/scripts/create-flaky-test-report.mjs @@ -0,0 +1,655 @@ +#!/usr/bin/env node + +// Based on the original script done by @itsyoboieltr on Extension repo + +import { Octokit } from '@octokit/rest'; +import unzipper from 'unzipper'; +import { IncomingWebhook } from '@slack/webhook'; + +const githubToken = process.env.GITHUB_TOKEN; +if (!githubToken) throw new Error('Missing GITHUB_TOKEN env var'); + +const env = { + GITHUB_TOKEN: process.env.GITHUB_TOKEN, + LOOKBACK_DAYS: parseInt(process.env.LOOKBACK_DAYS ?? '1'), + TEST_RESULTS_FILE_PATTERN: process.env.TEST_RESULTS_FILE_PATTERN || 'test-runs', + OWNER: process.env.OWNER || 'MetaMask', + REPOSITORY: process.env.REPOSITORY || 'metamask-extension', + WORKFLOW_ID: process.env.WORKFLOW_ID || 'main.yml', + BRANCH: process.env.BRANCH || 'main', + SLACK_WEBHOOK_FLAKY_TESTS: process.env.SLACK_WEBHOOK_FLAKY_TESTS || '', + TEST_REPORT_ARTIFACTS: process.env.TEST_REPORT_ARTIFACTS + ? process.env.TEST_REPORT_ARTIFACTS.split(',').map(name => name.trim()) + : ['test-e2e-android-report', 'test-e2e-ios-report', 'test-e2e-chrome-report', 'test-e2e-firefox-report'], +}; + +function getDateRange() { + const today = new Date(); + const daysAgo = new Date(today.getTime() - (env.LOOKBACK_DAYS * 24 * 60 * 60 * 1000)); + + const fromDisplay = daysAgo.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit' + }); + + const toDisplay = today.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit' + }); + + return { + from: daysAgo.toISOString(), + to: today.toISOString(), + display: `${fromDisplay} - ${toDisplay}` + }; +} + +async function getWorkflowRuns(github, from, to) { + try { + const runs = await github.paginate( + github.rest.actions.listWorkflowRuns, + { + owner: env.OWNER, + repo: env.REPOSITORY, + workflow_id: env.WORKFLOW_ID, + branch: env.BRANCH, + created: `${from}..${to}`, + per_page: 100, + } + ); + + // Filter to only completed runs + const completedRuns = runs.filter(run => run.status === 'completed'); + + // Sort by created date (newest first) + completedRuns.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + + return completedRuns; + } catch (error) { + if (error.status === 404) { + throw new Error(`Workflow '${env.WORKFLOW_ID}' not found in ${env.OWNER}/${env.REPOSITORY}`); + } + throw error; + } +} + +async function downloadArtifact(github, artifact) { + try { + const response = await github.rest.actions.downloadArtifact({ + owner: env.OWNER, + repo: env.REPOSITORY, + artifact_id: artifact.id, + archive_format: 'zip', + }); + + const buffer = Buffer.from(response.data); + const zip = await unzipper.Open.buffer(buffer); + + const testFile = zip.files.find(file => file.path.startsWith(env.TEST_RESULTS_FILE_PATTERN)); + if (!testFile) { + console.log(` โš ๏ธ No ${env.TEST_RESULTS_FILE_PATTERN} file found in ${artifact.name}`); + return null; + } + + const content = await testFile.buffer(); + const data = JSON.parse(content.toString()); + + console.log(` Parsed ${artifact.name} (${data.length} top testSuites)`); + return data; + } catch (error) { + console.log(` โŒ Failed to download ${artifact.name}: ${error.message}`); + return null; + } +} + +async function downloadTestArtifacts(github, runs) { + const allTestData = []; + + for (const [index, run] of runs.entries()) { + console.log(`๐Ÿ“ฆ Processing run ${index + 1}/${runs.length}: ${run.head_commit?.message?.split('\n')[0] || 'No commit message'}`); + + try { + const artifacts = await github.paginate( + github.rest.actions.listWorkflowRunArtifacts, + { + owner: env.OWNER, + repo: env.REPOSITORY, + run_id: run.id, + } + ); + + const testArtifacts = artifacts.filter(artifact => + env.TEST_REPORT_ARTIFACTS.includes(artifact.name) + ); + + if (testArtifacts.length === 0) { + console.log(` โš ๏ธ No test artifacts found for run ${run.id}`); + continue; + } + + for (const artifact of testArtifacts) { + const testData = await downloadArtifact(github, artifact); + if (testData) { + allTestData.push(...testData); + } + } + } catch (error) { + console.log(` โŒ Failed to process run ${run.id}: ${error.message}`); + } + } + + return allTestData; +} + + +function extractRealFailures(testData) { + const realFailures = []; + + for (const testRun of testData) { + for (const testFile of testRun.testFiles || []) { + for (const testSuite of testFile.testSuites || []) { + const retryCount = testSuite.attempts ? testSuite.attempts.length : 0; + + // Process tests that failed even after retries + for (const testCase of testSuite.testCases || []) { + if (testCase.status === 'failed') { + realFailures.push({ + name: testCase.name, + path: testFile.path, + error: testCase.error || 'No error details', + time: testCase.time || 0, + suite: testSuite.name, + jobId: testSuite.job?.id, + runId: testSuite.job?.runId, + date: new Date(testSuite.date || Date.now()), + retryCount: retryCount, + type: 'real_failure' + }); + } + } + } + } + } + + return realFailures; +} + +function extractFlakyTests(testData) { + const flakyTests = []; + + for (const testRun of testData) { + for (const testFile of testRun.testFiles || []) { + for (const testSuite of testFile.testSuites || []) { + const retryCount = testSuite.attempts ? testSuite.attempts.length : 0; + + // Only process suites that have attempts (retries) + if (retryCount > 0) { + // Track failed tests in attempts + const failedInAttempts = new Map(); + for (const attempt of testSuite.attempts || []) { + for (const testCase of attempt.testCases || []) { + if (testCase.status === 'failed') { + failedInAttempts.set(testCase.name, { + jobId: attempt.job?.id, + runId: attempt.job?.runId, + error: testCase.error || 'No error details', + date: new Date(attempt.date || Date.now()) + }); + } + } + } + + // Process tests that eventually passed but had initial failures + for (const testCase of testSuite.testCases || []) { + if (testCase.status === 'passed' && failedInAttempts.has(testCase.name)) { + const failureInfo = failedInAttempts.get(testCase.name); + flakyTests.push({ + name: testCase.name, + path: testFile.path, + error: failureInfo.error, + time: testCase.time || 0, + suite: testSuite.name, + jobId: failureInfo.jobId, + runId: failureInfo.runId, + date: failureInfo.date, + retryCount: retryCount, + type: 'flaky' + }); + } + } + } + } + } + } + + return flakyTests; +} + + +function summarizeFailures(realFailures, flakyTests = []) { + const summary = new Map(); + + // Process real failures first + for (const test of realFailures) { + if (summary.has(test.name)) { + const existing = summary.get(test.name); + existing.realFailures += 1; + existing.totalRetries += test.retryCount; + // Update to chronologically latest real failure + if (test.date > existing.lastRealFailureDate) { + existing.lastRealFailureJobId = test.jobId; + existing.lastRealFailureRunId = test.runId; + existing.lastRealFailureError = test.error; + existing.lastRealFailureDate = test.date; + } + // Update last seen + if (test.date > existing.lastSeen) { + existing.lastSeen = test.date; + } + } else { + summary.set(test.name, { + name: test.name, + path: test.path, + realFailures: 1, + totalRetries: test.retryCount, + lastSeen: test.date, + suite: test.suite, + lastRealFailureJobId: test.jobId, + lastRealFailureRunId: test.runId, + lastRealFailureError: test.error, + lastRealFailureDate: test.date, + // Initialize flaky info as null + flakyFailureJobId: null, + flakyFailureRunId: null, + flakyFailureError: null, + flakyFailureDate: null + }); + } + } + + // Process flaky tests second + for (const test of flakyTests) { + if (summary.has(test.name)) { + // This test also had real failures - just add flaky info + const existing = summary.get(test.name); + existing.totalRetries += test.retryCount; + // Keep most recent flaky failure info + if (!existing.flakyFailureJobId || test.date > existing.flakyFailureDate) { + existing.flakyFailureJobId = test.jobId; + existing.flakyFailureRunId = test.runId; + existing.flakyFailureError = test.error; + existing.flakyFailureDate = test.date; + } + // Update last seen + if (test.date > existing.lastSeen) { + existing.lastSeen = test.date; + } + } else { + // This is purely a flaky test (no real failures) + summary.set(test.name, { + name: test.name, + path: test.path, + realFailures: 0, + totalRetries: test.retryCount, + lastSeen: test.date, + suite: test.suite, + // No real failure info + lastRealFailureJobId: null, + lastRealFailureRunId: null, + lastRealFailureError: null, + lastRealFailureDate: null, + // Flaky failure info + flakyFailureJobId: test.jobId, + flakyFailureRunId: test.runId, + flakyFailureError: test.error, + flakyFailureDate: test.date + }); + } + } + + return Array.from(summary.values()) + .sort((a, b) => { + // Real failures first, sorted by failure count + if (a.realFailures !== b.realFailures) { + return b.realFailures - a.realFailures; + } + // If both have same real failure count, sort by total retries + return b.totalRetries - a.totalRetries; + }); +} + +async function sendSlackReport(summary, dateDisplay, workflowCount, failedCount) { + if (!env.SLACK_WEBHOOK_FLAKY_TESTS || !env.SLACK_WEBHOOK_FLAKY_TESTS.startsWith('https://')) { + console.log('Skipping Slack notification'); + return; + } + + console.log('\n๐Ÿ“ค Sending report to Slack...'); + try { + const webhook = new IncomingWebhook(env.SLACK_WEBHOOK_FLAKY_TESTS); + const blocks = createSlackBlocks(summary, dateDisplay, workflowCount, failedCount); + + // Slack has a limit of 50 blocks per message + const BATCH_SIZE = 50; + for (let i = 0; i < blocks.length; i += BATCH_SIZE) { + const batch = blocks.slice(i, i + BATCH_SIZE); + await webhook.send({ blocks: batch }); + } + + console.log('โœ… Report sent to Slack successfully'); + } catch (slackError) { + console.error('โŒ Failed to send Slack notification:', slackError.message); + } +} + +function createSlackBlocks(summary, dateDisplay, workflowCount = 0, failedCount = 0) { + const blocks = []; + + blocks.push({ + type: 'header', + text: { + type: 'plain_text', + text: 'Flaky Test Report - Top 10', + emoji: true + } + }); + + // Calculate counts first + const realFailures = summary.filter(test => test.realFailures > 0); + const flakyTests = summary.filter(test => test.realFailures === 0); + + blocks.push({ + type: 'context', + elements: [{ + type: 'mrkdwn', + text: `Period (UTC): ${dateDisplay} | Repo: ${env.REPOSITORY} | Failed CI Runs: ${failedCount}/${workflowCount} from ${env.BRANCH} branch\nFound: ${realFailures.length} tests failing, ${flakyTests.length} flaky (eventually passed)` + }] + }); + + blocks.push({ type: 'divider' }); + + if (summary.length === 0) { + blocks.push({ + type: 'rich_text', + elements: [{ + type: 'rich_text_section', + elements: [ + { type: 'text', text: 'No flaky tests found, great job! โœ… ' } + ] + }] + }); + return blocks; + } + + const top10 = summary.slice(0, 10); + + // Real failures section + if (realFailures.length > 0) { + blocks.push({ + type: 'rich_text', + elements: [{ + type: 'rich_text_section', + elements: [ + { type: 'emoji', name: 'x' }, + { type: 'text', text: ' ' }, + { type: 'text', text: 'Failures', style: { bold: true } } + ] + }] + }); + + // Each failure + top10.filter(test => test.realFailures > 0).forEach((test, idx) => { + const globalIndex = top10.indexOf(test) + 1; + const failText = test.realFailures === 1 ? 'time' : 'times'; + const retryText = test.totalRetries === 1 ? 'retry' : 'retries'; + + // Create GitHub file URL + const fileUrl = `https://github.com/${env.OWNER}/${env.REPOSITORY}/blob/${env.BRANCH}/${test.path}`; + + // Build elements for this test + const elements = [ + { type: 'text', text: ` ${globalIndex}. ` }, // 2 spaces indent + { type: 'link', url: fileUrl, text: test.name }, + { type: 'text', text: ` (failed ${test.realFailures} ${failText}, ${test.totalRetries} ${retryText})`, style: { bold: true } } + ]; + + if (test.lastRealFailureJobId && test.lastRealFailureRunId) { + const jobUrl = `https://github.com/${env.OWNER}/${env.REPOSITORY}/actions/runs/${test.lastRealFailureRunId}/job/${test.lastRealFailureJobId}`; + elements.push( + { type: 'text', text: ' - ' }, + { type: 'link', url: jobUrl, text: 'last log' } + ); + } + + blocks.push({ + type: 'rich_text', + elements: [{ + type: 'rich_text_section', + elements: elements + }] + }); + + // Error message (if exists) + const error = test.lastRealFailureError; + if (error) { + const errorPreview = error.length > 150 ? error.substring(0, 150) + '...' : error; + blocks.push({ + type: 'rich_text', + elements: [{ + type: 'rich_text_section', + elements: [ + { type: 'text', text: ` ${errorPreview.replace(/\n/g, ' ')}`, style: { italic: true } } + ] + }] + }); + } + }); + } + + if (realFailures.length >= 10) { + return blocks; + } + + // Divider between sections if both exist + if (realFailures.length > 0 && flakyTests.length > 0) { + blocks.push({ type: 'divider' }); + } + + // Flaky tests section + if (flakyTests.length > 0) { + // Title + blocks.push({ + type: 'rich_text', + elements: [{ + type: 'rich_text_section', + elements: [ + { type: 'emoji', name: 'large_yellow_circle' }, + { type: 'text', text: ' ' }, + { type: 'text', text: 'Flaky (eventually passed)', style: { bold: true } } + ] + }] + }); + + // Each flaky test (respecting the 10-item limit) + const displayedRealFailures = Math.min(realFailures.length, 10); + const remainingSlots = 10 - displayedRealFailures; + const flakyTestsToShow = flakyTests.slice(0, remainingSlots); + + flakyTestsToShow.forEach((test, idx) => { + const globalIndex = displayedRealFailures + idx + 1; + const retryText = test.totalRetries === 1 ? 'retry' : 'retries'; + + // Create GitHub file URL + const fileUrl = `https://github.com/${env.OWNER}/${env.REPOSITORY}/blob/${env.BRANCH}/${test.path}`; + + // Build elements for this test + const elements = [ + { type: 'text', text: ` ${globalIndex}. ` }, // 2 spaces indent + { type: 'link', url: fileUrl, text: test.name }, + { type: 'text', text: ` (${test.totalRetries} ${retryText})`, style: { bold: true } } + ]; + + if (test.flakyFailureJobId && test.flakyFailureRunId) { + const jobUrl = `https://github.com/${env.OWNER}/${env.REPOSITORY}/actions/runs/${test.flakyFailureRunId}/job/${test.flakyFailureJobId}`; + elements.push( + { type: 'text', text: ' - ' }, + { type: 'link', url: jobUrl, text: 'last log' } + ); + } + + blocks.push({ + type: 'rich_text', + elements: [{ + type: 'rich_text_section', + elements: elements + }] + }); + + // Error message (if exists) + const error = test.flakyFailureError; + if (error) { + const errorPreview = error.length > 150 ? error.substring(0, 150) + '...' : error; + blocks.push({ + type: 'rich_text', + elements: [{ + type: 'rich_text_section', + elements: [ + { type: 'text', text: ` ${errorPreview.replace(/\n/g, ' ')}` } + ] + }] + }); + } + }); + } + + return blocks; +} + +function displayResults(summary, dateDisplay) { + console.log('\n' + '='.repeat(80)); + console.log(`๐Ÿ“Š REPORT - ${dateDisplay}`); + console.log('='.repeat(80)); + + if (summary.length === 0) { + console.log('\nโœ… No failed tests found, great job!'); + return; + } + + const realFailures = summary.filter(test => test.realFailures > 0); + const flakyTests = summary.filter(test => test.realFailures === 0); + + console.log(`${realFailures.length} real failures (failed even after retries)`); + console.log(`${flakyTests.length} flaky tests (eventually passed after retries)`); + console.log(`\n๐Ÿ“Œ Sorted by: 1) Number of failures โ†“ 2) Total retries โ†“`); + console.log(`๐Ÿ“Š Numbers shown are cumulative across all runs in the time period\n`); + + const top10 = summary.slice(0, 10); + + for (const [index, test] of top10.entries()) { + console.log(`${(index + 1).toString().padStart(2)}. ${test.name}`); + console.log(` ๐Ÿ“ File: ${test.path}`); + + if (test.realFailures > 0) { + // Real failures (tests that failed even after retries) + const failurePlural = test.realFailures > 1 ? 's' : ''; + const retryPlural = test.totalRetries > 1 ? 'retries' : 'retry'; + const retryText = test.totalRetries > 0 ? ` (${test.totalRetries} total ${retryPlural})` : ''; + console.log(` โŒ Failed: ${test.realFailures} time${failurePlural}${retryText}`); + + // Show logs for real failures + if (test.lastRealFailureJobId && test.lastRealFailureRunId) { + console.log(` ๐Ÿ”— Logs: https://github.com/${env.OWNER}/${env.REPOSITORY}/actions/runs/${test.lastRealFailureRunId}/job/${test.lastRealFailureJobId}`); + } + + // Show error for real failures + if (test.lastRealFailureError) { + const errorPreview = test.lastRealFailureError.length > 100 + ? test.lastRealFailureError.substring(0, 100) + '...' + : test.lastRealFailureError; + console.log(` ๐Ÿ’ฅ Error: ${errorPreview.replace(/\n/g, ' ')}`); + } + } else { + // Flaky tests (failed initially but eventually passed) + const retryPlural = test.totalRetries > 1 ? 'retries' : 'retry'; + console.log(` ๐ŸŸก Flaky: eventually passed (${test.totalRetries} total ${retryPlural})`); + + // Show logs from when it failed (before retry succeeded) + if (test.flakyFailureJobId && test.flakyFailureRunId) { + console.log(` ๐Ÿ”— Logs: https://github.com/${env.OWNER}/${env.REPOSITORY}/actions/runs/${test.flakyFailureRunId}/job/${test.flakyFailureJobId}`); + } + + // Show error from initial failure + if (test.flakyFailureError) { + const errorPreview = test.flakyFailureError.length > 100 + ? test.flakyFailureError.substring(0, 100) + '...' + : test.flakyFailureError; + console.log(` ๐Ÿ’ฅ Initial error: ${errorPreview.replace(/\n/g, ' ')}`); + } + } + + console.log(''); + } + + if (summary.length > 10) { + console.log(`... and ${summary.length - 10} other tests\n`); + } +} + +async function main() { + const github = new Octokit({ auth: env.GITHUB_TOKEN }); + + console.log('๐Ÿงช๐Ÿง Flaky Test Report\n'); + + const dateRange = getDateRange(); + console.log(`Time range: ${dateRange.from} to ${dateRange.to}\n`); + + try { + console.log('Fetching workflow runs...'); + const workflowRuns = await getWorkflowRuns(github, dateRange.from, dateRange.to); + + if (workflowRuns.length === 0) { + console.log('โš ๏ธ No workflow runs found.'); + return; + } + + console.log(`Found ${workflowRuns.length} workflow run(s)`); + + // Count failed runs + const failedRuns = workflowRuns.filter(run => run.conclusion !== 'success'); + console.log(`Failed CI Runs: ${failedRuns.length}/${workflowRuns.length} from ${env.BRANCH}`); + + console.log('Downloading their test artifacts...'); + const testData = await downloadTestArtifacts(github, workflowRuns); + + if (testData.length === 0) { + console.log('โš ๏ธ No test artifacts found in failed runs'); + return; + } + + console.log('Analyzing test failures...'); + + // Two-pass approach: process real failures and flaky tests separately + const realFailures = extractRealFailures(testData); + const flakyTests = extractFlakyTests(testData); + + const summary = summarizeFailures(realFailures, flakyTests); + displayResults(summary, dateRange.display); + await sendSlackReport(summary, dateRange.display, workflowRuns.length, failedRuns.length); + + } catch (error) { + console.error('โŒ Error:', error.message); + if (error.status === 401) { + console.log('\n๐Ÿ’ก This might be a GitHub token issue. Make sure your token has the right permissions.'); + } + process.exit(1); + } +} + +main().catch(error => { + console.error('\nโŒ Unexpected error:', error); + process.exit(1); +}); diff --git a/.github/scripts/post-merge-validation-tracker.mjs b/.github/scripts/post-merge-validation-tracker.mjs index 68155ef9..8297ee0d 100644 --- a/.github/scripts/post-merge-validation-tracker.mjs +++ b/.github/scripts/post-merge-validation-tracker.mjs @@ -5,7 +5,7 @@ const githubToken = process.env.GITHUB_TOKEN; const spreadsheetId = process.env.SHEET_ID; const googleApplicationCredentialsBase64 = process.env.GOOGLE_APPLICATION_CREDENTIALS_BASE64; const repo = process.env.REPO; -const LOOKBACK_DAYS = parseInt(process.env.LOOKBACK_DAYS ?? '2'); +const LOOKBACK_DAYS = parseInt(process.env.LOOKBACK_DAYS ?? '1'); const START_HOUR_UTC = parseInt(process.env.START_HOUR_UTC ?? '7'); const START_MINUTE_UTC = 0; diff --git a/.github/workflows/flaky-test-report.yml b/.github/workflows/flaky-test-report.yml new file mode 100644 index 00000000..1f5c298a --- /dev/null +++ b/.github/workflows/flaky-test-report.yml @@ -0,0 +1,54 @@ +name: Flaky Test Report + +on: + workflow_call: + inputs: + repository: + description: 'Repository name (e.g. metamask-extension)' + required: true + type: string + workflow_id: + description: 'Workflow ID to analyze (e.g. main.yml)' + required: true + type: string + secrets: + github-token: + description: 'GitHub token with repo and actions:read access' + required: true + slack-webhook-flaky-tests: + description: 'Slack webhook URL for flaky test reports' + required: true + +jobs: + flaky-test-report: + runs-on: ubuntu-latest + steps: + - name: Checkout github-tools repository + uses: actions/checkout@v4 + with: + repository: MetaMask/github-tools + path: github-tools + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: ./github-tools/.nvmrc + cache-dependency-path: ./github-tools/yarn.lock + cache: yarn + + - name: Enable Corepack + run: corepack enable + working-directory: ./github-tools + + - name: Install dependencies + working-directory: ./github-tools + run: yarn --immutable + + - name: Run flaky test report script + env: + REPOSITORY: ${{ inputs.repository }} + WORKFLOW_ID: ${{ inputs.workflow_id }} + GITHUB_TOKEN: ${{ secrets.github-token }} + SLACK_WEBHOOK_FLAKY_TESTS: ${{ secrets.slack-webhook-flaky-tests }} + working-directory: ./github-tools + run: node .github/scripts/create-flaky-test-report.mjs diff --git a/package.json b/package.json index 9285b795..4eb16b0d 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@octokit/request": "^8.1.1", "@octokit/rest": "^19.0.13", "@slack/web-api": "^6.0.0", + "@slack/webhook": "^7.0.6", "@types/luxon": "^3.3.0", "axios": "^0.24.0", "csv-parse": "5.6.0", @@ -35,7 +36,8 @@ "luxon": "^3.3.0", "ora": "^5.4.1", "semver": "^7.7.2", - "simple-git": "3.27.0" + "simple-git": "3.27.0", + "unzipper": "^0.12.3" }, "devDependencies": { "@lavamoat/allow-scripts": "^2.3.1", @@ -49,6 +51,7 @@ "@types/jest": "^28.1.6", "@types/node": "^20.3.2", "@types/semver": "^7", + "@types/unzipper": "^0", "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", "depcheck": "^1.4.3", diff --git a/yarn.lock b/yarn.lock index 7e92bef3..80291c24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1022,12 +1022,14 @@ __metadata: "@octokit/request": "npm:^8.1.1" "@octokit/rest": "npm:^19.0.13" "@slack/web-api": "npm:^6.0.0" + "@slack/webhook": "npm:^7.0.6" "@swc/cli": "npm:^0.1.62" "@swc/core": "npm:^1.3.80" "@types/jest": "npm:^28.1.6" "@types/luxon": "npm:^3.3.0" "@types/node": "npm:^20.3.2" "@types/semver": "npm:^7" + "@types/unzipper": "npm:^0" "@typescript-eslint/eslint-plugin": "npm:^5.43.0" "@typescript-eslint/parser": "npm:^5.43.0" axios: "npm:^0.24.0" @@ -1054,6 +1056,7 @@ __metadata: ts-jest: "npm:^28.0.7" ts-node: "npm:^10.9.1" typescript: "npm:^5.1.3" + unzipper: "npm:^0.12.3" languageName: unknown linkType: soft @@ -1469,10 +1472,10 @@ __metadata: languageName: node linkType: hard -"@slack/types@npm:^2.11.0": - version: 2.14.0 - resolution: "@slack/types@npm:2.14.0" - checksum: 10/fa24a113b88e087f899078504c2ba50ab9795f7c2dd1a2d95b28217a3af20e554494f9cc3b8c8ce173120990d98e19400c95369f9067cecfcc46c08b59d2a46f +"@slack/types@npm:^2.11.0, @slack/types@npm:^2.9.0": + version: 2.16.0 + resolution: "@slack/types@npm:2.16.0" + checksum: 10/e18b568a47d94e9e7234dfd06f789224d6804edae4a2f31068b3f388ce4c482a6dbc6c035dc3dec63e5723f211f92c7694ee40b2ec83d4ac90d46bb35fa46eb5 languageName: node linkType: hard @@ -1495,6 +1498,17 @@ __metadata: languageName: node linkType: hard +"@slack/webhook@npm:^7.0.6": + version: 7.0.6 + resolution: "@slack/webhook@npm:7.0.6" + dependencies: + "@slack/types": "npm:^2.9.0" + "@types/node": "npm:>=18.0.0" + axios: "npm:^1.11.0" + checksum: 10/8f8083f9654e590f04731985b337f576842b2034a9261010f85d813c4e262f69d856c142b0dcf2022bfe69c22c2e97cc7d877a79989cd0f7a0cf2554ae0754ed + languageName: node + linkType: hard + "@swc/cli@npm:^0.1.62": version: 0.1.62 resolution: "@swc/cli@npm:0.1.62" @@ -1875,12 +1889,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12.0.0": - version: 22.13.1 - resolution: "@types/node@npm:22.13.1" +"@types/node@npm:*, @types/node@npm:>=12.0.0, @types/node@npm:>=18.0.0": + version: 24.3.0 + resolution: "@types/node@npm:24.3.0" dependencies: - undici-types: "npm:~6.20.0" - checksum: 10/d8ba7068b0445643c0fa6e4917cdb7a90e8756a9daff8c8a332689cd5b2eaa01e4cd07de42e3cd7e6a6f465eeda803d5a1363d00b5ab3f6cea7950350a159497 + undici-types: "npm:~7.10.0" + checksum: 10/1331c2d0e9a512ac27a016b4df3eff92317e4603dbbbab31731275dff14d3a04847a50c5776cbf94f99ff4dedac0ba5f721dce8cea020d8eea5e21711fd964b0 languageName: node linkType: hard @@ -1935,6 +1949,15 @@ __metadata: languageName: node linkType: hard +"@types/unzipper@npm:^0": + version: 0.10.11 + resolution: "@types/unzipper@npm:0.10.11" + dependencies: + "@types/node": "npm:*" + checksum: 10/c11c0e072556038730b218ccf8af849911ed8a1338e6db863bdf4c44d53d83dd23e3de4752322b1e19cf0205ed6eaf8746e25aa3c2b38e419da457f9d6be7b4e + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 15.0.0 resolution: "@types/yargs-parser@npm:15.0.0" @@ -2448,14 +2471,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.7.4": - version: 1.7.9 - resolution: "axios@npm:1.7.9" +"axios@npm:^1.11.0, axios@npm:^1.7.4": + version: 1.11.0 + resolution: "axios@npm:1.11.0" dependencies: follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.0" + form-data: "npm:^4.0.4" proxy-from-env: "npm:^1.1.0" - checksum: 10/b7a5f660ea53ba9c2a745bf5ad77ad8bf4f1338e13ccc3f9f09f810267d6c638c03dac88b55dae8dc98b79c57d2d6835be651d58d2af97c174f43d289a9fd007 + checksum: 10/232df4af7a4e4e07baa84621b9cc4b0c518a757b4eacc7f635c0eb3642cb98dff347326739f24b891b3b4481b7b838c79a3a0c4819c9fbc1fc40232431b9c5dc languageName: node linkType: hard @@ -2624,6 +2647,13 @@ __metadata: languageName: node linkType: hard +"bluebird@npm:~3.7.2": + version: 3.7.2 + resolution: "bluebird@npm:3.7.2" + checksum: 10/007c7bad22c5d799c8dd49c85b47d012a1fe3045be57447721e6afbd1d5be43237af1db62e26cb9b0d9ba812d2e4ca3bac82f6d7e016b6b88de06ee25ceb96e7 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -3085,6 +3115,13 @@ __metadata: languageName: node linkType: hard +"core-util-is@npm:~1.0.0": + version: 1.0.3 + resolution: "core-util-is@npm:1.0.3" + checksum: 10/9de8597363a8e9b9952491ebe18167e3b36e7707569eed0ebf14f8bba773611376466ae34575bca8cfe3c767890c859c74056084738f09d4e4a6f902b2ad7d99 + languageName: node + linkType: hard + "cosmiconfig@npm:^7.0.0": version: 7.1.0 resolution: "cosmiconfig@npm:7.1.0" @@ -3372,6 +3409,15 @@ __metadata: languageName: node linkType: hard +"duplexer2@npm:~0.1.4": + version: 0.1.4 + resolution: "duplexer2@npm:0.1.4" + dependencies: + readable-stream: "npm:^2.0.2" + checksum: 10/f60ff8b8955f992fd9524516e82faa5662d7aca5b99ee71c50bbbe1a3c970fafacb35d526d8b05cef8c08be56eed3663c096c50626c3c3651a52af36c408bf4d + languageName: node + linkType: hard + "ecdsa-sig-formatter@npm:1.0.11, ecdsa-sig-formatter@npm:^1.0.11": version: 1.0.11 resolution: "ecdsa-sig-formatter@npm:1.0.11" @@ -3513,14 +3559,15 @@ __metadata: languageName: node linkType: hard -"es-set-tostringtag@npm:^2.0.1": - version: 2.0.1 - resolution: "es-set-tostringtag@npm:2.0.1" +"es-set-tostringtag@npm:^2.0.1, es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" dependencies: - get-intrinsic: "npm:^1.1.3" - has: "npm:^1.0.3" - has-tostringtag: "npm:^1.0.0" - checksum: 10/ec416a12948cefb4b2a5932e62093a7cf36ddc3efd58d6c58ca7ae7064475ace556434b869b0bbeb0c365f1032a8ccd577211101234b69837ad83ad204fff884 + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.6" + has-tostringtag: "npm:^1.0.2" + hasown: "npm:^2.0.2" + checksum: 10/86814bf8afbcd8966653f731415888019d4bc4aca6b6c354132a7a75bb87566751e320369654a101d23a91c87a85c79b178bcf40332839bd347aff437c4fb65f languageName: node linkType: hard @@ -4229,14 +4276,27 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0": - version: 4.0.1 - resolution: "form-data@npm:4.0.1" +"form-data@npm:^4.0.4": + version: 4.0.4 + resolution: "form-data@npm:4.0.4" dependencies: asynckit: "npm:^0.4.0" combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" mime-types: "npm:^2.1.12" - checksum: 10/6adb1cff557328bc6eb8a68da205f9ae44ab0e88d4d9237aaf91eed591ffc64f77411efb9016af7d87f23d0a038c45a788aa1c6634e51175c4efa36c2bc53774 + checksum: 10/a4b62e21932f48702bc468cc26fb276d186e6b07b557e3dd7cc455872bdbb82db7db066844a64ad3cf40eaf3a753c830538183570462d3649fdfd705601cbcfb + languageName: node + linkType: hard + +"fs-extra@npm:^11.2.0": + version: 11.3.1 + resolution: "fs-extra@npm:11.3.1" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/2b893213411b1da11f9b061ccb0bcff4d6dd66fe90aa8f5b1616219a5e7ca659da869f454ebd8e94aa21c58342730fb43a2e5c98b5c6c5124f0c54a4633f64b0 languageName: node linkType: hard @@ -4611,10 +4671,10 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": - version: 4.2.10 - resolution: "graceful-fs@npm:4.2.10" - checksum: 10/0c83c52b62c68a944dcfb9d66b0f9f10f7d6e3d081e8067b9bfdc9e5f3a8896584d576036f82915773189eec1eba599397fc620e75c03c0610fb3d67c6713c1a +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 languageName: node linkType: hard @@ -4874,7 +4934,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:^2.0.4": +"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 @@ -5183,6 +5243,13 @@ __metadata: languageName: node linkType: hard +"isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: 10/f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -5811,6 +5878,19 @@ __metadata: languageName: node linkType: hard +"jsonfile@npm:^6.0.1": + version: 6.2.0 + resolution: "jsonfile@npm:6.2.0" + dependencies: + graceful-fs: "npm:^4.1.6" + universalify: "npm:^2.0.0" + dependenciesMeta: + graceful-fs: + optional: true + checksum: 10/513aac94a6eff070767cafc8eb4424b35d523eec0fcd8019fe5b975f4de5b10a54640c8d5961491ddd8e6f562588cf62435c5ddaf83aaf0986cd2ee789e0d7b9 + languageName: node + linkType: hard + "jwa@npm:^2.0.0": version: 2.0.0 resolution: "jwa@npm:2.0.0" @@ -6836,6 +6916,13 @@ __metadata: languageName: node linkType: hard +"process-nextick-args@npm:~2.0.0": + version: 2.0.1 + resolution: "process-nextick-args@npm:2.0.1" + checksum: 10/1d38588e520dab7cea67cbbe2efdd86a10cc7a074c09657635e34f035277b59fbb57d09d8638346bf7090f8e8ebc070c96fa5fd183b777fff4f5edff5e9466cf + languageName: node + linkType: hard + "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -6951,6 +7038,21 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^2.0.2": + version: 2.3.8 + resolution: "readable-stream@npm:2.3.8" + dependencies: + core-util-is: "npm:~1.0.0" + inherits: "npm:~2.0.3" + isarray: "npm:~1.0.0" + process-nextick-args: "npm:~2.0.0" + safe-buffer: "npm:~5.1.1" + string_decoder: "npm:~1.1.1" + util-deprecate: "npm:~1.0.1" + checksum: 10/8500dd3a90e391d6c5d889256d50ec6026c059fadee98ae9aa9b86757d60ac46fff24fafb7a39fa41d54cb39d8be56cc77be202ebd4cd8ffcf4cb226cbaa40d4 + languageName: node + linkType: hard + "readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" @@ -7161,7 +7263,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:~5.1.1": +"safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": version: 5.1.2 resolution: "safe-buffer@npm:5.1.2" checksum: 10/7eb5b48f2ed9a594a4795677d5a150faa7eb54483b2318b568dc0c4fc94092a6cce5be02c7288a0500a156282f5276d5688bce7259299568d1053b2150ef374a @@ -7616,6 +7718,15 @@ __metadata: languageName: node linkType: hard +"string_decoder@npm:~1.1.1": + version: 1.1.1 + resolution: "string_decoder@npm:1.1.1" + dependencies: + safe-buffer: "npm:~5.1.0" + checksum: 10/7c41c17ed4dea105231f6df208002ebddd732e8e9e2d619d133cecd8e0087ddfd9587d2feb3c8caf3213cbd841ada6d057f5142cae68a4e62d3540778d9819b4 + languageName: node + linkType: hard + "strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -8036,10 +8147,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.20.0": - version: 6.20.0 - resolution: "undici-types@npm:6.20.0" - checksum: 10/583ac7bbf4ff69931d3985f4762cde2690bb607844c16a5e2fbb92ed312fe4fa1b365e953032d469fa28ba8b224e88a595f0b10a449332f83fa77c695e567dbe +"undici-types@npm:~7.10.0": + version: 7.10.0 + resolution: "undici-types@npm:7.10.0" + checksum: 10/1f3fe777937690ab8a7a7bccabc8fdf4b3171f4899b5a384fb5f3d6b56c4b5fec2a51fbf345c9dd002ff6716fd440a37fa8fdb0e13af8eca8889f25445875ba3 languageName: node linkType: hard @@ -8075,6 +8186,26 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^2.0.0": + version: 2.0.1 + resolution: "universalify@npm:2.0.1" + checksum: 10/ecd8469fe0db28e7de9e5289d32bd1b6ba8f7183db34f3bfc4ca53c49891c2d6aa05f3fb3936a81285a905cc509fb641a0c3fc131ec786167eff41236ae32e60 + languageName: node + linkType: hard + +"unzipper@npm:^0.12.3": + version: 0.12.3 + resolution: "unzipper@npm:0.12.3" + dependencies: + bluebird: "npm:~3.7.2" + duplexer2: "npm:~0.1.4" + fs-extra: "npm:^11.2.0" + graceful-fs: "npm:^4.2.2" + node-int64: "npm:^0.4.0" + checksum: 10/b210c421308e1913e01b54faad4ae79e758c674311892414a0697acacba9f82fa0051b677faa77e62fab422eef928c858f2d5cda9ddb47a2f3db95b0e9b36359 + languageName: node + linkType: hard + "update-browserslist-db@npm:^1.0.5": version: 1.0.5 resolution: "update-browserslist-db@npm:1.0.5" @@ -8105,7 +8236,7 @@ __metadata: languageName: node linkType: hard -"util-deprecate@npm:^1.0.1": +"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" checksum: 10/474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2