From 40bda5273eaea8a575de3ac038f08d509ac0c204 Mon Sep 17 00:00:00 2001 From: Timothy Willard <9395586+TimothyWillard@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:05:18 -0400 Subject: [PATCH 1/4] Custom GH Action To Create PRs to Sync GitBook --- .github/workflows/create-docs-pr.yml | 146 +++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 .github/workflows/create-docs-pr.yml diff --git a/.github/workflows/create-docs-pr.yml b/.github/workflows/create-docs-pr.yml new file mode 100644 index 000000000..e26581224 --- /dev/null +++ b/.github/workflows/create-docs-pr.yml @@ -0,0 +1,146 @@ +name: create-docs-pr + +on: + workflow_dispatch: + schedule: + - cron: '1 10 * * 1-5' + +jobs: + create-pr: + runs-on: ubuntu-latest + env: + DOCS_BRANCH: documentation-gitbook + PR_LABELS: 'documentation,high priority' + PR_ASSIGNEES: 'emprzy,jcblemai,pearsonca,saraloo,TimothyWillard' + OWNER: 'HopkinsIDD' + REPO: 'flepiMoP' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + fetch-depth: 0 + - name: Determine Commits Ahead/Behind + run: | + git fetch --all + DOCS_AHEAD_MAIN=$( eval "git rev-list --count origin/main..origin/$DOCS_BRANCH -- documentation/" ) + DOCS_BEHIND_MAIN=$( eval "git rev-list --count origin/$DOCS_BRANCH..origin/main -- documentation/" ) + echo "DOCS_AHEAD_MAIN=$DOCS_AHEAD_MAIN" >> $GITHUB_ENV + echo "DOCS_BEHIND_MAIN=$DOCS_BEHIND_MAIN" >> $GITHUB_ENV + - name: Create PR If Needed + uses: actions/github-script@v7 + id: create-pr + with: + result-encoding: string + retries: 2 + script: | + const { DOCS_AHEAD_MAIN, DOCS_BEHIND_MAIN, DOCS_BRANCH, PR_LABELS, PR_ASSIGNEES, OWNER, REPO } = process.env + const prLabels = PR_LABELS.split(",") + const prAssignees = PR_ASSIGNEES.split(",") + const cc = prAssignees.map(x => "@" + x).join(", ") + const docsAheadMain = parseInt(DOCS_AHEAD_MAIN, 10) + const docsBehindMain = parseInt(DOCS_BEHIND_MAIN, 10) + if (isNaN(docsAheadMain) || isNaN(docsBehindMain)) { + throw new Error(`Cannot convert either "${DOCS_AHEAD_MAIN}" or "${DOCS_BEHIND_MAIN}" to integers.`) + } + const initialResults = await github.rest.search.issuesAndPullRequests({ + q: `owner:${OWNER} repo:${REPO} is:open is:pr in:title 'Sync GitBook'` + }) + let count = initialResults.data.total_count + console.log(`${DOCS_BRANCH} is ${docsAheadMain} ahead, ${docsBehindMain} behind main. The open PR count is ${count}.`) + async function deletePRs() { + initialResults.data.items.forEach((item) => { + github.rest.pulls.update({ + owner: OWNER, + repo: REPO, + pull_number: item.number, + state: "closed" + }) + }) + } + async function createPR({ from, to, body }) { + let prBody = `cc: ${cc}.` + if (body !== null) { + prBody = `${body} ${prBody}` + } + const today = (new Date()).toLocaleDateString() + const pr = await github.rest.pulls.create({ + owner: OWNER, + repo: REPO, + head: from, + base: to, + title: `${today} Sync GitBook From ${from} Into ${to}`, + body: prBody + }) + const labels = await github.rest.issues.addLabels({ + owner: OWNER, + repo: REPO, + issue_number: pr.data.number, + labels: prLabels + }) + const assignees = await github.rest.issues.addAssignees({ + owner: OWNER, + repo: REPO, + issue_number: pr.data.number, + assignees: prAssignees + }) + return pr.data.number + } + if (docsAheadMain > 0 && docsBehindMain > 0) { + if (count !== 2 && count > 0) { + // need to close this PR + deletePRs() + count = 0; + } + if (count !== 2) { + const docsIntoMainPrNumber = await createPR({ + from: DOCS_BRANCH, + to: "main", + body: null + }) + const mainIntoDocsPrNumber = await createPR({ + from: "main", + to: DOCS_BRANCH, + body: `Blocked by GH-${docsIntoMainPrNumber}.` + }) + } + } else if (docsAheadMain > 0) { + // need a PR from docs to main + if (count !== 1 && count > 0) { + deletePRs() + count = 0; + } else if (count === 1) { + if (!initialResults.data.items[0].title.includes(`Sync GitBook From ${DOCS_BRANCH} Into main`)) { + deletePRs() + count = 0; + } + } + if (count !== 1) { + const docsIntoMainPrNumber = await createPR({ + from: DOCS_BRANCH, + to: "main", + body: null + }) + } + } else if (docsBehindMain > 0) { + // need a PR from main to docs + if (count !== 1 && count > 0) { + deletePRs() + count = 0 + } else if (count === 1) { + if (!initialResults.data.items[0].title.includes(`Sync GitBook From main Into ${DOCS_BRANCH}`)) { + deletePRs() + count = 0 + } + } + if (count !== 1) { + const mainIntoDocsPrNumber = await createPR({ + from: "main", + to: DOCS_BRANCH, + body: null + }) + } + } else if (count > 0) { + // need to close PRs + deletePRs() + } From a5776b14131c8e31c694c22ccb0bba1253ff1905 Mon Sep 17 00:00:00 2001 From: Timothy Willard <9395586+TimothyWillard@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:17:48 -0400 Subject: [PATCH 2/4] Add more comments to explain logic Per @pearsonca request in GH-357 add more comments to explain the branching logic. --- .github/workflows/create-docs-pr.yml | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/create-docs-pr.yml b/.github/workflows/create-docs-pr.yml index e26581224..efd2db00b 100644 --- a/.github/workflows/create-docs-pr.yml +++ b/.github/workflows/create-docs-pr.yml @@ -87,12 +87,14 @@ jobs: return pr.data.number } if (docsAheadMain > 0 && docsBehindMain > 0) { + // Need PRs both ways if (count !== 2 && count > 0) { - // need to close this PR + // Previously opened PRs (i.e. only going one way) to be closed deletePRs() count = 0; } if (count !== 2) { + // There aren't the 2 expected PRs open, open them const docsIntoMainPrNumber = await createPR({ from: DOCS_BRANCH, to: "main", @@ -105,17 +107,21 @@ jobs: }) } } else if (docsAheadMain > 0) { - // need a PR from docs to main - if (count !== 1 && count > 0) { + // Need a PR from docs to main + if (count > 1) { + // 2 or more PRs, likely stale deletePRs() count = 0; } else if (count === 1) { - if (!initialResults.data.items[0].title.includes(`Sync GitBook From ${DOCS_BRANCH} Into main`)) { + let title = initialResults.data.items[0].title + if (!title.includes(`Sync GitBook From ${DOCS_BRANCH} Into main`)) { + // A PR that does not match the expected direction, stale deletePRs() count = 0; } } if (count !== 1) { + // There isn't the 1 expected PR, open it const docsIntoMainPrNumber = await createPR({ from: DOCS_BRANCH, to: "main", @@ -123,17 +129,21 @@ jobs: }) } } else if (docsBehindMain > 0) { - // need a PR from main to docs - if (count !== 1 && count > 0) { + // Need a PR from main to docs + if (count > 1) { + // 2 or more PRs, likely stale deletePRs() count = 0 } else if (count === 1) { - if (!initialResults.data.items[0].title.includes(`Sync GitBook From main Into ${DOCS_BRANCH}`)) { + let title = initialResults.data.items[0].title + if (!title.includes(`Sync GitBook From main Into ${DOCS_BRANCH}`)) { + // A PR that does not match the expected direction, stale deletePRs() count = 0 } } if (count !== 1) { + // There isn't the 1 expected PR, open it const mainIntoDocsPrNumber = await createPR({ from: "main", to: DOCS_BRANCH, @@ -141,6 +151,6 @@ jobs: }) } } else if (count > 0) { - // need to close PRs + // Ahead/behind commits is 0 but there are stale PRs to close deletePRs() } From cdc20233eaca0db689a4922fccdb7d109221f943 Mon Sep 17 00:00:00 2001 From: Timothy Willard <9395586+TimothyWillard@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:19:50 -0400 Subject: [PATCH 3/4] Make PR Title Configurable The main keyword in the PR title is configurable via the `PR_TITLE` environment variable. --- .github/workflows/create-docs-pr.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/create-docs-pr.yml b/.github/workflows/create-docs-pr.yml index efd2db00b..f75812ccb 100644 --- a/.github/workflows/create-docs-pr.yml +++ b/.github/workflows/create-docs-pr.yml @@ -12,6 +12,7 @@ jobs: DOCS_BRANCH: documentation-gitbook PR_LABELS: 'documentation,high priority' PR_ASSIGNEES: 'emprzy,jcblemai,pearsonca,saraloo,TimothyWillard' + PR_TITLE: 'Sync GitBook' OWNER: 'HopkinsIDD' REPO: 'flepiMoP' steps: @@ -34,7 +35,7 @@ jobs: result-encoding: string retries: 2 script: | - const { DOCS_AHEAD_MAIN, DOCS_BEHIND_MAIN, DOCS_BRANCH, PR_LABELS, PR_ASSIGNEES, OWNER, REPO } = process.env + const { DOCS_AHEAD_MAIN, DOCS_BEHIND_MAIN, DOCS_BRANCH, PR_LABELS, PR_ASSIGNEES, PR_TITLE, OWNER, REPO } = process.env const prLabels = PR_LABELS.split(",") const prAssignees = PR_ASSIGNEES.split(",") const cc = prAssignees.map(x => "@" + x).join(", ") @@ -44,7 +45,7 @@ jobs: throw new Error(`Cannot convert either "${DOCS_AHEAD_MAIN}" or "${DOCS_BEHIND_MAIN}" to integers.`) } const initialResults = await github.rest.search.issuesAndPullRequests({ - q: `owner:${OWNER} repo:${REPO} is:open is:pr in:title 'Sync GitBook'` + q: `owner:${OWNER} repo:${REPO} is:open is:pr in:title '${PR_TITLE}'` }) let count = initialResults.data.total_count console.log(`${DOCS_BRANCH} is ${docsAheadMain} ahead, ${docsBehindMain} behind main. The open PR count is ${count}.`) @@ -69,7 +70,7 @@ jobs: repo: REPO, head: from, base: to, - title: `${today} Sync GitBook From ${from} Into ${to}`, + title: `${today} ${PR_TITLE} From ${from} Into ${to}`, body: prBody }) const labels = await github.rest.issues.addLabels({ @@ -114,7 +115,7 @@ jobs: count = 0; } else if (count === 1) { let title = initialResults.data.items[0].title - if (!title.includes(`Sync GitBook From ${DOCS_BRANCH} Into main`)) { + if (!title.includes(`${PR_TITLE} From ${DOCS_BRANCH} Into main`)) { // A PR that does not match the expected direction, stale deletePRs() count = 0; @@ -136,7 +137,7 @@ jobs: count = 0 } else if (count === 1) { let title = initialResults.data.items[0].title - if (!title.includes(`Sync GitBook From main Into ${DOCS_BRANCH}`)) { + if (!title.includes(`${PR_TITLE} From main Into ${DOCS_BRANCH}`)) { // A PR that does not match the expected direction, stale deletePRs() count = 0 From 04a4c159b35d7bb1670c8a88496d7909d89d071a Mon Sep 17 00:00:00 2001 From: Timothy Willard <9395586+TimothyWillard@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:49:33 -0400 Subject: [PATCH 4/4] Avoid dismissing PRs if possible Changed logic to avoid dismissing PRs if possible. Added a table at the top of the file to explain details at a high level. Throw exception if more than two open PRs with the `PR_TITLE` are present. --- .github/workflows/create-docs-pr.yml | 142 ++++++++++++++++----------- 1 file changed, 87 insertions(+), 55 deletions(-) diff --git a/.github/workflows/create-docs-pr.yml b/.github/workflows/create-docs-pr.yml index f75812ccb..4feb6b3a7 100644 --- a/.github/workflows/create-docs-pr.yml +++ b/.github/workflows/create-docs-pr.yml @@ -1,3 +1,10 @@ +# Commits 0 Open PRs 1 Open PR 2 Open PRs +# ------------------------------------------- ---------------------------------------------------- ------------------------------------------------------------------------------------------------------- --------------------------------------------------------- +# Neither ahead or behind Nothing Dismiss PRs Dismiss PRs +# `documentation-gitbook` is behind Open A PR from `main` into `documentation-gitbook` Determine the directionality, if wrong dismiss and open a PR from `main` into `documentation-gitbook` Dismiss the PR from `documentation-gitbook` into `main` +# `documentation-gitbook` is behind Open A PR from `documentation-gitbook` into `main` Determine the directionality, if wrong dismiss and open a PR from `documentation-gitbook` into `main` Dismiss the PR from `main` into `documentation-gitbook` +# `documentation-gitbook` is ahead & behind Open 2 PRs Determine the directionality of the missing one and open that Nothing + name: create-docs-pr on: @@ -33,7 +40,7 @@ jobs: id: create-pr with: result-encoding: string - retries: 2 + retries: 5 script: | const { DOCS_AHEAD_MAIN, DOCS_BEHIND_MAIN, DOCS_BRANCH, PR_LABELS, PR_ASSIGNEES, PR_TITLE, OWNER, REPO } = process.env const prLabels = PR_LABELS.split(",") @@ -47,9 +54,12 @@ jobs: const initialResults = await github.rest.search.issuesAndPullRequests({ q: `owner:${OWNER} repo:${REPO} is:open is:pr in:title '${PR_TITLE}'` }) - let count = initialResults.data.total_count + const count = initialResults.data.total_count + if (count > 2) { + throw new Error(`There are ${count} open PRs containing '${PR_TITLE}', but this action can only handle 0, 1, or 2 open PRs.`) + } console.log(`${DOCS_BRANCH} is ${docsAheadMain} ahead, ${docsBehindMain} behind main. The open PR count is ${count}.`) - async function deletePRs() { + async function dismissAllPRs() { initialResults.data.items.forEach((item) => { github.rest.pulls.update({ owner: OWNER, @@ -73,13 +83,13 @@ jobs: title: `${today} ${PR_TITLE} From ${from} Into ${to}`, body: prBody }) - const labels = await github.rest.issues.addLabels({ + github.rest.issues.addLabels({ owner: OWNER, repo: REPO, issue_number: pr.data.number, labels: prLabels }) - const assignees = await github.rest.issues.addAssignees({ + github.rest.issues.addAssignees({ owner: OWNER, repo: REPO, issue_number: pr.data.number, @@ -87,71 +97,93 @@ jobs: }) return pr.data.number } - if (docsAheadMain > 0 && docsBehindMain > 0) { - // Need PRs both ways - if (count !== 2 && count > 0) { - // Previously opened PRs (i.e. only going one way) to be closed - deletePRs() - count = 0; - } - if (count !== 2) { - // There aren't the 2 expected PRs open, open them - const docsIntoMainPrNumber = await createPR({ - from: DOCS_BRANCH, - to: "main", + async function handleSingleDirection({ from, to }) { + if (count === 0) { + // There isn't the 1 expected PR, open it + createPR({ + from: from, + to: to, body: null }) - const mainIntoDocsPrNumber = await createPR({ - from: "main", - to: DOCS_BRANCH, - body: `Blocked by GH-${docsIntoMainPrNumber}.` - }) - } - } else if (docsAheadMain > 0) { - // Need a PR from docs to main - if (count > 1) { - // 2 or more PRs, likely stale - deletePRs() - count = 0; } else if (count === 1) { + // There is a PR open, determine direction let title = initialResults.data.items[0].title - if (!title.includes(`${PR_TITLE} From ${DOCS_BRANCH} Into main`)) { - // A PR that does not match the expected direction, stale - deletePRs() - count = 0; + if (!title.includes(`${PR_TITLE} From ${from} Into ${to}`)) { + // Wrong direction, close & recreate + dismissAllPRs() + createPR({ + from: from, + to: to, + body: null + }) } + } else { + // There are two PRs open, close the wrong direction + initialResults.data.items.forEach((item) => { + if (!title.includes(`${PR_TITLE} From ${from} Into ${to}`)) { + github.rest.pulls.update({ + owner: OWNER, + repo: REPO, + pull_number: item.number, + state: "closed" + }) + } + }) } - if (count !== 1) { - // There isn't the 1 expected PR, open it + } + if (docsAheadMain > 0 && docsBehindMain > 0) { + // Need PRs both ways + if (count === 0) { + // There are 0 PRs open, open both of them const docsIntoMainPrNumber = await createPR({ from: DOCS_BRANCH, to: "main", body: null }) - } - } else if (docsBehindMain > 0) { - // Need a PR from main to docs - if (count > 1) { - // 2 or more PRs, likely stale - deletePRs() - count = 0 - } else if (count === 1) { - let title = initialResults.data.items[0].title - if (!title.includes(`${PR_TITLE} From main Into ${DOCS_BRANCH}`)) { - // A PR that does not match the expected direction, stale - deletePRs() - count = 0 - } - } - if (count !== 1) { - // There isn't the 1 expected PR, open it - const mainIntoDocsPrNumber = await createPR({ + createPR({ from: "main", to: DOCS_BRANCH, - body: null + body: `Please merge GH-${docsIntoMainPrNumber} first.` }) + } else if (count === 1) { + // There is already a PR open in one direction, open the other direction + let title = initialResults.data.items[0].title + let number = initialResults.data.items[0].number + if (title.includes(`${PR_TITLE} From ${DOCS_BRANCH} Into main`)) { + // From docs into main already exists, create main into docs + createPR({ + from: "main", + to: DOCS_BRANCH, + body: `Please merge GH-${number} first.` + }) + } else { + // From main into docs already exists, create docs into main + const docsIntoMainPrNumber = await createPR({ + from: DOCS_BRANCH, + to: "main", + body: null + }) + github.rest.issues.createComment({ + owner: OWNER, + repo: REPO, + issue_number: number, + body: `Please merge GH-${docsIntoMainPrNumber} first.` + }) + } } + } else if (docsAheadMain > 0) { + // Need a PR from docs to main + handleSingleDirection({ + from: DOCS_BRANCH, + to: "main" + }) + } else if (docsBehindMain > 0) { + // Need a PR from main to docs + handleSingleDirection({ + from: "main", + to: DOCS_BRANCH + }) } else if (count > 0) { // Ahead/behind commits is 0 but there are stale PRs to close - deletePRs() + dismissAllPRs() }