From 2eda80725a8d336368023ce10ee87f2f8fa24df8 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Thu, 20 Nov 2025 23:10:24 +0100 Subject: [PATCH] Refactor all workflows to be composite actions --- .../actions/add-item-to-project/action.yml | 48 ++++ .github/actions/add-team-label/action.yml | 37 +++ .github/actions/create-release-pr/action.yml | 158 +++++++++++++ .github/actions/flaky-test-report/action.yml | 51 +++++ .../actions/get-release-timelines/action.yml | 50 ++++ .../log-merge-group-failure/action.yml} | 37 +-- .github/actions/post-gh-rca/action.yml | 136 +++++++++++ .../actions/post-merge-validation/action.yml | 70 ++++++ .github/actions/pr-line-check/action.yml | 204 +++++++++++++++++ .../action.yml | 75 ++++++ .../remove-rca-needed-label-sheets/action.yml | 73 ++++++ .github/actions/stable-sync/action.yml | 141 ++++++++++++ .github/actions/stale-issue-pr/action.yml | 97 ++++++++ .../update-release-changelog/action.yml | 77 +++++++ .github/workflows/add-item-to-project.yml | 66 ------ .github/workflows/add-team-label-test.yml | 12 - .github/workflows/add-team-label.yml | 36 --- .github/workflows/create-release-pr.yml | 176 --------------- .github/workflows/flaky-test-report.yml | 54 ----- .../workflows/get-release-timelines-test.yml | 18 -- .github/workflows/get-release-timelines.yml | 39 ---- .github/workflows/post-gh-rca.yml | 151 ------------- .github/workflows/post-merge-validation.yml | 66 ------ .github/workflows/pr-line-check.yml | 213 ------------------ .../publish-slack-release-testing-status.yml | 70 ------ .../remove-rca-needed-label-sheets.yml | 84 ------- .github/workflows/stable-sync.yml | 146 +----------- .github/workflows/stale-issue-pr.yml | 99 -------- .github/workflows/test-add-team-label.yml | 18 ++ .../workflows/test-get-release-timelines.yml | 24 ++ .../workflows/update-release-changelog.yml | 85 ------- 31 files changed, 1285 insertions(+), 1326 deletions(-) create mode 100644 .github/actions/add-item-to-project/action.yml create mode 100644 .github/actions/add-team-label/action.yml create mode 100644 .github/actions/create-release-pr/action.yml create mode 100644 .github/actions/flaky-test-report/action.yml create mode 100644 .github/actions/get-release-timelines/action.yml rename .github/{workflows/log-merge-group-failure.yml => actions/log-merge-group-failure/action.yml} (71%) create mode 100644 .github/actions/post-gh-rca/action.yml create mode 100644 .github/actions/post-merge-validation/action.yml create mode 100644 .github/actions/pr-line-check/action.yml create mode 100644 .github/actions/publish-slack-release-testing-status/action.yml create mode 100644 .github/actions/remove-rca-needed-label-sheets/action.yml create mode 100644 .github/actions/stable-sync/action.yml create mode 100644 .github/actions/stale-issue-pr/action.yml create mode 100644 .github/actions/update-release-changelog/action.yml delete mode 100644 .github/workflows/add-item-to-project.yml delete mode 100644 .github/workflows/add-team-label-test.yml delete mode 100644 .github/workflows/add-team-label.yml delete mode 100644 .github/workflows/create-release-pr.yml delete mode 100644 .github/workflows/flaky-test-report.yml delete mode 100644 .github/workflows/get-release-timelines-test.yml delete mode 100644 .github/workflows/get-release-timelines.yml delete mode 100644 .github/workflows/post-gh-rca.yml delete mode 100644 .github/workflows/post-merge-validation.yml delete mode 100644 .github/workflows/pr-line-check.yml delete mode 100644 .github/workflows/publish-slack-release-testing-status.yml delete mode 100644 .github/workflows/remove-rca-needed-label-sheets.yml delete mode 100644 .github/workflows/stale-issue-pr.yml create mode 100644 .github/workflows/test-add-team-label.yml create mode 100644 .github/workflows/test-get-release-timelines.yml delete mode 100644 .github/workflows/update-release-changelog.yml diff --git a/.github/actions/add-item-to-project/action.yml b/.github/actions/add-item-to-project/action.yml new file mode 100644 index 00000000..a67ac18e --- /dev/null +++ b/.github/actions/add-item-to-project/action.yml @@ -0,0 +1,48 @@ +name: 'Add Issue/PR to Project By Team' + +inputs: + project-url: + description: 'URL of the GitHub Project where items should be added.' + required: true + team-name: + description: 'Team name to match for PR review_requested or requested_team.' + required: true + team-label: + description: 'Label that indicates the Issue/PR belongs to the team.' + required: true + filter-enabled: + description: 'If true, only add items that match the team criteria. If false, add every item.' + required: false + default: 'true' + github-token: + description: 'GitHub token with access to the project.' + required: true + +runs: + using: composite + steps: + - name: Add item to project board + uses: actions/add-to-project@v1.0.2 + # If filtering is disabled, the condition is always true. + # If filtering is enabled, then: + # - For PRs, check that the PR either has the specified team in requested_team + # or contains the team label. + # - For Issues, check that the issue contains the team label. + if: | + inputs.filter-enabled != 'true' || + ((github.event_name == 'pull_request' && + ( + github.event.requested_team.name == inputs.team-name || + contains(github.event.pull_request.labels.*.name, inputs.team-label) || + contains(github.event.pull_request.requested_teams.*.name, inputs.team-name) + ) + ) + || + (github.event_name == 'issues' && + ( + contains(github.event.issue.labels.*.name, inputs.team-label) + ) + )) + with: + project-url: ${{ inputs.project-url }} + github-token: ${{ inputs.github-token }} diff --git a/.github/actions/add-team-label/action.yml b/.github/actions/add-team-label/action.yml new file mode 100644 index 00000000..ca7fb27b --- /dev/null +++ b/.github/actions/add-team-label/action.yml @@ -0,0 +1,37 @@ +name: Add Team Label +description: "Adds a GitHub team label to a pull request based on the author's entry in the MetaMask topology file." + +inputs: + team-label-token: + description: 'GitHub token with access to read topology.json and add labels to PRs.' + required: true + +runs: + using: composite + steps: + # Fetch the team label for the PR author from topology.json and expose it as a step output. + - name: Get team label + id: get-team-label + env: + GH_TOKEN: ${{ inputs.team-label-token }} + USER: ${{ github.event.pull_request.user.login }} + shell: bash + run: | + # Stream topology.json through jq, find the first team where USER appears in members, pm, em, or tl, and emit + # its githubLabel.name value. + team_label=$(gh api -H 'Accept: application/vnd.github.raw' 'repos/metamask/metamask-planning/contents/topology.json' | jq -r --arg USER "$USER" '.[] | select(any(.members[]?; . == $USER) or (.pm // empty) == $USER or (.em // empty) == $USER or (.tl // empty) == $USER) | .githubLabel.name' | head -n 1) + if [ -z "$team_label" ]; then + echo "::error::Team label not found for author: $USER. Please open a pull request with your GitHub handle and team label to update topology.json at https://github.com/MetaMask/MetaMask-planning/blob/main/topology.json" + exit 1 + fi + echo "TEAM_LABEL=$team_label" >> "$GITHUB_OUTPUT" + + # Apply the retrieved label to the pull request using the GitHub CLI. + - name: Add team label + env: + GH_TOKEN: ${{ secrets.TEAM_LABEL_TOKEN }} + PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }} + TEAM_LABEL: ${{ steps.get-team-label.outputs.TEAM_LABEL }} + shell: bash + run: | + gh issue edit "$PULL_REQUEST_URL" --add-label "$TEAM_LABEL" diff --git a/.github/actions/create-release-pr/action.yml b/.github/actions/create-release-pr/action.yml new file mode 100644 index 00000000..160b8b72 --- /dev/null +++ b/.github/actions/create-release-pr/action.yml @@ -0,0 +1,158 @@ +name: Create Release Pull Request + +inputs: + checkout-base-branch: + required: true + description: 'The base branch, tag, or SHA for git operations.' + release-pr-base-branch: + required: true + description: 'The base branch, tag, or SHA for the release pull request.' + semver-version: + required: true + description: 'A semantic version, e.g.: "x.y.z".' + mobile-build-version: + required: false + description: 'The build version for the mobile platform.' + previous-version-ref: + required: true + description: 'Previous release version branch name, tag or commit hash (e.g., release/7.7.0, v7.7.0, or 76fbc500034db9779e9ff7ce637ac5be1da0493d). For hotfix releases, pass the literal string "null".' + mobile-template-sheet-id: + required: false + description: 'The Mobile testing sheet template id.' + default: '1012668681' # prod sheet template + extension-template-sheet-id: + required: false + description: 'The Extension testing sheet template id.' + default: '295804563' # prod sheet template + test-only: + required: false + description: 'If true, the release will be marked as a test release.' + default: 'false' + release-sheet-google-document-id: + required: false + description: 'The Google Document ID for the release notes.' + default: '1tsoodlAlyvEUpkkcNcbZ4PM9HuC9cEM80RZeoVv5OCQ' # Prod Release Document + platform: + required: true + description: 'The platform for which the release PR is being created. Must be one of: mobile, extension.' + git-user-name: + description: 'Git user name for commits. Defaults to metamaskbot.' + default: 'metamaskbot' + git-user-email: + description: 'Git user email for commits. Defaults to metamaskbot@users.noreply.github.com.' + default: 'metamaskbot@users.noreply.github.com' + github-token: + description: 'GitHub token used for authentication.' + required: true + google-application-creds-base64: + description: 'Google application credentials base64 encoded.' + required: true + github-tools-repository: + description: 'The GitHub repository containing the GitHub tools. Defaults to the GitHub tools action repositor, and usually does not need to be changed.' + required: false + default: ${{ github.action_repository }} + github-tools-ref: + description: 'The SHA of the action to use. Defaults to the current action ref, and usually does not need to be changed.' + required: false + default: ${{ github.action_ref }} + +runs: + using: composite + steps: + # Step 1: Checkout invoking repository (metamask-mobile | metamask-extension ) + - name: Checkout invoking repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ inputs.checkout-base-branch }} + token: ${{ inputs.github-token }} + + # Step 2: Checkout github-tools repository + - name: Checkout github-tools repository + uses: actions/checkout@v4 + with: + repository: ${{ inputs.github-tools-repository }} + ref: ${{ inputs.github-tools-ref }} + path: github-tools + + # Step 3: Setup environment + - name: Checkout and setup environment + uses: MetaMask/action-checkout-and-setup@v2 + with: + is-high-risk-environment: true + + # Step 4: Print Input Values + - name: Print Input Values + env: + PLATFORM: ${{ inputs.platform }} + CHECKOUT_BASE_BRANCH: ${{ inputs.checkout-base-branch }} + RELEASE_PR_BASE_BRANCH: ${{ inputs.release-pr-base-branch }} + SEMVER_VERSION: ${{ inputs.semver-version }} + PREVIOUS_VERSION_REF: ${{ inputs.previous-version-ref }} + TEST_ONLY: ${{ inputs.test-only }} + MOBILE_BUILD_VERSION: ${{ inputs.mobile-build-version }} + MOBILE_TEMPLATE_SHEET_ID: ${{ inputs.mobile-template-sheet-id }} + EXTENSION_TEMPLATE_SHEET_ID: ${{ inputs.extension-template-sheet-id }} + RELEASE_SHEET_GOOGLE_DOCUMENT_ID: ${{ inputs.release-sheet-google-document-id }} + GIT_USER_NAME: ${{ inputs.git-user-name }} + GIT_USER_EMAIL: ${{ inputs.git-user-email }} + shell: bash + run: | + echo "Input Values:" + echo "-------------" + echo "Platform: $PLATFORM" + echo "Checkout Base Branch: $CHECKOUT_BASE_BRANCH" + echo "Release PR Base Branch: $RELEASE_PR_BASE_BRANCH" + echo "Semver Version: $SEMVER_VERSION" + echo "Previous Version Reference: $PREVIOUS_VERSION_REF" + echo "Test Only Mode: $TEST_ONLY" + if [[ "$PLATFORM" == "mobile" ]]; then + echo "Mobile Build Version: $MOBILE_BUILD_VERSION" + fi + echo "Mobile Template Sheet ID: $MOBILE_TEMPLATE_SHEET_ID" + echo "Extension Template Sheet ID: $EXTENSION_TEMPLATE_SHEET_ID" + echo "Release Sheet Google Document ID: $RELEASE_SHEET_GOOGLE_DOCUMENT_ID" + echo "Git User Name: $GIT_USER_NAME" + echo "Git User Email: $GIT_USER_EMAIL" + echo "-------------" + + # Step 5: Create Release PR + - name: Create Release PR + id: create-release-pr + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + BASE_BRANCH: ${{ inputs.release-pr-base-branch }} + GITHUB_REPOSITORY_URL: '${{ github.server_url }}/${{ github.repository }}' + TEST_ONLY: ${{ inputs.test-only }} + GOOGLE_DOCUMENT_ID: ${{ inputs.release-sheet-google-document-id }} + GOOGLE_APPLICATION_CREDENTIALS_BASE64: ${{ secrets.google-application-creds-base64 }} + NEW_VERSION: ${{ inputs.semver-version }} + MOBILE_TEMPLATE_SHEET_ID: ${{ inputs.mobile-template-sheet-id }} + EXTENSION_TEMPLATE_SHEET_ID: ${{ inputs.extension-template-sheet-id }} + PLATFORM: ${{ inputs.platform }} + PREVIOUS_VERSION_REF: ${{ inputs.previous-version-ref }} + SEMVER_VERSION: ${{ inputs.semver-version }} + MOBILE_BUILD_VERSION: ${{ inputs.mobile-build-version }} + GIT_USER_NAME: ${{ inputs.git-user-name }} + GIT_USER_EMAIL: ${{ inputs.git-user-email }} + working-directory: ${{ github.workspace }} + run: | + # Execute the script from github-tools + ./github-tools/.github/scripts/create-platform-release-pr.sh \ + "$PLATFORM" \ + "$PREVIOUS_VERSION_REF" \ + "$SEMVER_VERSION" \ + "$MOBILE_BUILD_VERSION" \ + "$GIT_USER_NAME" \ + "$GIT_USER_EMAIL" + + # Step 6: Upload commits.csv as artifact (if generated) + - name: Upload commits.csv artifact + if: ${{ hashFiles('commits.csv') != '' }} + uses: actions/upload-artifact@v4 + with: + name: commits-csv + path: commits.csv + if-no-files-found: error + diff --git a/.github/actions/flaky-test-report/action.yml b/.github/actions/flaky-test-report/action.yml new file mode 100644 index 00000000..386468ff --- /dev/null +++ b/.github/actions/flaky-test-report/action.yml @@ -0,0 +1,51 @@ +name: Flaky Test Report + +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/get-release-timelines/action.yml b/.github/actions/get-release-timelines/action.yml new file mode 100644 index 00000000..ae77e233 --- /dev/null +++ b/.github/actions/get-release-timelines/action.yml @@ -0,0 +1,50 @@ +name: Get Release Timelines + +inputs: + version: + required: true + description: The version of the release. + github-token: + required: true + description: The GitHub token used for authentication. + runway-app-id: + required: true + description: The Runway application ID. + runway-api-key: + required: true + description: The Runway API key. + github-tools-repository: + description: 'The GitHub repository containing the GitHub tools. Defaults to the GitHub tools action repositor, and usually does not need to be changed.' + required: false + default: ${{ github.action_repository }} + github-tools-ref: + description: 'The SHA of the action to use. Defaults to the current action ref, and usually does not need to be changed.' + required: false + default: ${{ github.action_ref }} + +runs: + using: composite + steps: + - name: Checkout GitHub tools repository + uses: actions/checkout@v5 + with: + repository: ${{ inputs.github-tools-repository }} + ref: ${{ inputs.github-tools-ref }} + path: ./github-tools + + - name: Get release timelines + env: + OWNER: ${{ github.repository_owner }} + REPOSITORY: ${{ github.event.repository.name }} + VERSION: ${{ inputs.version }} + RUNWAY_APP_ID: ${{ inputs.runway-app-id }} + RUNWAY_API_KEY: ${{ inputs.runway-api-key }} + GH_TOKEN: ${{ inputs.github-token }} + shell: bash + run: ./github-tools/.github/scripts/get-release-timelines.sh + + - name: Upload artifact release-timelines-${{ inputs.version }}.csv + uses: actions/upload-artifact@v4 + with: + name: release-timelines-${{ inputs.version }}.csv + path: release-timelines-${{ inputs.version }}.csv diff --git a/.github/workflows/log-merge-group-failure.yml b/.github/actions/log-merge-group-failure/action.yml similarity index 71% rename from .github/workflows/log-merge-group-failure.yml rename to .github/actions/log-merge-group-failure/action.yml index 9da867a3..666827b8 100644 --- a/.github/workflows/log-merge-group-failure.yml +++ b/.github/actions/log-merge-group-failure/action.yml @@ -1,17 +1,18 @@ -name: Log merge group failure +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: +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 jobs: log-merge-group-failure: @@ -25,16 +26,16 @@ jobs: - name: Create service_account.json env: - GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} - GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} + GOOGLE_APPLICATION_CREDENTIALS: ${{ inputs.google-application-credentials }} + GOOGLE_SERVICE_ACCOUNT: ${{ inputs.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 }} + GOOGLE_APPLICATION_CREDENTIALS: ${{ inputs.google-application-credentials }} + SPREADSHEET_ID: ${{ inputs.spreadsheet-id }} + SHEET_NAME: ${{ inputs.sheet-name }} run: | current_date=$(date +%Y-%m-%d) token=$(oauth2l fetch --scope https://www.googleapis.com/auth/spreadsheets) diff --git a/.github/actions/post-gh-rca/action.yml b/.github/actions/post-gh-rca/action.yml new file mode 100644 index 00000000..913b0a18 --- /dev/null +++ b/.github/actions/post-gh-rca/action.yml @@ -0,0 +1,136 @@ +name: Post RCA Form + +inputs: + google-form-base-url: + description: Base URL of the Google Form. + default: 'https://docs.google.com/forms/d/e/1FAIpQLSeLOVVUy7mO1j-5Isb04OAWk3dM0b1NY1R8kf0tiEBs9elcEg/viewform?usp=pp_url' + repo-owner: + description: The repo owner + required: true + repo-name: + description: The repo name + required: true + issue-number: + description: The number of the closed issue + required: true + issue-labels: + description: JSON-stringified array of labels that should trigger the RCA prompt + required: true + entry-issue: + description: The entry ID for the issue field in the Google Form + default: 'entry.1417567074' + entry-regression: + description: The entry ID for the regression field in the Google Form + default: 'entry.1470697156' + entry-team: + description: The entry ID for the team field in the Google Form + default: 'entry.1198657478' + entry-repo-name: + description: The entry ID for the repository name field + default: 'entry.1085838323' + entry-issue-url: + description: The entry ID for the GitHub issue URL field + default: 'entry.516762472' + github-token: + description: GitHub token for authentication + required: true + +runs: + using: composite + steps: + - name: Post RCA Form Link + uses: actions/github-script@v8 + env: + GOOGLE_FORM_BASE_URL: ${{ inputs.google-form-base-url }} + ISSUE_LABELS: ${{ inputs.issue-labels }} + OWNER_NAME: ${{ inputs.repo-owner }} + REPO_NAME: ${{ inputs.repo-name }} + ISSUE_NUMBER: ${{ inputs.issue-number }} + ENTRY_ISSUE: ${{ inputs.entry-issue }} + ENTRY_REGRESSION: ${{ inputs.entry-regression }} + ENTRY_TEAM: ${{ inputs.entry-team }} + ENTRY_REPO_NAME: ${{ inputs.entry-repo-name }} + ENTRY_ISSUE_URL: ${{ inputs.entry-issue-url }} + with: + github-token: ${{ inputs.github-token }} + script: | + const { + GOOGLE_FORM_BASE_URL: baseUrl, + ENTRY_ISSUE, + ENTRY_REGRESSION, + ENTRY_TEAM, + ENTRY_REPO_NAME, + ENTRY_ISSUE_URL, + OWNER_NAME: owner, + REPO_NAME: repo, + ISSUE_NUMBER: issueNumStr, + } = process.env; + + const issue_number = parseInt(issueNumStr, 10); + const allowedLabels = JSON.parse(process.env.ISSUE_LABELS); + + // Fetch issue details to get the assignees + const { data: issue } = await github.rest.issues.get({ + owner, + repo, + issue_number: issue_number, + }); + + const hasAllowedLabel = issue.labels.some(label => + allowedLabels.includes(label.name) + ); + + if (!hasAllowedLabel) { + console.log(`❌ Issue #${issue_number} skipped — no matching label.`); + return; + } + + // if it's a sev1-high or sev0-high, lets grab team and regression labels, if there's any + // if there's none, an empty value will be sent, which is what we want + const teamLabels = issue.labels + .map(l => l.name) + .filter(n => n.startsWith('team-')); + + const regressionLabels = issue.labels + .map(l => l.name) + .filter(n => n.startsWith('regression-')); + + const formUrl = new URL(baseUrl); + formUrl.searchParams.set(ENTRY_ISSUE, issue_number); + formUrl.searchParams.set( + ENTRY_REGRESSION, + regressionLabels.length ? regressionLabels.join(',') : '' + ); + formUrl.searchParams.set( + ENTRY_TEAM, + teamLabels.length ? teamLabels.join(',') : '' + ); + + formUrl.searchParams.set(ENTRY_REPO_NAME, repo); + formUrl.searchParams.set(ENTRY_ISSUE_URL, `https://github.com/${owner}/${repo}/issues/${issue_number}`); + + const assignees = issue.assignees.map(u=>`@${u.login}`).join(', '); + const body = `Hi ${assignees}, + + This issue has been closed. Please complete this RCA form: + ${formUrl.toString()} + + `; + + await github.rest.issues.createComment({ + owner, repo, issue_number, body + }); + console.log(`✅ Comment posted on issue #${issue_number}`); + + // Add the RCA-needed label + try { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: issue_number, + labels: ['RCA-needed'] + }); + console.log(`✅ Added 'RCA-needed' label on issue #${issue_number}`); + } catch (error) { + console.log(`⚠️ Could not add label: ${error.message}`); + } diff --git a/.github/actions/post-merge-validation/action.yml b/.github/actions/post-merge-validation/action.yml new file mode 100644 index 00000000..a38c56e7 --- /dev/null +++ b/.github/actions/post-merge-validation/action.yml @@ -0,0 +1,70 @@ +name: Post Merge Validation + +inputs: + repo: + description: 'The repo owner/name to process (e.g. MetaMask/metamask-extension)' + required: true + start-hour-utc: + description: 'The hour of the day (UTC) to start processing the PRs merged in main' + required: true + spreadsheet-id: + description: 'Google Spreadsheet ID to update' + required: false + default: '1tsoodlAlyvEUpkkcNcbZ4PM9HuC9cEM80RZeoVv5OCQ' + lookback-days: + description: 'Number of days to look back for PRs' + required: false + default: '1' + github-token: + description: 'GitHub token with repo access' + required: true + google-application-creds-base64: + description: 'Base64 encoded Google service account credentials' + required: true + github-tools-repository: + description: 'The GitHub repository containing the GitHub tools. Defaults to the GitHub tools action repositor, and usually does not need to be changed.' + required: false + default: ${{ github.action_repository }} + github-tools-ref: + description: 'The SHA of the action to use. Defaults to the current action ref, and usually does not need to be changed.' + required: false + default: ${{ github.action_ref }} + +runs: + using: composite + steps: + - name: Checkout GitHub tools repository + uses: actions/checkout@v5 + with: + repository: ${{ inputs.github-tools-repository }} + ref: ${{ inputs.github-tools-ref }} + 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 + shell: bash + run: corepack enable + working-directory: ./github-tools + + - name: Install dependencies + working-directory: ./github-tools + shell: bash + run: yarn --immutable + + - name: Run post-merge-validation script + working-directory: ./github-tools + env: + SHEET_ID: ${{ inputs.spreadsheet-id }} + START_HOUR_UTC: ${{ inputs.start-hour-utc }} + LOOKBACK_DAYS: ${{ inputs.lookback-days }} + REPO: ${{ inputs.repo }} + GITHUB_TOKEN: ${{ inputs.github-token }} + GOOGLE_APPLICATION_CREDENTIALS_BASE64: ${{ inputs.google-application-creds-base64 }} + shell: bash + run: node .github/scripts/post-merge-validation-tracker.mjs diff --git a/.github/actions/pr-line-check/action.yml b/.github/actions/pr-line-check/action.yml new file mode 100644 index 00000000..9811e0c5 --- /dev/null +++ b/.github/actions/pr-line-check/action.yml @@ -0,0 +1,204 @@ +name: Check PR Lines Changed + +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 }} + 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 + + # 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}.`); + } diff --git a/.github/actions/publish-slack-release-testing-status/action.yml b/.github/actions/publish-slack-release-testing-status/action.yml new file mode 100644 index 00000000..65ef9133 --- /dev/null +++ b/.github/actions/publish-slack-release-testing-status/action.yml @@ -0,0 +1,75 @@ +name: Publish Slack Release Testing Status + +inputs: + platform: + description: 'The platform for which the release testing status is being published (e.g., mobile, extension).' + required: true + google-document-id: + description: 'The ID of the Google Document containing the release testing status.' + required: true + test-only: + description: 'If set to true, the action will only run in test mode and not publish to production Slack channels.' + required: false + default: 'false' + slack-api-key: + description: 'The API key for Slack to post the release testing status.' + required: true + github-token: + description: 'The GitHub token for authentication.' + required: true + google-application-creds-base64: + description: 'Base64 encoded Google application credentials for accessing Google Docs.' + required: true + github-tools-repository: + description: 'The GitHub repository containing the GitHub tools. Defaults to the GitHub tools action repositor, and usually does not need to be changed.' + required: false + default: ${{ github.action_repository }} + github-tools-ref: + description: 'The SHA of the action to use. Defaults to the current action ref, and usually does not need to be changed.' + required: false + default: ${{ github.action_ref }} + +jobs: + publish-status: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout GitHub tools repository + uses: actions/checkout@v5 + with: + repository: ${{ inputs.github-tools-repository }} + ref: ${{ inputs.github-tools-ref }} + 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 + shell: bash + working-directory: ./github-tools + + - name: Install dependencies + run: yarn --immutable + shell: bash + working-directory: ./github-tools + + - name: Publish Slack Release Testing Status + id: publish-slack-release-testing-status + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + SLACK_API_KEY: ${{ inputs.slack-api-key }} + GOOG_DOCUMENT_ID: ${{ inputs.google-document-id }} + GOOGLE_APPLICATION_CREDENTIALS_BASE64: ${{ inputs.google-application-creds-base64 }} + TEST_ONLY: ${{ inputs.test-only }} + PLATFORM: ${{ inputs.platform }} + working-directory: ./github-tools + run: | + yarn run slack:release-testing diff --git a/.github/actions/remove-rca-needed-label-sheets/action.yml b/.github/actions/remove-rca-needed-label-sheets/action.yml new file mode 100644 index 00000000..176776cd --- /dev/null +++ b/.github/actions/remove-rca-needed-label-sheets/action.yml @@ -0,0 +1,73 @@ +name: Remove RCA-needed Label + +inputs: + dry-run: + description: 'Run in dry-run mode (no changes made)' + required: false + default: 'false' + spreadsheet-id: + description: 'Google Spreadsheet ID (must be provided by consuming repository)' + required: true + sheet-name: + description: 'Sheet tab name (uses default if not provided)' + required: false + default: 'Form Responses 1' + github-token: + description: 'Github token with issues write permissions' + required: true + google-application-creds-base64: + description: 'Base64 encoded Google application service account credentials' + required: true + github-tools-repository: + description: 'The GitHub repository containing the GitHub tools. Defaults to the GitHub tools action repositor, and usually does not need to be changed.' + required: false + default: ${{ github.action_repository }} + github-tools-ref: + description: 'The SHA of the action to use. Defaults to the current action ref, and usually does not need to be changed.' + required: false + default: ${{ github.action_ref }} + +runs: + using: composite + steps: + - name: Checkout consuming repository + uses: actions/checkout@v5 + with: + token: ${{ inputs.github-token }} + + - name: Checkout GitHub tools repository + uses: actions/checkout@v5 + with: + repository: ${{ inputs.github-tools-repository }} + ref: ${{ inputs.github-tools-ref }} + path: ./github-tools + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + + - name: Run RCA Google Sheets check + shell: bash + run: | + # Move to github-tools directory where our script lives + cd github-tools/.github/scripts + + # Create a simple package.json for npm to work with + echo '{}' > package.json + + # Install exact versions of required packages locally + npm install --no-save --no-package-lock \ + @actions/core@1.10.1 \ + @actions/github@6.0.0 \ + googleapis@144.0.0 \ + tsx@4.7.1 + + # Run the script with tsx + npx tsx remove-rca-needed-label-sheets.ts + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + GOOGLE_SHEETS_CREDENTIALS: ${{ inputs.google-application-creds-base64 }} + DRY_RUN: ${{ inputs.dry-run }} + SPREADSHEET_ID: ${{ inputs.spreadsheet-id }} + SHEET_NAME: ${{ inputs.sheet-name }} diff --git a/.github/actions/stable-sync/action.yml b/.github/actions/stable-sync/action.yml new file mode 100644 index 00000000..96968f96 --- /dev/null +++ b/.github/actions/stable-sync/action.yml @@ -0,0 +1,141 @@ +name: Stable Sync + +inputs: + semver-version: + required: true + description: 'The semantic version to use for the sync (e.g., x.x.x)' + repo-type: + required: false + description: 'Type of repository (mobile or extension)' + default: 'mobile' + stable-branch-name: + required: false + description: 'The name of the stable branch to sync to (e.g., stable, main)' + default: 'stable' + github-tools-repository: + description: 'The GitHub repository containing the GitHub tools. Defaults to the GitHub tools action repositor, and usually does not need to be changed.' + required: false + default: ${{ github.action_repository }} + github-tools-ref: + description: 'The SHA of the action to use. Defaults to the current action ref, and usually does not need to be changed.' + required: false + default: ${{ github.action_ref }} + +runs: + using: composite + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Checkout GitHub tools repository + uses: actions/checkout@v5 + with: + repository: ${{ inputs.github-tools-repository }} + ref: ${{ inputs.github-tools-ref }} + path: ./github-tools + + - name: Setup Node.js Mobile + if: ${{ inputs.repo-type == 'mobile' }} + uses: actions/setup-node@v6 + with: + node-version: '18' + + - name: Setup Node.js Extension + if: ${{ inputs.repo-type == 'extension' }} + uses: actions/setup-node@v6 + with: + node-version: '22.15' + + - name: Prepare Yarn + if: ${{ inputs.repo-type == 'extension' }} + shell: bash + run: corepack prepare yarn@4.5.1 --activate + + - name: Prepare Yarn - Enable corepack + if: ${{ inputs.repo-type == 'extension' }} + shell: bash + run: corepack enable + + - name: Check if PR exists + id: check-pr + uses: actions/github-script@v7 + with: + script: | + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + head: `${context.repo.owner}:stable-main-${process.env.SEMVER_VERSION}`, + base: 'main' + }); + return prs.length > 0; + env: + SEMVER_VERSION: ${{ inputs.semver-version }} + + - name: Set Git user and email + shell: bash + run: | + git config --global user.name "metamaskbot" + git config --global user.email "metamaskbot@users.noreply.github.com" + + - name: Run stable sync + id: run-stable-sync + # if: steps.check-pr.outputs.result != 'true' + env: + CREATE_BRANCH: 'false' # let the script handle the branch creation + REPO: ${{ inputs.repo-type }} # Default to 'mobile' if not specified + BASE_BRANCH: ${{ inputs.stable-branch-name }} + SEMVER_VERSION: ${{ inputs.semver-version }} + shell: bash + run: | + # Ensure github-tools is in .gitignore to prevent it from being committed + if ! grep -q "^github-tools/" .gitignore 2>/dev/null; then + echo "github-tools/" >> .gitignore + echo "Added github-tools/ to .gitignore" + fi + + # Execute the script from github-tools + node ./github-tools/.github/scripts/stable-sync.js "stable-main-$SEMVER_VERSION" + BRANCH_NAME="stable-main-$SEMVER_VERSION" + if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then + echo "Branch $BRANCH_NAME exists remotely, force pushing to overwrite" + git push origin "$BRANCH_NAME" --force + else + echo "Branch $BRANCH_NAME doesn't exist remotely, pushing with --set-upstream" + git push --set-upstream origin "$BRANCH_NAME" + fi + + - name: Create Pull Request + if: steps.check-pr.outputs.result != 'true' + env: + GITHUB_TOKEN: ${{ secrets.github-token }} + BRANCH_NAME: stable-main-${{ inputs.semver-version }} + VERSION: ${{ inputs.semver-version }} + shell: bash + run: | + # Create PR using GitHub CLI + gh pr create \ + --title "release: sync stable to main for version $VERSION" \ + --body "This PR syncs the stable branch to main for version $VERSION. + + *Synchronization Process:* + + - Fetches the latest changes from the remote repository + - Resets the branch to match the stable branch + - Attempts to merge changes from main into the branch + - Handles merge conflicts if they occur + + *File Preservation:* + + Preserves specific files from the stable branch: + - CHANGELOG.md + - bitrise.yml + - android/app/build.gradle + - ios/MetaMask.xcodeproj/project.pbxproj + - package.json + + Indicates the next version candidate of main to $VERSION" \ + --base main \ + --head "$BRANCH_NAME" + #--label "sync" \ + #--label "stable" diff --git a/.github/actions/stale-issue-pr/action.yml b/.github/actions/stale-issue-pr/action.yml new file mode 100644 index 00000000..77fba7fd --- /dev/null +++ b/.github/actions/stale-issue-pr/action.yml @@ -0,0 +1,97 @@ +name: 'Close stale issues and PRs' + +inputs: + stale-issue-message: + description: 'Message to post when marking an issue as stale' + required: false + default: 'This issue has been automatically marked as stale because it has not had recent activity in the last 30 days. It will be closed in 60 days. Thank you for your contributions.' + type: string + close-issue-message: + description: 'Message to post when closing a stale issue' + required: false + default: 'This issue was closed because there has been no follow activity in 90 days. If you feel this was closed in error please provide evidence on the current production app in a new issue or comment in the existing issue to a maintainer. Thank you for your contributions.' + type: string + stale-issue-label: + description: 'Label to use when marking an issue as stale' + required: false + default: 'stale' + type: string + any-of-issue-labels: + description: 'Comma-separated list of labels to check for issues' + required: false + default: 'needs-information, needs-reproduction' + type: string + exempt-issue-labels: + description: 'Comma-separated list of labels that exempt issues from being marked as stale' + required: false + default: 'type-security, feature-request, Sev1-high, needs-triage' + type: string + days-before-issue-stale: + description: 'Number of days of inactivity before an issue becomes stale' + required: false + default: 30 + type: number + days-before-issue-close: + description: 'Number of days of inactivity before a stale issue is closed' + required: false + default: 60 + type: number + stale-pr-message: + description: 'Message to post when marking a PR as stale' + required: false + default: 'This PR has been automatically marked as stale because it has not had recent activity in the last 90 days. It will be closed in 7 days. Thank you for your contributions.' + type: string + stale-pr-label: + description: 'Label to use when marking a PR as stale' + required: false + default: 'stale' + type: string + exempt-pr-labels: + description: 'Comma-separated list of labels that exempt PRs from being marked as stale' + required: false + default: 'work-in-progress, external-contributor' + type: string + close-pr-message: + description: 'Message to post when closing a stale PR' + required: false + default: 'This PR was closed because there has been no follow up activity in 7 days. Thank you for your contributions.' + type: string + days-before-pr-stale: + description: 'Number of days of inactivity before a PR becomes stale' + required: false + default: 90 + type: number + days-before-pr-close: + description: 'Number of days of inactivity before a stale PR is closed' + required: false + default: 7 + type: number + operations-per-run: + description: 'Maximum number of operations to perform per run' + required: false + default: 200 + type: number + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v10 + with: + stale-issue-message: ${{ inputs.stale-issue-message }} + close-issue-message: ${{ inputs.close-issue-message }} + stale-issue-label: ${{ inputs.stale-issue-label }} + any-of-issue-labels: ${{ inputs.any-of-issue-labels }} + exempt-issue-labels: ${{ inputs.exempt-issue-labels }} + days-before-issue-stale: ${{ inputs.days-before-issue-stale }} + days-before-issue-close: ${{ inputs.days-before-issue-close }} + stale-pr-message: ${{ inputs.stale-pr-message }} + stale-pr-label: ${{ inputs.stale-pr-label }} + exempt-pr-labels: ${{ inputs.exempt-pr-labels }} + close-pr-message: ${{ inputs.close-pr-message }} + days-before-pr-stale: ${{ inputs.days-before-pr-stale }} + days-before-pr-close: ${{ inputs.days-before-pr-close }} + operations-per-run: ${{ inputs.operations-per-run }} diff --git a/.github/actions/update-release-changelog/action.yml b/.github/actions/update-release-changelog/action.yml new file mode 100644 index 00000000..044a8987 --- /dev/null +++ b/.github/actions/update-release-changelog/action.yml @@ -0,0 +1,77 @@ +name: Update Release Changelog + +inputs: + release-branch: + required: true + type: string + description: 'Release branch name (e.g., Version-v13.3.0 or release/6.20.0).' + platform: + required: false + type: string + default: 'extension' + description: 'Target platform (extension | mobile). Defaults to extension.' + repository-url: + required: true + type: string + description: 'Full HTTPS URL for the invoking repository.' + previous-version-ref: + required: false + type: string + default: 'null' + description: 'Previous release version reference (branch/tag/SHA). Use "null" for hotfixes.' + github-tools-version: + required: false + type: string + default: 'main' + description: 'Version of github-tools to use (branch, tag, or SHA).' + github-token: + required: true + description: 'GitHub token with write access to the invoking repository and read access to planning resources.' + +runs: + using: composite + steps: + # Step 1: Checkout invoking repository (metamask-mobile | metamask-extension) + - name: Checkout invoking repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ inputs.release-branch }} + token: ${{ inputs.github-token }} + + # Step 2: Checkout github-tools repository + - name: Checkout github-tools repository + uses: actions/checkout@v4 + with: + repository: MetaMask/github-tools + ref: ${{ inputs.github-tools-version }} + path: github-tools + + # Step 3: Setup environment from github-tools + - name: Checkout and setup environment + uses: ./github-tools/.github/actions/checkout-and-setup + with: + is-high-risk-environment: true + + # Step 4: Update changelog using shared helper script + - name: Update changelog branch + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + GIT_AUTHOR_NAME: metamaskbot + GIT_AUTHOR_EMAIL: metamaskbot@users.noreply.github.com + RELEASE_BRANCH: ${{ inputs.release-branch }} + PLATFORM: ${{ inputs.platform }} + REPOSITORY_URL: ${{ inputs.repository-url }} + PREVIOUS_VERSION_REF: ${{ inputs.previous-version-ref }} + run: | + set -euo pipefail + + corepack enable + yarn install --immutable + + ./github-tools/.github/scripts/update-release-changelog.sh \ + "$RELEASE_BRANCH" \ + "$PLATFORM" \ + "$REPOSITORY_URL" \ + "$PREVIOUS_VERSION_REF" diff --git a/.github/workflows/add-item-to-project.yml b/.github/workflows/add-item-to-project.yml deleted file mode 100644 index 328f30df..00000000 --- a/.github/workflows/add-item-to-project.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: 'Add Issue/PR to Project by team' - -on: - workflow_call: - inputs: - project-url: - description: 'URL of the GitHub Project where items should be added' - required: true - type: string - team-name: - description: 'Team name to match for PR review_requested or requested_team' - required: true - type: string - team-label: - description: 'Label that indicates the Issue/PR belongs to the team' - required: true - type: string - filter-enabled: - description: 'If true, only add items that match the team criteria. If false, add every item.' - required: false - type: boolean - default: true - secrets: - github-token: - description: 'GitHub token with permissions to add items to projects' - required: true - -jobs: - add_to_project: - name: 'Add to Project Board' - runs-on: ubuntu-latest - - # Ensure we have permissions to read issues/PRs and create project issues - # (some repos may need 'contents: read', 'issues: write', 'pull-requests: write', etc.) - permissions: - contents: read - issues: write - pull-requests: write - # If you are using "classic" Projects, also add: projects: write - - steps: - - name: Add item to project board - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e - # If filtering is disabled, the condition is always true. - # If filtering is enabled, then: - # - For PRs, check that the PR either has the specified team in requested_team - # or contains the team label. - # - For Issues, check that the issue contains the team label. - if: | - !inputs.filter-enabled || - ((github.event_name == 'pull_request' && - ( - github.event.requested_team.name == inputs.team-name || - contains(github.event.pull_request.labels.*.name, inputs.team-label) || - contains(github.event.pull_request.requested_teams.*.name, inputs.team-name) - ) - ) - || - (github.event_name == 'issues' && - ( - contains(github.event.issue.labels.*.name, inputs.team-label) - ) - )) - with: - project-url: ${{ inputs.project-url }} - github-token: ${{ secrets.github-token }} diff --git a/.github/workflows/add-team-label-test.yml b/.github/workflows/add-team-label-test.yml deleted file mode 100644 index 1f65dbe3..00000000 --- a/.github/workflows/add-team-label-test.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Add team label - -on: - pull_request: - types: - - opened - -jobs: - add-team-label: - uses: ./.github/workflows/add-team-label.yml - secrets: - TEAM_LABEL_TOKEN: ${{ secrets.TEAM_LABEL_TOKEN }} diff --git a/.github/workflows/add-team-label.yml b/.github/workflows/add-team-label.yml deleted file mode 100644 index 132fc6d3..00000000 --- a/.github/workflows/add-team-label.yml +++ /dev/null @@ -1,36 +0,0 @@ -# Adds a GitHub team label to a pull request based on the author's entry in the MetaMask topology file. -name: Add team label - -on: - workflow_call: - secrets: - TEAM_LABEL_TOKEN: - required: true - -jobs: - add-team-label: - runs-on: ubuntu-latest - steps: - # Fetch the team label for the PR author from topology.json and expose it as a step output. - - name: Get team label - id: get-team-label - env: - GH_TOKEN: ${{ secrets.TEAM_LABEL_TOKEN }} - USER: ${{ github.event.pull_request.user.login }} - run: | - # Stream topology.json through jq, find the first team where USER appears in members, pm, em, or tl, and emit its githubLabel.name value. - team_label=$(gh api -H 'Accept: application/vnd.github.raw' 'repos/metamask/metamask-planning/contents/topology.json' | jq -r --arg USER "$USER" '.[] | select(any(.members[]?; . == $USER) or (.pm // empty) == $USER or (.em // empty) == $USER or (.tl // empty) == $USER) | .githubLabel.name' | head -n 1) - if [ -z "$team_label" ]; then - echo "::error::Team label not found for author: $USER. Please open a pull request with your GitHub handle and team label to update topology.json at https://github.com/MetaMask/MetaMask-planning/blob/main/topology.json" - exit 1 - fi - echo 'TEAM_LABEL='"$team_label" >> "$GITHUB_OUTPUT" - - # Apply the retrieved label to the pull request using the GitHub CLI. - - name: Add team label - env: - GH_TOKEN: ${{ secrets.TEAM_LABEL_TOKEN }} - PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }} - TEAM_LABEL: ${{ steps.get-team-label.outputs.TEAM_LABEL }} - run: | - gh issue edit "$PULL_REQUEST_URL" --add-label "$TEAM_LABEL" diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml deleted file mode 100644 index d0417e52..00000000 --- a/.github/workflows/create-release-pr.yml +++ /dev/null @@ -1,176 +0,0 @@ -name: Create Release Pull Request - -on: - workflow_call: - inputs: - checkout-base-branch: - required: true - type: string - description: 'The base branch, tag, or SHA for git operations.' - release-pr-base-branch: - required: true - type: string - description: 'The base branch, tag, or SHA for the release pull request.' - semver-version: - required: true - type: string - description: 'A semantic version. eg: x.x.x' - mobile-build-version: - required: false - type: string - description: 'The build version for the mobile platform.' - previous-version-ref: - required: true - type: string - description: 'Previous release version branch name, tag or commit hash (e.g., release/7.7.0, v7.7.0, or 76fbc500034db9779e9ff7ce637ac5be1da0493d). For hotfix releases, pass the literal string "null".' - # Flag to indicate if the release is a test release for development purposes only - mobile-template-sheet-id: - required: false - type: string - description: 'The Mobile testing sheet template id.' - default: '1012668681' # prod sheet template - extension-template-sheet-id: - required: false - type: string - description: 'The Extension testing sheet template id.' - default: '295804563' # prod sheet template - test-only: - required: false - type: string - description: 'If true, the release will be marked as a test release.' - default: 'false' - # possible values are [ mobile, extension ] - release-sheet-google-document-id: - required: false - type: string - description: 'The Google Document ID for the release notes.' - default: '1tsoodlAlyvEUpkkcNcbZ4PM9HuC9cEM80RZeoVv5OCQ' # Prod Release Document - platform: - required: true - type: string - description: 'The platform for which the release PR is being created.' - github-tools-version: - type: string - description: 'The version of github-tools to use. Defaults to main.' - default: 'main' - git-user-name: - type: string - description: 'Git user name for commits. Defaults to metamaskbot.' - default: 'metamaskbot' - git-user-email: - type: string - description: 'Git user email for commits. Defaults to metamaskbot@users.noreply.github.com.' - default: 'metamaskbot@users.noreply.github.com' - secrets: - github-token: - required: true - description: 'GitHub token used for authentication.' - google-application-creds-base64: - required: true - description: 'Google application credentials base64 encoded.' - -jobs: - create-release-pr: - name: Create Release Pull Request - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - # Step 1: Checkout invoking repository (metamask-mobile | metamask-extension ) - - name: Checkout invoking repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ inputs.checkout-base-branch }} - token: ${{ secrets.github-token }} - - # Step 2: Checkout github-tools repository - - name: Checkout github-tools repository - uses: actions/checkout@v4 - with: - repository: MetaMask/github-tools - ref: ${{ inputs.github-tools-version }} - path: github-tools - - # Step 3: Setup environment from github-tools - - name: Checkout and setup environment - uses: ./github-tools/.github/actions/checkout-and-setup - with: - is-high-risk-environment: true - - # Step 4: Print Input Values - - name: Print Input Values - env: - PLATFORM: ${{ inputs.platform }} - CHECKOUT_BASE_BRANCH: ${{ inputs.checkout-base-branch }} - RELEASE_PR_BASE_BRANCH: ${{ inputs.release-pr-base-branch }} - SEMVER_VERSION: ${{ inputs.semver-version }} - PREVIOUS_VERSION_REF: ${{ inputs.previous-version-ref }} - TEST_ONLY: ${{ inputs.test-only }} - MOBILE_BUILD_VERSION: ${{ inputs.mobile-build-version }} - MOBILE_TEMPLATE_SHEET_ID: ${{ inputs.mobile-template-sheet-id }} - EXTENSION_TEMPLATE_SHEET_ID: ${{ inputs.extension-template-sheet-id }} - RELEASE_SHEET_GOOGLE_DOCUMENT_ID: ${{ inputs.release-sheet-google-document-id }} - GITHUB_TOOLS_VERSION: ${{ inputs.github-tools-version }} - GIT_USER_NAME: ${{ inputs.git-user-name }} - GIT_USER_EMAIL: ${{ inputs.git-user-email }} - run: | - echo "Input Values:" - echo "-------------" - echo "Platform: $PLATFORM" - echo "Checkout Base Branch: $CHECKOUT_BASE_BRANCH" - echo "Release PR Base Branch: $RELEASE_PR_BASE_BRANCH" - echo "Semver Version: $SEMVER_VERSION" - echo "Previous Version Reference: $PREVIOUS_VERSION_REF" - echo "Test Only Mode: $TEST_ONLY" - if [[ "$PLATFORM" == "mobile" ]]; then - echo "Mobile Build Version: $MOBILE_BUILD_VERSION" - fi - echo "Mobile Template Sheet ID: $MOBILE_TEMPLATE_SHEET_ID" - echo "Extension Template Sheet ID: $EXTENSION_TEMPLATE_SHEET_ID" - echo "Release Sheet Google Document ID: $RELEASE_SHEET_GOOGLE_DOCUMENT_ID" - echo "GitHub Tools Version: $GITHUB_TOOLS_VERSION" - echo "Git User Name: $GIT_USER_NAME" - echo "Git User Email: $GIT_USER_EMAIL" - echo "-------------" - - # Step 5: Create Release PR - - name: Create Release PR - id: create-release-pr - shell: bash - env: - GITHUB_TOKEN: ${{ secrets.github-token }} - BASE_BRANCH: ${{ inputs.release-pr-base-branch }} - GITHUB_REPOSITORY_URL: '${{ github.server_url }}/${{ github.repository }}' - TEST_ONLY: ${{ inputs.test-only }} - GOOGLE_DOCUMENT_ID: ${{ inputs.release-sheet-google-document-id }} - GOOGLE_APPLICATION_CREDENTIALS_BASE64: ${{ secrets.google-application-creds-base64 }} - NEW_VERSION: ${{ inputs.semver-version }} - MOBILE_TEMPLATE_SHEET_ID: ${{ inputs.mobile-template-sheet-id }} - EXTENSION_TEMPLATE_SHEET_ID: ${{ inputs.extension-template-sheet-id }} - PLATFORM: ${{ inputs.platform }} - PREVIOUS_VERSION_REF: ${{ inputs.previous-version-ref }} - SEMVER_VERSION: ${{ inputs.semver-version }} - MOBILE_BUILD_VERSION: ${{ inputs.mobile-build-version }} - GIT_USER_NAME: ${{ inputs.git-user-name }} - GIT_USER_EMAIL: ${{ inputs.git-user-email }} - working-directory: ${{ github.workspace }} - run: | - # Execute the script from github-tools - ./github-tools/.github/scripts/create-platform-release-pr.sh \ - "$PLATFORM" \ - "$PREVIOUS_VERSION_REF" \ - "$SEMVER_VERSION" \ - "$MOBILE_BUILD_VERSION" \ - "$GIT_USER_NAME" \ - "$GIT_USER_EMAIL" - - # Step 6: Upload commits.csv as artifact (if generated) - - name: Upload commits.csv artifact - if: ${{ hashFiles('commits.csv') != '' }} - uses: actions/upload-artifact@v4 - with: - name: commits-csv - path: commits.csv - if-no-files-found: error 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/get-release-timelines-test.yml b/.github/workflows/get-release-timelines-test.yml deleted file mode 100644 index 745dcc69..00000000 --- a/.github/workflows/get-release-timelines-test.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Get release timelines - -on: - workflow_dispatch: - inputs: - version: - required: true - type: string - description: The version of the release - -jobs: - get-release-timelines: - uses: ./.github/workflows/get-release-timelines.yml - with: - version: ${{ inputs.version }} - secrets: - RUNWAY_APP_ID: ${{ secrets.RUNWAY_APP_ID }} - RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} diff --git a/.github/workflows/get-release-timelines.yml b/.github/workflows/get-release-timelines.yml deleted file mode 100644 index 91f5fceb..00000000 --- a/.github/workflows/get-release-timelines.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Get release timelines - -on: - workflow_call: - inputs: - version: - required: true - type: string - description: The version of the release - secrets: - RUNWAY_APP_ID: - required: true - RUNWAY_API_KEY: - required: true - -jobs: - get-release-timelines: - runs-on: ubuntu-latest - steps: - - name: Checkout the 'github-tools' repository - uses: actions/checkout@v4 - with: - repository: metamask/github-tools - - - name: Get release timelines - env: - OWNER: ${{ github.repository_owner }} - REPOSITORY: ${{ github.event.repository.name }} - VERSION: ${{ inputs.version }} - RUNWAY_APP_ID: ${{ secrets.RUNWAY_APP_ID }} - RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: .github/scripts/get-release-timelines.sh - - - name: Upload artifact release-timelines-${{ inputs.version }}.csv - uses: actions/upload-artifact@v4 - with: - name: release-timelines-${{ inputs.version }}.csv - path: release-timelines-${{ inputs.version }}.csv diff --git a/.github/workflows/post-gh-rca.yml b/.github/workflows/post-gh-rca.yml deleted file mode 100644 index 09713f05..00000000 --- a/.github/workflows/post-gh-rca.yml +++ /dev/null @@ -1,151 +0,0 @@ -name: Post RCA Form - -permissions: - issues: write - contents: read - -on: - workflow_call: - inputs: - google-form-base-url: - description: Base URL of the Google Form. - default: 'https://docs.google.com/forms/d/e/1FAIpQLSeLOVVUy7mO1j-5Isb04OAWk3dM0b1NY1R8kf0tiEBs9elcEg/viewform?usp=pp_url' - type: string - repo-owner: - description: The repo owner - required: true - type: string - repo-name: - description: The repo name - required: true - type: string - issue-number: - description: The number of the closed issue - required: true - type: string - issue-labels: - description: JSON-stringified array of labels that should trigger the RCA prompt - required: true - type: string - entry-issue: - description: The entry ID for the issue field in the Google Form - default: 'entry.1417567074' - type: string - entry-regression: - description: The entry ID for the regression field in the Google Form - default: 'entry.1470697156' - type: string - entry-team: - description: The entry ID for the team field in the Google Form - default: 'entry.1198657478' - type: string - entry-repo-name: - description: The entry ID for the repository name field - default: 'entry.1085838323' - type: string - entry-issue-url: - description: The entry ID for the GitHub issue URL field - default: 'entry.516762472' - type: string - -jobs: - post-rca-form: - name: Post Google Form link and log results on issue close - runs-on: ubuntu-latest - steps: - - name: Post RCA Form Link - uses: actions/github-script@v7 - env: - GOOGLE_FORM_BASE_URL: ${{ inputs.google-form-base-url }} - ISSUE_LABELS: ${{ inputs.issue-labels }} - OWNER_NAME: ${{ inputs.repo-owner }} - REPO_NAME: ${{ inputs.repo-name }} - ISSUE_NUMBER: ${{ inputs.issue-number }} - ENTRY_ISSUE: ${{ inputs.entry-issue }} - ENTRY_REGRESSION: ${{ inputs.entry-regression }} - ENTRY_TEAM: ${{ inputs.entry-team }} - ENTRY_REPO_NAME: ${{ inputs.entry-repo-name }} - ENTRY_ISSUE_URL: ${{ inputs.entry-issue-url }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { - GOOGLE_FORM_BASE_URL: baseUrl, - ENTRY_ISSUE, - ENTRY_REGRESSION, - ENTRY_TEAM, - ENTRY_REPO_NAME, - ENTRY_ISSUE_URL, - OWNER_NAME: owner, - REPO_NAME: repo, - ISSUE_NUMBER: issueNumStr, - } = process.env; - - const issue_number = parseInt(issueNumStr, 10); - const allowedLabels = JSON.parse(process.env.ISSUE_LABELS); - - // Fetch issue details to get the assignees - const { data: issue } = await github.rest.issues.get({ - owner, - repo, - issue_number: issue_number, - }); - - const hasAllowedLabel = issue.labels.some(label => - allowedLabels.includes(label.name) - ); - - if (!hasAllowedLabel) { - console.log(`❌ Issue #${issue_number} skipped — no matching label.`); - return; - } - - // if it's a sev1-high or sev0-high, lets grab team and regression labels, if there's any - // if there's none, an empty value will be sent, which is what we want - const teamLabels = issue.labels - .map(l => l.name) - .filter(n => n.startsWith('team-')); - - const regressionLabels = issue.labels - .map(l => l.name) - .filter(n => n.startsWith('regression-')); - - const formUrl = new URL(baseUrl); - formUrl.searchParams.set(ENTRY_ISSUE, issue_number); - formUrl.searchParams.set( - ENTRY_REGRESSION, - regressionLabels.length ? regressionLabels.join(',') : '' - ); - formUrl.searchParams.set( - ENTRY_TEAM, - teamLabels.length ? teamLabels.join(',') : '' - ); - - formUrl.searchParams.set(ENTRY_REPO_NAME, repo); - formUrl.searchParams.set(ENTRY_ISSUE_URL, `https://github.com/${owner}/${repo}/issues/${issue_number}`); - - const assignees = issue.assignees.map(u=>`@${u.login}`).join(', '); - const body = `Hi ${assignees}, - - This issue has been closed. Please complete this RCA form: - ${formUrl.toString()} - - `; - - await github.rest.issues.createComment({ - owner, repo, issue_number, body - }); - console.log(`✅ Comment posted on issue #${issue_number}`); - - // Add the RCA-needed label - try { - await github.rest.issues.addLabels({ - owner, - repo, - issue_number: issue_number, - labels: ['RCA-needed'] - }); - console.log(`✅ Added 'RCA-needed' label on issue #${issue_number}`); - } catch (error) { - console.log(`⚠️ Could not add label: ${error.message}`); - } diff --git a/.github/workflows/post-merge-validation.yml b/.github/workflows/post-merge-validation.yml deleted file mode 100644 index 750988c4..00000000 --- a/.github/workflows/post-merge-validation.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Post Merge Validation - -on: - workflow_call: - inputs: - repo: - description: 'The repo owner/name to process (e.g. MetaMask/metamask-extension)' - required: true - type: string - start_hour_utc: - description: 'The hour of the day (UTC) to start processing the PRs merged in main' - required: true - type: number - spreadsheet_id: - description: 'Google Spreadsheet ID to update' - required: false - type: string - default: '1tsoodlAlyvEUpkkcNcbZ4PM9HuC9cEM80RZeoVv5OCQ' - lookback_days: - description: 'Number of days to look back for PRs' - required: false - type: number - default: 1 - secrets: - github-token: - description: 'GitHub token with repo access' - required: true - google-application-creds-base64: - description: 'Base64 encoded Google service account credentials' - required: true - -jobs: - post-merge-validation-tracker: - 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 post-merge-validation script - working-directory: ./github-tools - env: - SHEET_ID: ${{ inputs.spreadsheet_id }} - START_HOUR_UTC: ${{ inputs.start_hour_utc }} - LOOKBACK_DAYS: ${{ inputs.lookback_days }} - REPO: ${{ inputs.repo }} - GITHUB_TOKEN: ${{ secrets.github-token }} - GOOGLE_APPLICATION_CREDENTIALS_BASE64: ${{ secrets.google-application-creds-base64 }} - run: node .github/scripts/post-merge-validation-tracker.mjs 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}.`); - } diff --git a/.github/workflows/publish-slack-release-testing-status.yml b/.github/workflows/publish-slack-release-testing-status.yml deleted file mode 100644 index c1d29385..00000000 --- a/.github/workflows/publish-slack-release-testing-status.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Publish Slack Release Testing Status - -on: - workflow_call: - inputs: - platform: # possible values are [ mobile, extension ] - required: true - type: string - google-document-id: - required: true - type: string - # Controls whether to actually publish to production slack channels, true will publish to prod slack channels - test-only: - required: false - type: string - default: 'false' - secrets: - slack-api-key: - required: true - github-token: - required: true - google-application-creds-base64: - required: true - -jobs: - publish-status: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - # Step 2: Checkout github-tools repository - - name: Checkout github-tools repository - uses: actions/checkout@v4 - with: - repository: MetaMask/github-tools - ref: main - 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 - shell: bash - working-directory: ./github-tools - - - name: Install dependencies - run: yarn --immutable - shell: bash - working-directory: ./github-tools - - # Step 4: Run Script - - name: Publish Slack Release Testing Status - id: publish-slack-release-testing-status - shell: bash - env: - GITHUB_TOKEN: ${{ secrets.github-token }} - SLACK_API_KEY: ${{ secrets.slack-api-key }} - GOOG_DOCUMENT_ID: ${{ inputs.google-document-id }} - GOOGLE_APPLICATION_CREDENTIALS_BASE64: ${{ secrets.google-application-creds-base64 }} - TEST_ONLY: ${{inputs.test-only}} - PLATFORM: ${{inputs.platform}} - working-directory: ./github-tools - run: | - yarn run slack:release-testing diff --git a/.github/workflows/remove-rca-needed-label-sheets.yml b/.github/workflows/remove-rca-needed-label-sheets.yml deleted file mode 100644 index 5a6432f1..00000000 --- a/.github/workflows/remove-rca-needed-label-sheets.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Remove RCA-needed Label - -on: - workflow_call: - inputs: - dry_run: - description: 'Run in dry-run mode (no changes made)' - required: false - default: 'false' - type: string - spreadsheet_id: - description: 'Google Spreadsheet ID (must be provided by consuming repository)' - required: true - type: string - sheet_name: - description: 'Sheet tab name (uses default if not provided)' - required: false - default: 'Form Responses 1' - type: string - github-tools-version: - description: 'The version of github-tools to use. Defaults to main.' - required: false - default: 'main' - type: string - secrets: - github-token: - description: 'GitHub token with issues write permissions' - required: true - google-application-creds-base64: - description: 'Base64 encoded Google application service account credentials' - required: true - -permissions: - issues: write - contents: read - -jobs: - remove-rca-labels: - name: Remove RCA-needed Labels Based on Sheet Data - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout consuming repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.github-token }} - - - name: Checkout github-tools - uses: actions/checkout@v4 - with: - repository: MetaMask/github-tools - ref: ${{ inputs.github-tools-version }} - token: ${{ secrets.github-token }} - path: github-tools - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Run RCA Google Sheets check - run: | - # Move to github-tools directory where our script lives - cd github-tools/.github/scripts - - # Create a simple package.json for npm to work with - echo '{}' > package.json - - # Install exact versions of required packages locally - npm install --no-save --no-package-lock \ - @actions/core@1.10.1 \ - @actions/github@6.0.0 \ - googleapis@144.0.0 \ - tsx@4.7.1 - - # Run the script with tsx - npx tsx remove-rca-needed-label-sheets.ts - env: - GITHUB_TOKEN: ${{ secrets.github-token }} - GOOGLE_SHEETS_CREDENTIALS: ${{ secrets.google-application-creds-base64 }} - DRY_RUN: ${{ inputs.dry_run }} - SPREADSHEET_ID: ${{ inputs.spreadsheet_id }} - SHEET_NAME: ${{ inputs.sheet_name }} diff --git a/.github/workflows/stable-sync.yml b/.github/workflows/stable-sync.yml index 7ecc4131..adbbb13e 100644 --- a/.github/workflows/stable-sync.yml +++ b/.github/workflows/stable-sync.yml @@ -20,148 +20,16 @@ on: type: string description: 'The name of the stable branch to sync to (e.g., stable, main)' default: 'stable' - github-tools-version: - required: false - type: string - description: 'The version of github-tools to use. Defaults to main.' - default: 'main' - workflow_call: - inputs: - semver-version: - required: true - type: string - description: 'The semantic version to use for the sync (e.g., x.x.x)' - repo-type: - required: false - type: string - description: 'Type of repository (mobile or extension)' - default: 'mobile' - stable-branch-name: - required: false - type: string - description: 'The name of the stable branch to sync to (e.g., stable, main)' - default: 'stable' - github-tools-version: - required: false - type: string - description: 'The version of github-tools to use. Defaults to main.' - default: ${{ github.action_ref }} - secrets: - github-token: - description: 'GitHub token for creating pull requests' - required: true jobs: stable-sync: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v5 + - name: Stable sync + uses: ./.github/actions/stable-sync with: - fetch-depth: 0 - - - name: Checkout github-tools repository - uses: actions/checkout@v4 - with: - repository: MetaMask/github-tools - ref: ${{ inputs.github-tools-version }} - path: github-tools - - - name: Setup Node.js Mobile - if: ${{ inputs.repo-type == 'mobile' }} - uses: actions/setup-node@v4 - with: - node-version: '18' - - - name: Setup Node.js Extension - if: ${{ inputs.repo-type == 'extension' }} - uses: actions/setup-node@v4 - with: - node-version: '22.15' - - - name: Prepare Yarn - if: ${{ inputs.repo-type == 'extension' }} - run: corepack prepare yarn@4.5.1 --activate - - - name: Prepare Yarn - Enable corepack - if: ${{ inputs.repo-type == 'extension' }} - run: corepack enable - - - name: Check if PR exists - id: check-pr - uses: actions/github-script@v7 - with: - script: | - const { data: prs } = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - head: `${context.repo.owner}:stable-main-${process.env.SEMVER_VERSION}`, - base: 'main' - }); - return prs.length > 0; - env: - SEMVER_VERSION: ${{ inputs.semver-version }} - - - name: Set Git user and email - run: | - git config --global user.name "metamaskbot" - git config --global user.email "metamaskbot@users.noreply.github.com" - - - name: Run stable sync - id: run-stable-sync - # if: steps.check-pr.outputs.result != 'true' - env: - CREATE_BRANCH: 'false' # let the script handle the branch creation - REPO: ${{ inputs.repo-type }} # Default to 'mobile' if not specified - BASE_BRANCH: ${{ inputs.stable-branch-name }} - SEMVER_VERSION: ${{ inputs.semver-version }} - run: | - # Ensure github-tools is in .gitignore to prevent it from being committed - if ! grep -q "^github-tools/" .gitignore 2>/dev/null; then - echo "github-tools/" >> .gitignore - echo "Added github-tools/ to .gitignore" - fi - - # Execute the script from github-tools - node ./github-tools/.github/scripts/stable-sync.js "stable-main-$SEMVER_VERSION" - BRANCH_NAME="stable-main-$SEMVER_VERSION" - if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then - echo "Branch $BRANCH_NAME exists remotely, force pushing to overwrite" - git push origin "$BRANCH_NAME" --force - else - echo "Branch $BRANCH_NAME doesn't exist remotely, pushing with --set-upstream" - git push --set-upstream origin "$BRANCH_NAME" - fi - - - name: Create Pull Request - if: steps.check-pr.outputs.result != 'true' - env: - GITHUB_TOKEN: ${{ secrets.github-token }} - BRANCH_NAME: stable-main-${{ inputs.semver-version }} - VERSION: ${{ inputs.semver-version }} - run: | - # Create PR using GitHub CLI - gh pr create \ - --title "release: sync stable to main for version $VERSION" \ - --body "This PR syncs the stable branch to main for version $VERSION. - - *Synchronization Process:* - - - Fetches the latest changes from the remote repository - - Resets the branch to match the stable branch - - Attempts to merge changes from main into the branch - - Handles merge conflicts if they occur - - *File Preservation:* - - Preserves specific files from the stable branch: - - CHANGELOG.md - - bitrise.yml - - android/app/build.gradle - - ios/MetaMask.xcodeproj/project.pbxproj - - package.json - - Indicates the next version candidate of main to $VERSION" \ - --base main \ - --head "$BRANCH_NAME" - #--label "sync" \ - #--label "stable" + semver-version: ${{ github.event.inputs.semver-version }} + repo-type: ${{ github.event.inputs.repo-type }} + stable-branch-name: ${{ github.event.inputs.stable-branch-name }} diff --git a/.github/workflows/stale-issue-pr.yml b/.github/workflows/stale-issue-pr.yml deleted file mode 100644 index 6784a496..00000000 --- a/.github/workflows/stale-issue-pr.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: 'Close stale issues and PRs' - -on: - workflow_call: - inputs: - stale_issue_message: - description: 'Message to post when marking an issue as stale' - required: false - default: 'This issue has been automatically marked as stale because it has not had recent activity in the last 30 days. It will be closed in 60 days. Thank you for your contributions.' - type: string - close_issue_message: - description: 'Message to post when closing a stale issue' - required: false - default: 'This issue was closed because there has been no follow activity in 90 days. If you feel this was closed in error please provide evidence on the current production app in a new issue or comment in the existing issue to a maintainer. Thank you for your contributions.' - type: string - stale_issue_label: - description: 'Label to use when marking an issue as stale' - required: false - default: 'stale' - type: string - any_of_issue_labels: - description: 'Comma-separated list of labels to check for issues' - required: false - default: 'needs-information, needs-reproduction' - type: string - exempt_issue_labels: - description: 'Comma-separated list of labels that exempt issues from being marked as stale' - required: false - default: 'type-security, feature-request, Sev1-high, needs-triage' - type: string - days_before_issue_stale: - description: 'Number of days of inactivity before an issue becomes stale' - required: false - default: 30 - type: number - days_before_issue_close: - description: 'Number of days of inactivity before a stale issue is closed' - required: false - default: 60 - type: number - stale_pr_message: - description: 'Message to post when marking a PR as stale' - required: false - default: 'This PR has been automatically marked as stale because it has not had recent activity in the last 90 days. It will be closed in 7 days. Thank you for your contributions.' - type: string - stale_pr_label: - description: 'Label to use when marking a PR as stale' - required: false - default: 'stale' - type: string - exempt_pr_labels: - description: 'Comma-separated list of labels that exempt PRs from being marked as stale' - required: false - default: 'work-in-progress, external-contributor' - type: string - close_pr_message: - description: 'Message to post when closing a stale PR' - required: false - default: 'This PR was closed because there has been no follow up activity in 7 days. Thank you for your contributions.' - type: string - days_before_pr_stale: - description: 'Number of days of inactivity before a PR becomes stale' - required: false - default: 90 - type: number - days_before_pr_close: - description: 'Number of days of inactivity before a stale PR is closed' - required: false - default: 7 - type: number - operations_per_run: - description: 'Maximum number of operations to perform per run' - required: false - default: 200 - type: number - -jobs: - stale: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/stale@72afbce2b0dbd1d903bb142cebe2d15dc307ae57 - with: - stale-issue-message: ${{ inputs.stale_issue_message }} - close-issue-message: ${{ inputs.close_issue_message }} - stale-issue-label: ${{ inputs.stale_issue_label }} - any-of-issue-labels: ${{ inputs.any_of_issue_labels }} - exempt-issue-labels: ${{ inputs.exempt_issue_labels }} - days-before-issue-stale: ${{ inputs.days_before_issue_stale }} - days-before-issue-close: ${{ inputs.days_before_issue_close }} - stale-pr-message: ${{ inputs.stale_pr_message }} - stale-pr-label: ${{ inputs.stale_pr_label }} - exempt-pr-labels: ${{ inputs.exempt_pr_labels }} - close-pr-message: ${{ inputs.close_pr_message }} - days-before-pr-stale: ${{ inputs.days_before_pr_stale }} - days-before-pr-close: ${{ inputs.days_before_pr_close }} - operations-per-run: ${{ inputs.operations_per_run }} diff --git a/.github/workflows/test-add-team-label.yml b/.github/workflows/test-add-team-label.yml new file mode 100644 index 00000000..960c6d58 --- /dev/null +++ b/.github/workflows/test-add-team-label.yml @@ -0,0 +1,18 @@ +name: Test "Add Team Label" Action + +on: + pull_request: + types: + - opened + +jobs: + test-add-team-label: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + - name: Add team label + uses: ./.github/actions/add-team-label + with: + team-label-token: ${{ secrets.TEAM_LABEL_TOKEN }} diff --git a/.github/workflows/test-get-release-timelines.yml b/.github/workflows/test-get-release-timelines.yml new file mode 100644 index 00000000..3135f90c --- /dev/null +++ b/.github/workflows/test-get-release-timelines.yml @@ -0,0 +1,24 @@ +name: Test "Get Release Timelines" Action + +on: + workflow_dispatch: + inputs: + version: + required: true + type: string + description: The version of the release + +jobs: + test-get-release-timelines: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + - name: Add team label + uses: ./.github/actions/get-release-timelines + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + runway-app-id: ${{ secrets.RUNWAY_APP_ID }} + runway-api-key: ${{ secrets.RUNWAY_API_KEY }} + version: ${{ github.event.inputs.version }} diff --git a/.github/workflows/update-release-changelog.yml b/.github/workflows/update-release-changelog.yml deleted file mode 100644 index 535b91bb..00000000 --- a/.github/workflows/update-release-changelog.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Update Release Changelog - -on: - workflow_call: - inputs: - release-branch: - required: true - type: string - description: 'Release branch name (e.g., Version-v13.3.0 or release/6.20.0).' - platform: - required: false - type: string - default: 'extension' - description: 'Target platform (extension | mobile). Defaults to extension.' - repository-url: - required: true - type: string - description: 'Full HTTPS URL for the invoking repository.' - previous-version-ref: - required: false - type: string - default: 'null' - description: 'Previous release version reference (branch/tag/SHA). Use "null" for hotfixes.' - github-tools-version: - required: false - type: string - default: 'main' - description: 'Version of github-tools to use (branch, tag, or SHA).' - secrets: - github-token: - required: true - description: 'GitHub token with write access to the invoking repository and read access to planning resources.' - -jobs: - update-changelog: - name: Update release changelog - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - # Step 1: Checkout invoking repository (metamask-mobile | metamask-extension) - - name: Checkout invoking repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ inputs.release-branch }} - token: ${{ secrets.github-token }} - - # Step 2: Checkout github-tools repository - - name: Checkout github-tools repository - uses: actions/checkout@v4 - with: - repository: MetaMask/github-tools - ref: ${{ inputs.github-tools-version }} - path: github-tools - - # Step 3: Setup environment from github-tools - - name: Checkout and setup environment - uses: ./github-tools/.github/actions/checkout-and-setup - with: - is-high-risk-environment: true - - # Step 4: Update changelog using shared helper script - - name: Update changelog branch - shell: bash - env: - GITHUB_TOKEN: ${{ secrets.github-token }} - GIT_AUTHOR_NAME: metamaskbot - GIT_AUTHOR_EMAIL: metamaskbot@users.noreply.github.com - RELEASE_BRANCH: ${{ inputs.release-branch }} - PLATFORM: ${{ inputs.platform }} - REPOSITORY_URL: ${{ inputs.repository-url }} - PREVIOUS_VERSION_REF: ${{ inputs.previous-version-ref }} - run: | - set -euo pipefail - - corepack enable - yarn install --immutable - - ./github-tools/.github/scripts/update-release-changelog.sh \ - "$RELEASE_BRANCH" \ - "$PLATFORM" \ - "$REPOSITORY_URL" \ - "$PREVIOUS_VERSION_REF"