diff --git a/.github/workflows/helpers.js b/.github/workflows/helpers.js index da3565c095..c086fe88d1 100644 --- a/.github/workflows/helpers.js +++ b/.github/workflows/helpers.js @@ -3,6 +3,60 @@ 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 CURRENT_YEAR = 2024; +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 isWithinRCP = () => { + const now = new Date(); + if (now.getFullYear() !== CURRENT_YEAR) { + console.log(`ADD NEW RCPs for ${CURRENT_YEAR + 1}`); + return true; + } + + if (RCPDates.some(({ start, end }) => start <= now && now <= end)) { + console.log('Current date is within a RCP. Stopping execution.'); + return true; + } + + return false; +}; const getLocalConfigs = () => { if (!owner || !repo || !auth) { @@ -12,7 +66,12 @@ Then run: node --env-file=.env .github/workflows/update-ims.js`); const { Octokit } = require('@octokit/rest'); return { - github: { rest: new Octokit({ auth }) }, + github: { + rest: new Octokit({ auth }), + repos: { + createDispatchEvent: () => console.log('local mock createDispatch'), + }, + }, context: { repo: { owner, @@ -83,4 +142,5 @@ module.exports = { getLocalConfigs, slackNotification, pulls: { addLabels, addFiles, getChecks, getReviews }, + isWithinRCP, }; diff --git a/.github/workflows/mark-stale-prs.yaml b/.github/workflows/mark-stale-prs.yaml index add1224c3e..788f30eaf7 100644 --- a/.github/workflows/mark-stale-prs.yaml +++ b/.github/workflows/mark-stale-prs.yaml @@ -1,7 +1,7 @@ -name: "Close stale pull requests" +name: Close stale pull requests on: schedule: - - cron: "0 0 * * *" + - cron: '0 0 * * *' workflow_dispatch: jobs: diff --git a/.github/workflows/merge-to-main.js b/.github/workflows/merge-to-main.js new file mode 100644 index 0000000000..efbc9e55bc --- /dev/null +++ b/.github/workflows/merge-to-main.js @@ -0,0 +1,77 @@ +const { + slackNotification, + getLocalConfigs, + isWithinRCP, + 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-main.js +const PR_TITLE = '[Release] Stage to Main'; +const STAGE = 'stage'; +const PROD = 'main'; + +let github, owner, repo; + +const getStageToMainPR = () => + github.rest.pulls + .list({ owner, repo, state: 'open', base: PROD, head: STAGE }) + .then(({ data } = {}) => data.find(({ title } = {}) => title === PR_TITLE)) + .then((pr) => pr && addLabels({ pr, github, owner, repo })); + +const workingHours = () => { + const now = new Date(); + const day = now.getUTCDay(); + const hour = now.getUTCHours(); + const isSunday = day === 0; + const isSaturday = day === 6; + const isFriday = day === 5; + return hour >= 8 && hour <= 20 && !isFriday && !isSaturday && !isSunday; +}; + +const main = async (params) => { + github = params.github; + owner = params.context.repo.owner; + repo = params.context.repo.repo; + + if (isWithinRCP()) return console.log('Stopped, within RCP period.'); + if (!workingHours()) return console.log('Stopped, outside working hours.'); + + try { + const stageToMainPR = await getStageToMainPR(); + const signOffs = stageToMainPR?.labels.filter((l) => l.includes('SOT')); + console.log(`${signOffs.length} SOT labels on PR ${stageToMainPR.number}`); + if (signOffs.length >= 4) { + console.log('Stage to Main PR has all required labels. Merging...'); + await github.rest.pulls.merge({ + owner, + repo, + pull_number: stageToMainPR.number, + merge_method: 'merge', + }); + + await slackNotification( + `:rocket: Production release <${stageToMainPR.html_url}|${stageToMainPR.number}>` + ); + + await github.rest.repos.createDispatchEvent({ + owner, + repo, + event_type: 'merge-to-stage', + }); + } + + console.log('Process successfully executed.'); + } catch (error) { + console.error(error); + } +}; + +if (process.env.LOCAL_RUN) { + const { github, context } = getLocalConfigs(); + main({ + github, + context, + }); +} + +module.exports = main; diff --git a/.github/workflows/merge-to-main.yaml b/.github/workflows/merge-to-main.yaml new file mode 100644 index 0000000000..9b4ba782bf --- /dev/null +++ b/.github/workflows/merge-to-main.yaml @@ -0,0 +1,37 @@ +name: Merge to main + +on: + pull_request: + types: [labeled] + schedule: + - cron: '0 9 * * *' # Run every day at 9am UTC + workflow_dispatch: # Allow manual trigger + +env: + MILO_RELEASE_SLACK_WH: ${{ secrets.MILO_RELEASE_SLACK_WH }} + +jobs: + merge-to-main: + runs-on: ubuntu-latest + environment: milo_pr_merge + # Run this when manually triggered or on a schedule + # Otherwise run this only on PRs that are merged from stage to main + if: github.repository_owner == 'adobecom' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' && github.event.pull_request.head.ref == 'stage')) + + steps: + - uses: actions/create-github-app-token@v1.10.0 + id: milo-pr-merge-token + with: + app-id: ${{ secrets.MILO_PR_MERGE_APP_ID }} + private-key: ${{ secrets.MILO_PR_MERGE_PRIVATE_KEY }} + + - name: Checkout repository + uses: actions/checkout@v4.1.4 + + - name: Merge to main + uses: actions/github-script@v7.0.1 + with: + github-token: ${{ steps.milo-pr-merge-token.outputs.token }} + script: | + const main = require('./.github/workflows/merge-to-main.js') + main({ github, context }) diff --git a/.github/workflows/merge-to-stage.js b/.github/workflows/merge-to-stage.js index 4c81f2aaa7..1ce20ca4c4 100644 --- a/.github/workflows/merge-to-stage.js +++ b/.github/workflows/merge-to-stage.js @@ -1,6 +1,7 @@ const { slackNotification, getLocalConfigs, + isWithinRCP, pulls: { addLabels, addFiles, getChecks, getReviews }, } = require('./helpers.js'); @@ -44,45 +45,6 @@ let body = ` - 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 = (labels) => labels.includes(LABELS.highPriority); const hasFailingChecks = (checks) => @@ -217,18 +179,8 @@ const main = async (params) => { github = params.github; owner = params.context.repo.owner; repo = params.context.repo.repo; + if (isWithinRCP()) return console.log('Stopped, within RCP period.'); - 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.'); - return; - } - } try { const stageToMainPR = await getStageToMainPR(); console.log('has Stage to Main PR:', !!stageToMainPR); diff --git a/.github/workflows/merge-to-stage.yaml b/.github/workflows/merge-to-stage.yaml index d99b26e942..c6457233d6 100644 --- a/.github/workflows/merge-to-stage.yaml +++ b/.github/workflows/merge-to-stage.yaml @@ -4,6 +4,8 @@ on: schedule: - cron: '0 */4 * * *' # Run every 4 hours workflow_dispatch: # Allow manual trigger + repository_dispatch: + types: [merge-to-stage] env: MILO_RELEASE_SLACK_WH: ${{ secrets.MILO_RELEASE_SLACK_WH }}