From 951e2e4e8f66e797f74c480dd767d9e94bbebe79 Mon Sep 17 00:00:00 2001 From: Okan Sahin Date: Wed, 17 Apr 2024 16:23:30 +0200 Subject: [PATCH 1/7] [MWPW-147001] Stage initiative automation --- .github/workflows/helpers.js | 86 ++++++++ .github/workflows/localWorkflowConfigs.js | 25 --- .github/workflows/merge-to-stage.js | 233 ++++++++++++++++++++++ .github/workflows/merge-to-stage.yaml | 48 +++++ .github/workflows/pr-reminders.js | 16 +- .github/workflows/update-script.js | 41 +++- 6 files changed, 409 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/helpers.js delete mode 100644 .github/workflows/localWorkflowConfigs.js create mode 100644 .github/workflows/merge-to-stage.js create mode 100644 .github/workflows/merge-to-stage.yaml diff --git a/.github/workflows/helpers.js b/.github/workflows/helpers.js new file mode 100644 index 0000000000..fe39d45dc6 --- /dev/null +++ b/.github/workflows/helpers.js @@ -0,0 +1,86 @@ +// Those env variables are set by an github action automatically +// For local testing, you should test on your fork. +const owner = process.env.REPO_OWNER || ''; // example owner: adobecom +const repo = process.env.REPO_NAME || ''; // example repo name: milo +const auth = process.env.GH_TOKEN || ''; // https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens + +const getLocalConfigs = () => { + if (!owner || !repo || !auth) { + throw new Error(`Create a .env file on the root of the project with credentials. +Then run: node --env-file=.env .github/workflows/update-ims.js`); + } + + const { Octokit } = require('@octokit/rest'); + return { + github: { rest: new Octokit({ auth: process.env.GH_TOKEN }) }, + context: { + repo: { + owner, + repo, + }, + }, + }; +}; + +const slackNotification = (text) => { + console.log(text); + return fetch(process.env.MILO_RELEASE_SLACK_WH, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ text }), + }); +}; + +const addLabels = ({ pr, github, owner, repo }) => + github.rest.issues + .listLabelsOnIssue({ owner, repo, issue_number: pr.number }) + .then(({ data }) => { + pr.labels = data.map(({ name }) => name); + return pr; + }); + +const addFiles = ({ pr, github, owner, repo }) => + github.rest.pulls + .listFiles({ owner, repo, pull_number: pr.number }) + .then(({ data }) => { + pr.files = data.map(({ filename }) => filename); + return pr; + }); + +const getChecks = ({ pr, github, owner, repo }) => + github.rest.checks + .listForRef({ owner, repo, ref: pr.head.sha }) + .then(({ data }) => { + const checksByName = data.check_runs.reduce((map, check) => { + if ( + !map.has(check.name) || + new Date(map.get(check.name).completed_at) < + new Date(check.completed_at) + ) { + map.set(check.name, check); + } + return map; + }, new Map()); + pr.checks = Array.from(checksByName.values()); + return pr; + }); + +const getReviews = ({ pr, github, owner, repo }) => + github.rest.pulls + .listReviews({ + owner, + repo, + pull_number: pr.number, + }) + .then(({ data }) => { + pr.reviews = data; + return pr; + }); + +module.exports = { + getLocalConfigs, + slackNotification, + pulls: { addLabels, addFiles, getChecks, getReviews }, +}; diff --git a/.github/workflows/localWorkflowConfigs.js b/.github/workflows/localWorkflowConfigs.js deleted file mode 100644 index 399e0eb9ca..0000000000 --- a/.github/workflows/localWorkflowConfigs.js +++ /dev/null @@ -1,25 +0,0 @@ -// Those env variables are set by an github action automatically -// For local testing, you should test on your fork. -const owner = process.env.REPO_OWNER || ''; // example owner: adobecom -const repo = process.env.REPO_NAME || ''; // example repo name: milo -const auth = process.env.GH_TOKEN || ''; // https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens - -const getLocalConfigs = () => { - if (!owner || !repo || !auth) { - throw new Error(`Create a .env file on the root of the project with credentials. -Then run: node --env-file=.env .github/workflows/update-ims.js`); - } - - const { Octokit } = require('@octokit/rest'); - return { - github: { rest: new Octokit({ auth: process.env.GH_TOKEN }) }, - context: { - repo: { - owner, - repo, - }, - }, - }; -}; - -module.exports = getLocalConfigs; diff --git a/.github/workflows/merge-to-stage.js b/.github/workflows/merge-to-stage.js new file mode 100644 index 0000000000..1ddfad6562 --- /dev/null +++ b/.github/workflows/merge-to-stage.js @@ -0,0 +1,233 @@ +const { + slackNotification, + getLocalConfigs, + pulls: { addLabels, addFiles, getChecks, getReviews }, +} = require('./helpers.js'); + +// Run from the root of the project for local testing: node --env-file=.env .github/workflows/merge-to-stage.js +const prTitle = '[Release] Stage to Main'; +const seen = {}; +const requiredApprovals = process.env.REQUIRED_APPROVALS || 2; +const stage = 'stage'; +const prod = 'main'; +const labels = { + highPriority: 'high priority', + readyForStage: 'Ready for Stage', + SOTPrefix: 'SOT', +}; + +const slack = { + fileOverlap: ({ html_url, number, title }) => + `:fast_forward: Skipping <${html_url}|${number}: ${title}> due to overlap in files.`, + merge: ({ html_url, number, title }) => + `:merged: Stage merge PR <${html_url}|${number}: ${title}>.`, + failingChecks: ({ html_url, number, title }) => + `:x: Skipping <${html_url}|${number}: ${title}> due to failing checks`, + requireApprovals: ({ html_url, number, title }) => + `:x: Skipping <${html_url}|${number}: ${title}> due to insufficient approvals`, + openedSyncPr: ({ html_url, number }) => + `:fast_forward: Created <${html_url}|Stage to Main PR ${number}>`, +}; + +let github, owner, repo, currPrNumber, core; + +let body = ` +## common base root URLs +**Homepage :** https://www.stage.adobe.com/ +**BACOM:** https://business.stage.adobe.com/fr/ +**CC:** https://www.stage.adobe.com/creativecloud.html +**Blog:** https://blog.stage.adobe.com/ +**Acrobat:** https://www.stage.adobe.com/acrobat/online/sign-pdf.html + +**Milo:** +- Before: https://main--milo--adobecom.hlx.live/?martech=off +- After: https://stage--milo--adobecom.hlx.live/?martech=off +`; + +const RCPDates = [ + { + start: new Date('2024-05-26T00:00:00-07:00'), + end: new Date('2024-06-01T00:00:00-07:00'), + }, + { + start: new Date('2024-06-13T11:00:00-07:00'), + end: new Date('2024-06-13T14:00:00-07:00'), + }, + { + start: new Date('2024-06-30T00:00:00-07:00'), + end: new Date('2024-07-06T00:00:00-07:00'), + }, + { + start: new Date('2024-08-25T00:00:00-07:00'), + end: new Date('2024-08-31T00:00:00-07:00'), + }, + { + start: new Date('2024-09-12T11:00:00-07:00'), + end: new Date('2024-09-12T14:00:00-07:00'), + }, + { + start: new Date('2024-10-14T00:00:00-07:00'), + end: new Date('2024-11-18T17:00:00-08:00'), + }, + { + start: new Date('2024-11-17T00:00:00-08:00'), + end: new Date('2024-11-30T00:00:00-08:00'), + }, + { + start: new Date('2024-12-12T11:00:00-08:00'), + end: new Date('2024-12-12T14:00:00-08:00'), + }, + { + start: new Date('2024-12-15T00:00:00-08:00'), + end: new Date('2025-01-02T00:00:00-08:00'), + }, +]; + +const isHighPrio = (label) => label.includes(labels.highPriority); + +const hasFailingChecks = (checks) => + checks.some( + ({ conclusion, name }) => + name !== 'merge-to-stage' && conclusion === 'failure' + ); + +const getPRs = async () => { + let prs = await github.rest.pulls + .list({ owner, repo, state: 'open', per_page: 100, base: stage }) + .then(({ data }) => data); + await Promise.all(prs.map((pr) => addLabels({ pr, github, owner, repo }))); + prs = prs.filter((pr) => pr.labels.includes(labels.readyForStage)); + await Promise.all([ + ...prs.map((pr) => addFiles({ pr, github, owner, repo })), + ...prs.map((pr) => getChecks({ pr, github, owner, repo })), + ...prs.map((pr) => getReviews({ pr, github, owner, repo })), + ]); + + prs = prs.filter(({ checks, reviews, html_url, number, title }) => { + if (hasFailingChecks(checks)) { + slackNotification(slack.failingChecks({ html_url, number, title })); + if (number === currPrNumber) + core.setFailed(`Failing checks on the current PR ${number}`); + return false; + } + + const approvals = reviews.filter(({ state }) => state === 'APPROVED'); + if (approvals.length < requiredApprovals) { + slackNotification(slack.requireApprovals({ html_url, number, title })); + if (number === currPrNumber) + core.setFailed(`Insufficient approvals on the current PR ${number}`); + return false; + } + + return true; + }); + + return prs.reverse(); // OLD PRs first +}; + +const merge = async ({ prs }) => { + console.log(`Merging ${prs.length || 0} PRs that are ready... `); + for await (const { number, files, html_url, title } of prs) { + if (files.some((file) => seen[file])) { + await slackNotification(slack.fileOverlap({ html_url, number, title })); + continue; + } + files.forEach((file) => (seen[file] = true)); + if (!process.env.LOCAL_RUN) + await github.rest.pulls.merge({ owner, repo, pull_number: number }); + body = `- [${title}](${html_url})\n${body}`; + await slackNotification(slack.merge({ html_url, number, title })); + } +}; + +const getStageToMainPR = () => + github.rest.pulls + .list({ owner, repo, state: 'open', base: prod }) + .then(({ data } = {}) => data.find(({ title } = {}) => title === prTitle)) + .then((pr) => pr && addLabels({ pr, github, owner, repo })) + .then((pr) => pr && addFiles({ pr, github, owner, repo })) + .then((pr) => { + pr?.files.forEach((file) => (seen[file] = true)); + return pr; + }); + +const openStageToMainPR = async () => { + const { data: comparisonData } = await github.rest.repos.compareCommits({ + owner, + repo, + base: prod, + head: stage, + }); + + for (const commit of comparisonData.commits) { + const { data: pullRequestData } = + await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner, + repo, + commit_sha: commit.sha, + }); + + for (const pr of pullRequestData) { + if (!body.includes(pr.html_url)) + body = `- [${pr.title}](${pr.html_url})\n${body}`; + } + } + + try { + const { + data: { html_url, number }, + } = await github.rest.pulls.create({ + owner, + repo, + title: prTitle, + head: stage, + base: prod, + body, + }); + await slackNotification(slack.openedSyncPr({ html_url, number })); + } catch (error) { + if (error.message.includes('No commits between main and stage')) + return console.log('No new commits, no stage->main PR opened'); + throw error; + } +}; + +const main = async (params) => { + github = params.github; + owner = params.context.repo.owner; + repo = params.context.repo.repo; + currPrNumber = params.context.issue?.number; + core = params.core; + + const now = new Date(); + for (const { start, end } of RCPDates) { + if (start <= now && now <= end) { + console.log('Current date is within a RCP. Stopping execution.'); + return; + } + } + try { + const stageToMainPR = await getStageToMainPR(); + console.log('has Stage to Main PR:', !!stageToMainPR); + if (stageToMainPR?.labels.some((label) => label.includes(labels.SOTPrefix))) + return console.log('PR exists & testing started. Stopping execution.'); + const prs = await getPRs(); + await merge({ prs: prs.filter(({ labels }) => isHighPrio(labels)) }); + await merge({ prs: prs.filter(({ labels }) => !isHighPrio(labels)) }); + if (!stageToMainPR) await openStageToMainPR(); + console.log('Process successfully executed.'); + } catch (error) { + console.error(error); + } +}; + +if (process.env.LOCAL_RUN) { + const { github, context } = getLocalConfigs(); + main({ + github, + context, + core: { setFailed: console.error }, + }); +} + +module.exports = main; diff --git a/.github/workflows/merge-to-stage.yaml b/.github/workflows/merge-to-stage.yaml new file mode 100644 index 0000000000..3310906661 --- /dev/null +++ b/.github/workflows/merge-to-stage.yaml @@ -0,0 +1,48 @@ +name: Merge to stage + +on: + schedule: + - cron: '0 8 * * *' # Run once a day at 8:00 UTC. For any cases where adding the label wouldnt work + workflow_dispatch: # Allow manual trigger + pull_request: + types: [labeled] # Run when a label is added to a PR + +env: + MILO_RELEASE_SLACK_WH: ${{ secrets.MILO_RELEASE_SLACK_WH }} + REQUIRED_APPROVALS: ${{ secrets.REQUIRED_APPROVALS }} + +jobs: + merge-to-stage: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4.1.1 + + - name: Check label + id: check_label + uses: actions/github-script@v7.0.1 + with: + script: | + const fs = require('fs'); + const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8')); + if (process.env.GITHUB_EVENT_NAME === 'pull_request') { + const { label: { name } } = event; + if (name !== 'Ready for Stage') { + console.log('Label is not "Ready for Stage", stopping workflow...'); + return false; + } + } + return true; + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Merge to stage or queue to merge + if: steps.check_label.outputs.result == 'true' + uses: actions/github-script@v7.0.1 + with: + script: | + const main = require('./.github/workflows/merge-to-stage.js') + main({ github, context, core }) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-reminders.js b/.github/workflows/pr-reminders.js index a387846f34..975ec743ae 100644 --- a/.github/workflows/pr-reminders.js +++ b/.github/workflows/pr-reminders.js @@ -1,4 +1,5 @@ // Run from the root of the project for local testing: node --env-file=.env .github/workflows/pr-reminders.js +const { getLocalConfigs } = require('./helpers.js'); const main = async ({ github, context }) => { const comment = async ({ pr, message, comments }) => { @@ -61,7 +62,11 @@ const main = async ({ github, context }) => { issue_number: pr.number, }); - if (labels.some(({ name } = {}) => name === 'Ready for Stage' || name === 'Stale')) { + if ( + labels.some( + ({ name } = {}) => name === 'Ready for Stage' || name === 'Stale' + ) + ) { console.log( `PR #${pr.number} has the 'Ready for Stage' or 'Stale' label. Skipping...` ); @@ -101,12 +106,13 @@ const main = async ({ github, context }) => { continue; } - if(labels.some(({ name } = {}) => name === 'needs-verification')) { + if (labels.some(({ name } = {}) => name === 'needs-verification')) { comment({ pr, comments, - message: 'This PR is currently in the `needs-verification` state. Please assign a QA engineer to verify the changes.' - }) + message: + 'This PR is currently in the `needs-verification` state. Please assign a QA engineer to verify the changes.', + }); continue; } @@ -123,7 +129,7 @@ const main = async ({ github, context }) => { }; if (process.env.LOCAL_RUN) { - const { github, context } = require('./localWorkflowConfigs.js')(); + const { github, context } = getLocalConfigs(); main({ github, context, diff --git a/.github/workflows/update-script.js b/.github/workflows/update-script.js index 28e28088e0..6db34cfa06 100644 --- a/.github/workflows/update-script.js +++ b/.github/workflows/update-script.js @@ -1,18 +1,22 @@ const https = require('https'); const { execSync } = require('child_process'); const fs = require('fs'); +const { getLocalConfigs } = require('./helpers.js'); // Run from the root of the project for local testing: node --env-file=.env .github/workflows/update-script.js const localExecution = process.env.LOCAL_RUN || false; const localRunConfigs = { branch: process.env.LOCAL_RUN_BRANCH || 'update-imslib', - title: process.env.LOCAL_RUN_TITLTE || '[AUTOMATED-PR] Update imslib.min.js dependency', - path: process.env.LOCAL_RUN_SCRIPT || 'https://auth.services.adobe.com/imslib/imslib.min.js', + title: + process.env.LOCAL_RUN_TITLTE || + '[AUTOMATED-PR] Update imslib.min.js dependency', + path: + process.env.LOCAL_RUN_SCRIPT || + 'https://auth.services.adobe.com/imslib/imslib.min.js', scriptPath: process.env.LOCAL_RUN_SCRIPT_PATH || './libs/deps/imslib.min.js', origin: process.env.LOCAL_RUN_ORIGIN || 'origin', }; - const getPrDescription = ({ branch, scriptPath }) => `## Description Update ${scriptPath} to the latest version @@ -126,12 +130,23 @@ const main = async ({ const selfHostedScript = fs.existsSync(scriptPath) && fs.readFileSync(scriptPath, 'utf8'); - console.log(`/libs/deps script build date: ${selfHostedScript.match(/^\/\/ Built (.*?) -/)[1]}`); - console.log(`/libs/deps script last modified date: ${selfHostedScript.match(/- Last Modified (.*?)\n/)[1]}`); + console.log( + `/libs/deps script build date: ${ + selfHostedScript.match(/^\/\/ Built (.*?) -/)[1] + }` + ); + console.log( + `/libs/deps script last modified date: ${ + selfHostedScript.match(/- Last Modified (.*?)\n/)[1] + }` + ); console.log(`External script last modified date: ${lastModified}`); - const scriptIsEqual = script === selfHostedScript.replace(/^\/\/ Built .*\n/, ''); - console.log(`Validating if "${scriptPath}" has changed. Script is the same: ${scriptIsEqual}`); + const scriptIsEqual = + script === selfHostedScript.replace(/^\/\/ Built .*\n/, ''); + console.log( + `Validating if "${scriptPath}" has changed. Script is the same: ${scriptIsEqual}` + ); if (!scriptIsEqual || localExecution) { const { data: openPRs } = await github.rest.pulls.list({ @@ -141,7 +156,10 @@ const main = async ({ }); const hasPR = openPRs.find((pr) => pr.head.ref === branch); - if (hasPR) return console.log(`PR already exists for branch ${branch}. Execution stopped.`); + if (hasPR) + return console.log( + `PR already exists for branch ${branch}. Execution stopped.` + ); createAndPushBranch({ script, branch, scriptPath, origin, lastModified }); @@ -175,12 +193,15 @@ const main = async ({ }); } } catch (error) { - console.error(`An error occurred while running workflow for ${title}`, error); + console.error( + `An error occurred while running workflow for ${title}`, + error + ); } }; if (localExecution) { - const { github, context } = require('./localWorkflowConfigs.js')(); + const { github, context } = getLocalConfigs(); main({ github, context, From bb474aeecfea777a0d0b07493cc1de7597cf1da4 Mon Sep 17 00:00:00 2001 From: Okan Sahin Date: Tue, 7 May 2024 15:13:42 +0200 Subject: [PATCH 2/7] Add high prio PRs & fail on RCP cutoff --- .github/workflows/helpers.js | 2 +- .github/workflows/merge-to-stage.js | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/helpers.js b/.github/workflows/helpers.js index fe39d45dc6..8cfb11fd14 100644 --- a/.github/workflows/helpers.js +++ b/.github/workflows/helpers.js @@ -12,7 +12,7 @@ Then run: node --env-file=.env .github/workflows/update-ims.js`); const { Octokit } = require('@octokit/rest'); return { - github: { rest: new Octokit({ auth: process.env.GH_TOKEN }) }, + github: { rest: new Octokit({ auth }) }, context: { repo: { owner, diff --git a/.github/workflows/merge-to-stage.js b/.github/workflows/merge-to-stage.js index 1ddfad6562..531d65ab15 100644 --- a/.github/workflows/merge-to-stage.js +++ b/.github/workflows/merge-to-stage.js @@ -14,13 +14,14 @@ const labels = { highPriority: 'high priority', readyForStage: 'Ready for Stage', SOTPrefix: 'SOT', + highImpact: 'high-impact', }; const slack = { fileOverlap: ({ html_url, number, title }) => `:fast_forward: Skipping <${html_url}|${number}: ${title}> due to overlap in files.`, - merge: ({ html_url, number, title }) => - `:merged: Stage merge PR <${html_url}|${number}: ${title}>.`, + merge: ({ html_url, number, title, highImpact }) => + `:merged:${highImpact} Stage merge PR <${html_url}|${number}: ${title}>.`, failingChecks: ({ html_url, number, title }) => `:x: Skipping <${html_url}|${number}: ${title}> due to failing checks`, requireApprovals: ({ html_url, number, title }) => @@ -83,7 +84,8 @@ const RCPDates = [ }, ]; -const isHighPrio = (label) => label.includes(labels.highPriority); +const isHighPrio = (labels) => labels.includes(labels.highPriority); +const isHighImpact = (labels) => labels.includes(labels.highImpact); const hasFailingChecks = (checks) => checks.some( @@ -127,7 +129,7 @@ const getPRs = async () => { const merge = async ({ prs }) => { console.log(`Merging ${prs.length || 0} PRs that are ready... `); - for await (const { number, files, html_url, title } of prs) { + for await (const { number, files, html_url, title, labels } of prs) { if (files.some((file) => seen[file])) { await slackNotification(slack.fileOverlap({ html_url, number, title })); continue; @@ -136,7 +138,14 @@ const merge = async ({ prs }) => { if (!process.env.LOCAL_RUN) await github.rest.pulls.merge({ owner, repo, pull_number: number }); body = `- [${title}](${html_url})\n${body}`; - await slackNotification(slack.merge({ html_url, number, title })); + await slackNotification( + slack.merge({ + html_url, + number, + title, + highImpact: `${isHighImpact(labels)} ? ' :high-priority:' : ''`, + }) + ); } }; @@ -200,6 +209,10 @@ const main = async (params) => { core = params.core; const now = new Date(); + // We need to revisit this every year + if (now.getFullYear() !== 2024) { + throw new Error('ADD NEW RCPs'); + } for (const { start, end } of RCPDates) { if (start <= now && now <= end) { console.log('Current date is within a RCP. Stopping execution.'); From 1a147f5abf8bf7e8dec8f6f8b96cf10aae1b02a8 Mon Sep 17 00:00:00 2001 From: Okan Sahin Date: Tue, 7 May 2024 15:25:22 +0200 Subject: [PATCH 3/7] Rename constants --- .github/workflows/merge-to-stage.js | 60 ++++++++++++++--------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/merge-to-stage.js b/.github/workflows/merge-to-stage.js index 531d65ab15..d7ad48b222 100644 --- a/.github/workflows/merge-to-stage.js +++ b/.github/workflows/merge-to-stage.js @@ -5,19 +5,19 @@ const { } = require('./helpers.js'); // Run from the root of the project for local testing: node --env-file=.env .github/workflows/merge-to-stage.js -const prTitle = '[Release] Stage to Main'; -const seen = {}; -const requiredApprovals = process.env.REQUIRED_APPROVALS || 2; -const stage = 'stage'; -const prod = 'main'; -const labels = { +const PR_TITLE = '[Release] Stage to Main'; +const SEEN = {}; +const REQUIRED_APPROVALS = process.env.REQUIRED_APPROVALS || 2; +const STAGE = 'stage'; +const PROD = 'main'; +const LABELS = { highPriority: 'high priority', readyForStage: 'Ready for Stage', SOTPrefix: 'SOT', highImpact: 'high-impact', }; -const slack = { +const SLACK = { fileOverlap: ({ html_url, number, title }) => `:fast_forward: Skipping <${html_url}|${number}: ${title}> due to overlap in files.`, merge: ({ html_url, number, title, highImpact }) => @@ -84,8 +84,8 @@ const RCPDates = [ }, ]; -const isHighPrio = (labels) => labels.includes(labels.highPriority); -const isHighImpact = (labels) => labels.includes(labels.highImpact); +const isHighPrio = (labels) => labels.includes(LABELS.highPriority); +const isHighImpact = (labels) => labels.includes(LABELS.highImpact); const hasFailingChecks = (checks) => checks.some( @@ -95,10 +95,10 @@ const hasFailingChecks = (checks) => const getPRs = async () => { let prs = await github.rest.pulls - .list({ owner, repo, state: 'open', per_page: 100, base: stage }) + .list({ owner, repo, state: 'open', per_page: 100, base: STAGE }) .then(({ data }) => data); await Promise.all(prs.map((pr) => addLabels({ pr, github, owner, repo }))); - prs = prs.filter((pr) => pr.labels.includes(labels.readyForStage)); + prs = prs.filter((pr) => pr.labels.includes(LABELS.readyForStage)); await Promise.all([ ...prs.map((pr) => addFiles({ pr, github, owner, repo })), ...prs.map((pr) => getChecks({ pr, github, owner, repo })), @@ -107,15 +107,15 @@ const getPRs = async () => { prs = prs.filter(({ checks, reviews, html_url, number, title }) => { if (hasFailingChecks(checks)) { - slackNotification(slack.failingChecks({ html_url, number, title })); + slackNotification(SLACK.failingChecks({ html_url, number, title })); if (number === currPrNumber) core.setFailed(`Failing checks on the current PR ${number}`); return false; } const approvals = reviews.filter(({ state }) => state === 'APPROVED'); - if (approvals.length < requiredApprovals) { - slackNotification(slack.requireApprovals({ html_url, number, title })); + if (approvals.length < REQUIRED_APPROVALS) { + slackNotification(SLACK.requireApprovals({ html_url, number, title })); if (number === currPrNumber) core.setFailed(`Insufficient approvals on the current PR ${number}`); return false; @@ -130,20 +130,20 @@ const getPRs = async () => { const merge = async ({ prs }) => { console.log(`Merging ${prs.length || 0} PRs that are ready... `); for await (const { number, files, html_url, title, labels } of prs) { - if (files.some((file) => seen[file])) { - await slackNotification(slack.fileOverlap({ html_url, number, title })); - continue; + if (files.some((file) => SEEN[file])) { + await slackNotification(SLACK.fileOverlap({ html_url, number, title })); + // continue; } - files.forEach((file) => (seen[file] = true)); + files.forEach((file) => (SEEN[file] = true)); if (!process.env.LOCAL_RUN) await github.rest.pulls.merge({ owner, repo, pull_number: number }); body = `- [${title}](${html_url})\n${body}`; await slackNotification( - slack.merge({ + SLACK.merge({ html_url, number, title, - highImpact: `${isHighImpact(labels)} ? ' :high-priority:' : ''`, + highImpact: isHighImpact(labels) ? ' :high-priority:' : '', }) ); } @@ -151,12 +151,12 @@ const merge = async ({ prs }) => { const getStageToMainPR = () => github.rest.pulls - .list({ owner, repo, state: 'open', base: prod }) - .then(({ data } = {}) => data.find(({ title } = {}) => title === prTitle)) + .list({ owner, repo, state: 'open', base: PROD }) + .then(({ data } = {}) => data.find(({ title } = {}) => title === PR_TITLE)) .then((pr) => pr && addLabels({ pr, github, owner, repo })) .then((pr) => pr && addFiles({ pr, github, owner, repo })) .then((pr) => { - pr?.files.forEach((file) => (seen[file] = true)); + pr?.files.forEach((file) => (SEEN[file] = true)); return pr; }); @@ -164,8 +164,8 @@ const openStageToMainPR = async () => { const { data: comparisonData } = await github.rest.repos.compareCommits({ owner, repo, - base: prod, - head: stage, + base: PROD, + head: STAGE, }); for (const commit of comparisonData.commits) { @@ -188,12 +188,12 @@ const openStageToMainPR = async () => { } = await github.rest.pulls.create({ owner, repo, - title: prTitle, - head: stage, - base: prod, + title: PR_TITLE, + head: STAGE, + base: PROD, body, }); - await slackNotification(slack.openedSyncPr({ html_url, number })); + await slackNotification(SLACK.openedSyncPr({ html_url, number })); } catch (error) { if (error.message.includes('No commits between main and stage')) return console.log('No new commits, no stage->main PR opened'); @@ -222,7 +222,7 @@ const main = async (params) => { try { const stageToMainPR = await getStageToMainPR(); console.log('has Stage to Main PR:', !!stageToMainPR); - if (stageToMainPR?.labels.some((label) => label.includes(labels.SOTPrefix))) + if (stageToMainPR?.labels.some((label) => label.includes(LABELS.SOTPrefix))) return console.log('PR exists & testing started. Stopping execution.'); const prs = await getPRs(); await merge({ prs: prs.filter(({ labels }) => isHighPrio(labels)) }); From 15627f198b3a508d47400bb8b17d0b1d472529b4 Mon Sep 17 00:00:00 2001 From: Okan Sahin Date: Tue, 7 May 2024 15:28:03 +0200 Subject: [PATCH 4/7] Add exclamation mark --- .github/workflows/merge-to-stage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge-to-stage.js b/.github/workflows/merge-to-stage.js index d7ad48b222..9a687a3920 100644 --- a/.github/workflows/merge-to-stage.js +++ b/.github/workflows/merge-to-stage.js @@ -143,7 +143,7 @@ const merge = async ({ prs }) => { html_url, number, title, - highImpact: isHighImpact(labels) ? ' :high-priority:' : '', + highImpact: isHighImpact(labels) ? ' :exclamation:' : '', }) ); } From 533b3c724f7b10ceb1dcd4b1bdc87f340c5b6858 Mon Sep 17 00:00:00 2001 From: Okan Sahin Date: Tue, 7 May 2024 15:30:08 +0200 Subject: [PATCH 5/7] Add the alert emoji --- .github/workflows/merge-to-stage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge-to-stage.js b/.github/workflows/merge-to-stage.js index 9a687a3920..efbc6ce1d4 100644 --- a/.github/workflows/merge-to-stage.js +++ b/.github/workflows/merge-to-stage.js @@ -143,7 +143,7 @@ const merge = async ({ prs }) => { html_url, number, title, - highImpact: isHighImpact(labels) ? ' :exclamation:' : '', + highImpact: isHighImpact(labels) ? ' :alert:' : '', }) ); } From 4fe42094365a213dda59e74423ba974c777a8e55 Mon Sep 17 00:00:00 2001 From: Okan Sahin Date: Tue, 7 May 2024 16:08:11 +0200 Subject: [PATCH 6/7] Add a high impact slack webhook --- .github/workflows/helpers.js | 4 ++-- .github/workflows/merge-to-stage.js | 10 ++++++++-- .github/workflows/merge-to-stage.yaml | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/helpers.js b/.github/workflows/helpers.js index 8cfb11fd14..da3565c095 100644 --- a/.github/workflows/helpers.js +++ b/.github/workflows/helpers.js @@ -22,9 +22,9 @@ Then run: node --env-file=.env .github/workflows/update-ims.js`); }; }; -const slackNotification = (text) => { +const slackNotification = (text, webhook) => { console.log(text); - return fetch(process.env.MILO_RELEASE_SLACK_WH, { + return fetch(webhook || process.env.MILO_RELEASE_SLACK_WH, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/.github/workflows/merge-to-stage.js b/.github/workflows/merge-to-stage.js index efbc6ce1d4..759a02cae3 100644 --- a/.github/workflows/merge-to-stage.js +++ b/.github/workflows/merge-to-stage.js @@ -85,7 +85,6 @@ const RCPDates = [ ]; const isHighPrio = (labels) => labels.includes(LABELS.highPriority); -const isHighImpact = (labels) => labels.includes(LABELS.highImpact); const hasFailingChecks = (checks) => checks.some( @@ -138,12 +137,19 @@ const merge = async ({ prs }) => { if (!process.env.LOCAL_RUN) await github.rest.pulls.merge({ owner, repo, pull_number: number }); body = `- [${title}](${html_url})\n${body}`; + const isHighImpact = labels.includes(LABELS.highImpact); + if (isHighImpact && process.env.SLACK_HIGH_IMPACT_PR_WEBHOOK) { + await slackNotification( + SLACK.merge({ html_url, number, title, highImpact: ' :alert:' }), + process.env.SLACK_HIGH_IMPACT_PR_WEBHOOK + ); + } await slackNotification( SLACK.merge({ html_url, number, title, - highImpact: isHighImpact(labels) ? ' :alert:' : '', + highImpact: isHighImpact ? ' :alert:' : '', }) ); } diff --git a/.github/workflows/merge-to-stage.yaml b/.github/workflows/merge-to-stage.yaml index 3310906661..4187295192 100644 --- a/.github/workflows/merge-to-stage.yaml +++ b/.github/workflows/merge-to-stage.yaml @@ -10,6 +10,7 @@ on: env: MILO_RELEASE_SLACK_WH: ${{ secrets.MILO_RELEASE_SLACK_WH }} REQUIRED_APPROVALS: ${{ secrets.REQUIRED_APPROVALS }} + SLACK_HIGH_IMPACT_PR_WEBHOOK: ${{ secrets.SLACK_HIGH_IMPACT_PR_WEBHOOK }} jobs: merge-to-stage: From 7f8664fbbf47f56b7b0f0295fd0d49a37f246fc2 Mon Sep 17 00:00:00 2001 From: Okan Sahin Date: Tue, 7 May 2024 16:12:31 +0200 Subject: [PATCH 7/7] Re-add overlap check --- .github/workflows/merge-to-stage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge-to-stage.js b/.github/workflows/merge-to-stage.js index 759a02cae3..153c155904 100644 --- a/.github/workflows/merge-to-stage.js +++ b/.github/workflows/merge-to-stage.js @@ -131,7 +131,7 @@ const merge = async ({ prs }) => { for await (const { number, files, html_url, title, labels } of prs) { if (files.some((file) => SEEN[file])) { await slackNotification(SLACK.fileOverlap({ html_url, number, title })); - // continue; + continue; } files.forEach((file) => (SEEN[file] = true)); if (!process.env.LOCAL_RUN)