diff --git a/.github/actions/flaky-test-report/action.yml b/.github/actions/flaky-test-report/action.yml new file mode 100644 index 00000000..077cff6f --- /dev/null +++ b/.github/actions/flaky-test-report/action.yml @@ -0,0 +1,52 @@ +name: Flaky Test Report +description: 'Generates a report of flaky tests from a specified GitHub Actions workflow and sends it to a Slack channel.' + +inputs: + repository: + description: 'Repository name (e.g. metamask-extension)' + required: true + workflow-id: + description: 'Workflow ID to analyze (e.g. main.yml)' + required: true + 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 + +runs: + using: composite + 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 + working-directory: ./github-tools + shell: bash + run: corepack enable + + - name: Install dependencies + working-directory: ./github-tools + shell: bash + run: yarn --immutable + + - name: Run flaky test report script + env: + REPOSITORY: ${{ inputs.repository }} + WORKFLOW_ID: ${{ inputs.workflow-id }} + GITHUB_TOKEN: ${{ inputs.github-token }} + SLACK_WEBHOOK_FLAKY_TESTS: ${{ inputs.slack-webhook-flaky-tests }} + working-directory: ./github-tools + shell: bash + run: node .github/scripts/create-flaky-test-report.mjs diff --git a/.github/actions/log-merge-group-failure/action.yml b/.github/actions/log-merge-group-failure/action.yml new file mode 100644 index 00000000..7f23ddbe --- /dev/null +++ b/.github/actions/log-merge-group-failure/action.yml @@ -0,0 +1,50 @@ +name: Log Merge Group Failure + +inputs: + google-application-credentials: + description: 'Path to Google application credentials JSON file.' + required: true + google-service-account: + description: 'Base64 encoded Google service account JSON.' + required: true + spreadsheet-id: + description: 'Google Spreadsheet ID.' + required: true + sheet-name: + description: 'Sheet tab name.' + required: true + +runs: + using: composite + steps: + - name: Download oauth2l + shell: bash + run: | + curl --silent https://storage.googleapis.com/oauth2l/1.3.2/linux_amd64.tgz | tar xz + echo "$PWD/linux_amd64" >> "$GITHUB_PATH" + + - name: Create service_account.json + env: + GOOGLE_APPLICATION_CREDENTIALS: ${{ inputs.google-application-credentials }} + GOOGLE_SERVICE_ACCOUNT: ${{ inputs.google-service-account }} + shell: bash + run: | + echo "$GOOGLE_SERVICE_ACCOUNT" > "$GOOGLE_APPLICATION_CREDENTIALS" + + - name: Write data to google sheets + env: + GOOGLE_APPLICATION_CREDENTIALS: ${{ inputs.google-application-credentials }} + SPREADSHEET_ID: ${{ inputs.spreadsheet-id }} + SHEET_NAME: ${{ inputs.sheet-name }} + shell: bash + run: | + current_date=$(date +%Y-%m-%d) + token=$(oauth2l fetch --scope https://www.googleapis.com/auth/spreadsheets) + spreadsheet_data=$(curl --silent --header "Authorization: Bearer $token" https://sheets.googleapis.com/v4/spreadsheets/"$SPREADSHEET_ID"/values/"$SHEET_NAME"!A:B) + current_date_index=$(echo "$spreadsheet_data" | jq --arg current_date "$current_date" '(.values | map(.[0])) | (index($current_date) | if . == null then null else . + 1 end)') + current_number_of_prs=$(echo "$spreadsheet_data" | jq --arg current_date "$current_date" '(.values[] | select(.[0] == $current_date) | .[1] | tonumber) // null') + if [ "$current_date_index" == "null" ]; then + curl --silent --header "Authorization: Bearer $token" --header "Content-Type: application/json" --request POST --data "{\"values\":[[\"$current_date\", 1]]}" https://sheets.googleapis.com/v4/spreadsheets/"$SPREADSHEET_ID"/values/"$SHEET_NAME"!A:A:append?valueInputOption=USER_ENTERED + else + curl --silent --header "Authorization: Bearer $token" --header "Content-Type: application/json" --request PUT --data "{\"values\":[[\"$current_date\", $(("$current_number_of_prs" + 1))]]}" https://sheets.googleapis.com/v4/spreadsheets/"$SPREADSHEET_ID"/values/"$SHEET_NAME"!A"$current_date_index":B"$current_date_index"?valueInputOption=USER_ENTERED + fi diff --git a/.github/actions/pr-line-check/action.yml b/.github/actions/pr-line-check/action.yml new file mode 100644 index 00000000..8149ef7b --- /dev/null +++ b/.github/actions/pr-line-check/action.yml @@ -0,0 +1,203 @@ +name: Check PR Lines Changed +description: 'Checks the number of lines changed in a PR and manages size labels accordingly.' + +inputs: + max-lines: + description: 'Maximum allowed total lines changed' + required: false + default: '1000' + base-ref: + description: 'Default base branch to compare against (if not running on a PR)' + required: false + default: 'main' + ignore-patterns: + description: 'Regex pattern for files to ignore when calculating changes' + required: false + default: '(\.lock$)' + xs-max-size: + description: 'Maximum lines for XS size' + required: false + default: '10' + s-max-size: + description: 'Maximum lines for S size' + required: false + default: '100' + m-max-size: + description: 'Maximum lines for M size' + required: false + default: '500' + l-max-size: + description: 'Maximum lines for L size' + required: false + default: '1000' + +runs: + using: composite + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Calculate changed lines + id: line-count + env: + BASE_BRANCH: ${{ github.event.pull_request.base.ref || inputs.base-ref }} + IGNORE_PATTERNS: ${{ inputs.ignore-patterns }} + shell: bash + run: | + set -e + + echo "Using base branch: $BASE_BRANCH" + + # Instead of a full fetch, perform incremental fetches at increasing depth + # until the merge-base between origin/ and HEAD is present. + fetch_with_depth() { + local depth=$1 + echo "Attempting to fetch with depth $depth..." + git fetch --depth="$depth" origin "$BASE_BRANCH" + } + + depths=(1 10 100) + merge_base_found=false + + for d in "${depths[@]}"; do + fetch_with_depth "$d" + if git merge-base "origin/$BASE_BRANCH" HEAD > /dev/null 2>&1; then + echo "Merge base found with depth $d." + merge_base_found=true + break + else + echo "Merge base not found with depth $d, increasing depth..." + fi + done + + # If we haven't found the merge base with shallow fetches, unshallow the repo. + if [ "$merge_base_found" = false ]; then + echo "Could not find merge base with shallow fetches, fetching full history..." + git fetch --unshallow origin "$BASE_BRANCH" || git fetch origin "$BASE_BRANCH" + fi + + # Calculate additions and deletions across all changes between the base and HEAD, + # filtering out files matching the ignore pattern. + additions=$(git diff "origin/$BASE_BRANCH"...HEAD --numstat | grep -Ev "$IGNORE_PATTERNS" | awk '{add += $1} END {print add+0}') + deletions=$(git diff "origin/$BASE_BRANCH"...HEAD --numstat | grep -Ev "$IGNORE_PATTERNS" | awk '{del += $2} END {print del+0}') + total=$((additions + deletions)) + + echo "Additions: $additions, Deletions: $deletions, Total: $total" + { + echo "lines-changed=$total" + echo "additions=$additions" + echo "deletions=$deletions" + } >> "$GITHUB_OUTPUT" + + - name: Check line count limit + uses: actions/github-script@v7 + env: + LINES_CHANGED: ${{ steps.line-count.outputs.lines-changed }} + ADDITIONS: ${{ steps.line-count.outputs.additions }} + DELETIONS: ${{ steps.line-count.outputs.deletions }} + MAX_LINES: ${{ inputs.max-lines }} + XS_MAX_SIZE: ${{ inputs.xs-max-size }} + S_MAX_SIZE: ${{ inputs.s-max-size }} + M_MAX_SIZE: ${{ inputs.m-max-size }} + L_MAX_SIZE: ${{ inputs.l-max-size }} + with: + script: | + const { + LINES_CHANGED, + ADDITIONS, + DELETIONS, + MAX_LINES, + XS_MAX_SIZE, + S_MAX_SIZE, + M_MAX_SIZE, + L_MAX_SIZE, + } = process.env; + + const total = parseInt(LINES_CHANGED, 10) || 0; + const additions = parseInt(ADDITIONS, 10) || 0; + const deletions = parseInt(DELETIONS, 10) || 0; + + // Thresholds from inputs with fallback to defaults + const maxLines = parseInt(MAX_LINES, 10) || 1000; + const xsMaxSize = parseInt(XS_MAX_SIZE, 10) || 10; + const sMaxSize = parseInt(S_MAX_SIZE, 10) || 100; + const mMaxSize = parseInt(M_MAX_SIZE, 10) || 500; + const lMaxSize = parseInt(L_MAX_SIZE, 10) || 1000; + + // Print summary + console.log('Summary:'); + console.log(` - Additions: ${additions}`); + console.log(` - Deletions: ${deletions}`); + console.log(` - Total: ${total}`); + console.log(` - Limit: ${maxLines}`); + + // Determine size label based on configured criteria + let sizeLabel = ''; + if (total <= xsMaxSize) { + sizeLabel = 'size-XS'; + } else if (total <= sMaxSize) { + sizeLabel = 'size-S'; + } else if (total <= mMaxSize) { + sizeLabel = 'size-M'; + } else if (total <= lMaxSize) { + sizeLabel = 'size-L'; + } else { + sizeLabel = 'size-XL'; + } + + console.log(` - Size category: ${sizeLabel}`); + + // Manage PR labels + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.payload.pull_request.number; + + try { + const existingSizeLabels = ['size-XS', 'size-S', 'size-M', 'size-L', 'size-XL']; + + // Get current labels + const currentLabels = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number + }); + + const currentLabelNames = currentLabels.data.map(l => l.name); + + // Build new label set: keep non-size labels and add the new size label + const newLabels = currentLabelNames + .filter(name => !existingSizeLabels.includes(name)) // Remove all size labels + .concat(sizeLabel); // Add the correct size label + + // Check if labels need updating + const currentSizeLabel = currentLabelNames.find(name => existingSizeLabels.includes(name)); + if (currentSizeLabel === sizeLabel && currentLabelNames.length === newLabels.length) { + console.log(`✅ Correct label '${sizeLabel}' already present, no changes needed`); + } else { + // Update all labels in a single API call + await github.rest.issues.setLabels({ + owner, + repo, + issue_number, + labels: newLabels + }); + + if (currentSizeLabel && currentSizeLabel !== sizeLabel) { + console.log(` - Replaced '${currentSizeLabel}' with '${sizeLabel}'`); + } else if (!currentSizeLabel) { + console.log(`✅ Added '${sizeLabel}' label to PR #${issue_number}`); + } else { + console.log(`✅ Updated labels for PR #${issue_number}`); + } + } + } catch (error) { + console.log(`⚠️ Could not manage labels: ${error.message}`); + } + + // Check if exceeds limit + if (total > maxLines) { + console.log(`❌ Error: Total changed lines (${total}) exceed the limit of ${maxLines}.`); + process.exit(1); + } else { + console.log(`✅ Success: Total changed lines (${total}) are within the limit of ${maxLines}.`); + } diff --git a/.github/workflows/flaky-test-report.yml b/.github/workflows/flaky-test-report.yml deleted file mode 100644 index 1f5c298a..00000000 --- a/.github/workflows/flaky-test-report.yml +++ /dev/null @@ -1,54 +0,0 @@ -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/.github/workflows/log-merge-group-failure.yml b/.github/workflows/log-merge-group-failure.yml deleted file mode 100644 index 9da867a3..00000000 --- a/.github/workflows/log-merge-group-failure.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Log merge group failure - -on: - workflow_call: - secrets: - GOOGLE_APPLICATION_CREDENTIALS: - required: true - GOOGLE_SERVICE_ACCOUNT: - required: true - SPREADSHEET_ID: - required: true - SHEET_NAME: - required: true - workflow_dispatch: - -jobs: - log-merge-group-failure: - name: Log merge group failure - runs-on: ubuntu-latest - steps: - - name: Download oauth2l - run: | - curl --silent https://storage.googleapis.com/oauth2l/1.3.2/linux_amd64.tgz | tar xz - echo "$PWD/linux_amd64" >> "$GITHUB_PATH" - - - name: Create service_account.json - env: - GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} - GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - run: | - echo "$GOOGLE_SERVICE_ACCOUNT" > "$GOOGLE_APPLICATION_CREDENTIALS" - - - name: Write data to google sheets - env: - GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} - SPREADSHEET_ID: ${{ secrets.SPREADSHEET_ID }} - SHEET_NAME: ${{ secrets.SHEET_NAME }} - run: | - current_date=$(date +%Y-%m-%d) - token=$(oauth2l fetch --scope https://www.googleapis.com/auth/spreadsheets) - spreadsheet_data=$(curl --silent --header "Authorization: Bearer $token" https://sheets.googleapis.com/v4/spreadsheets/"$SPREADSHEET_ID"/values/"$SHEET_NAME"!A:B) - current_date_index=$(echo "$spreadsheet_data" | jq --arg current_date "$current_date" '(.values | map(.[0])) | (index($current_date) | if . == null then null else . + 1 end)') - current_number_of_prs=$(echo "$spreadsheet_data" | jq --arg current_date "$current_date" '(.values[] | select(.[0] == $current_date) | .[1] | tonumber) // null') - if [ "$current_date_index" == "null" ]; then - curl --silent --header "Authorization: Bearer $token" --header "Content-Type: application/json" --request POST --data "{\"values\":[[\"$current_date\", 1]]}" https://sheets.googleapis.com/v4/spreadsheets/"$SPREADSHEET_ID"/values/"$SHEET_NAME"!A:A:append?valueInputOption=USER_ENTERED - else - curl --silent --header "Authorization: Bearer $token" --header "Content-Type: application/json" --request PUT --data "{\"values\":[[\"$current_date\", $(("$current_number_of_prs" + 1))]]}" https://sheets.googleapis.com/v4/spreadsheets/"$SPREADSHEET_ID"/values/"$SHEET_NAME"!A"$current_date_index":B"$current_date_index"?valueInputOption=USER_ENTERED - fi diff --git a/.github/workflows/pr-line-check.yml b/.github/workflows/pr-line-check.yml deleted file mode 100644 index cd0868c3..00000000 --- a/.github/workflows/pr-line-check.yml +++ /dev/null @@ -1,213 +0,0 @@ -name: Check PR Lines Changed - -on: - workflow_call: - inputs: - max_lines: - description: 'Maximum allowed total lines changed' - required: false - type: number - default: 1000 - base_ref: - description: 'Default base branch to compare against (if not running on a PR)' - required: false - type: string - default: 'main' - ignore_patterns: - description: 'Regex pattern for files to ignore when calculating changes' - required: false - type: string - default: '(\.lock$)' - xs_max_size: - description: 'Maximum lines for XS size' - required: false - type: number - default: 10 - s_max_size: - description: 'Maximum lines for S size' - required: false - type: number - default: 100 - m_max_size: - description: 'Maximum lines for M size' - required: false - type: number - default: 500 - l_max_size: - description: 'Maximum lines for L size' - required: false - type: number - default: 1000 - -jobs: - check-lines: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Calculate changed lines - id: line_count - env: - BASE_BRANCH: ${{ github.event.pull_request.base.ref || inputs.base_ref }} - run: | - set -e - - echo "Using base branch: $BASE_BRANCH" - - # Instead of a full fetch, perform incremental fetches at increasing depth - # until the merge-base between origin/ and HEAD is present. - fetch_with_depth() { - local depth=$1 - echo "Attempting to fetch with depth $depth..." - git fetch --depth="$depth" origin "$BASE_BRANCH" - } - - depths=(1 10 100) - merge_base_found=false - - for d in "${depths[@]}"; do - fetch_with_depth "$d" - if git merge-base "origin/$BASE_BRANCH" HEAD > /dev/null 2>&1; then - echo "Merge base found with depth $d." - merge_base_found=true - break - else - echo "Merge base not found with depth $d, increasing depth..." - fi - done - - # If we haven't found the merge base with shallow fetches, unshallow the repo. - if [ "$merge_base_found" = false ]; then - echo "Could not find merge base with shallow fetches, fetching full history..." - git fetch --unshallow origin "$BASE_BRANCH" || git fetch origin "$BASE_BRANCH" - fi - - # Set the ignore pattern from input - ignore_pattern="${{ inputs.ignore_patterns }}" - - # Calculate additions and deletions across all changes between the base and HEAD, - # filtering out files matching the ignore pattern. - additions=$(git diff "origin/$BASE_BRANCH"...HEAD --numstat | grep -Ev "$ignore_pattern" | awk '{add += $1} END {print add+0}') - deletions=$(git diff "origin/$BASE_BRANCH"...HEAD --numstat | grep -Ev "$ignore_pattern" | awk '{del += $2} END {print del+0}') - total=$((additions + deletions)) - - echo "Additions: $additions, Deletions: $deletions, Total: $total" - { - echo "lines_changed=$total" - echo "additions=$additions" - echo "deletions=$deletions" - } >> "$GITHUB_OUTPUT" - - - name: Check line count limit - uses: actions/github-script@v7 - env: - LINES_CHANGED: ${{ steps.line_count.outputs.lines_changed }} - ADDITIONS: ${{ steps.line_count.outputs.additions }} - DELETIONS: ${{ steps.line_count.outputs.deletions }} - MAX_LINES: ${{ inputs.max_lines }} - XS_MAX_SIZE: ${{ inputs.xs_max_size }} - S_MAX_SIZE: ${{ inputs.s_max_size }} - M_MAX_SIZE: ${{ inputs.m_max_size }} - L_MAX_SIZE: ${{ inputs.l_max_size }} - with: - script: | - const { - LINES_CHANGED, - ADDITIONS, - DELETIONS, - MAX_LINES, - XS_MAX_SIZE, - S_MAX_SIZE, - M_MAX_SIZE, - L_MAX_SIZE, - } = process.env; - - const total = parseInt(LINES_CHANGED, 10) || 0; - const additions = parseInt(ADDITIONS, 10) || 0; - const deletions = parseInt(DELETIONS, 10) || 0; - - // Thresholds from inputs with fallback to defaults - const maxLines = parseInt(MAX_LINES, 10) || 1000; - const xsMaxSize = parseInt(XS_MAX_SIZE, 10) || 10; - const sMaxSize = parseInt(S_MAX_SIZE, 10) || 100; - const mMaxSize = parseInt(M_MAX_SIZE, 10) || 500; - const lMaxSize = parseInt(L_MAX_SIZE, 10) || 1000; - - // Print summary - console.log('Summary:'); - console.log(` - Additions: ${additions}`); - console.log(` - Deletions: ${deletions}`); - console.log(` - Total: ${total}`); - console.log(` - Limit: ${maxLines}`); - - // Determine size label based on configured criteria - let sizeLabel = ''; - if (total <= xsMaxSize) { - sizeLabel = 'size-XS'; - } else if (total <= sMaxSize) { - sizeLabel = 'size-S'; - } else if (total <= mMaxSize) { - sizeLabel = 'size-M'; - } else if (total <= lMaxSize) { - sizeLabel = 'size-L'; - } else { - sizeLabel = 'size-XL'; - } - - console.log(` - Size category: ${sizeLabel}`); - - // Manage PR labels - const owner = context.repo.owner; - const repo = context.repo.repo; - const issue_number = context.payload.pull_request.number; - - try { - const existingSizeLabels = ['size-XS', 'size-S', 'size-M', 'size-L', 'size-XL']; - - // Get current labels - const currentLabels = await github.rest.issues.listLabelsOnIssue({ - owner, - repo, - issue_number - }); - - const currentLabelNames = currentLabels.data.map(l => l.name); - - // Build new label set: keep non-size labels and add the new size label - const newLabels = currentLabelNames - .filter(name => !existingSizeLabels.includes(name)) // Remove all size labels - .concat(sizeLabel); // Add the correct size label - - // Check if labels need updating - const currentSizeLabel = currentLabelNames.find(name => existingSizeLabels.includes(name)); - if (currentSizeLabel === sizeLabel && currentLabelNames.length === newLabels.length) { - console.log(`✅ Correct label '${sizeLabel}' already present, no changes needed`); - } else { - // Update all labels in a single API call - await github.rest.issues.setLabels({ - owner, - repo, - issue_number, - labels: newLabels - }); - - if (currentSizeLabel && currentSizeLabel !== sizeLabel) { - console.log(` - Replaced '${currentSizeLabel}' with '${sizeLabel}'`); - } else if (!currentSizeLabel) { - console.log(`✅ Added '${sizeLabel}' label to PR #${issue_number}`); - } else { - console.log(`✅ Updated labels for PR #${issue_number}`); - } - } - } catch (error) { - console.log(`⚠️ Could not manage labels: ${error.message}`); - } - - // Check if exceeds limit - if (total > maxLines) { - console.log(`❌ Error: Total changed lines (${total}) exceed the limit of ${maxLines}.`); - process.exit(1); - } else { - console.log(`✅ Success: Total changed lines (${total}) are within the limit of ${maxLines}.`); - }