|
| 1 | +name: Repo Sync |
| 2 | + |
| 3 | +# **What it does**: HashiCorp Docs has two repositories: hashicorp/web-unified-docs (public) and hashicorp/web-unified-docs-internal (private). |
| 4 | +# This GitHub Actions workflow keeps the `main` branch of those two repos in sync. |
| 5 | +# **Why we have it**: To keep the open-source repository up-to-date |
| 6 | +# while still having an internal repository for sensitive work. |
| 7 | +# **Who does it impact**: Open-source. |
| 8 | + |
| 9 | +on: |
| 10 | + workflow_dispatch: |
| 11 | + schedule: |
| 12 | + - cron: '0 */2 * * *' # Runs every 2 hours |
| 13 | + |
| 14 | +permissions: |
| 15 | + contents: write |
| 16 | + pull-requests: write |
| 17 | + |
| 18 | +jobs: |
| 19 | + repo-sync: |
| 20 | + if: github.repository == 'hashicorp/web-unified-docs-internal' || github.repository == 'hashicorp/web-unified-docs' |
| 21 | + name: Repo Sync |
| 22 | + runs-on: ubuntu-latest |
| 23 | + steps: |
| 24 | + - name: Check out repo |
| 25 | + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 2024-10-28 |
| 26 | + |
| 27 | + - name: Sync repo to branch |
| 28 | + uses: repo-sync/github-sync@3832fe8e2be32372e1b3970bbae8e7079edeec88 # v2.3.0 2023-07-13 |
| 29 | + with: |
| 30 | + source_repo: https://${{ secrets.CI_GITHUB_TOKEN }}@github.com/hashicorp/${{ github.repository == 'hashicorp/web-unified-docs-internal' && 'web-unified-docs' || 'web-unified-docs-internal' }}.git |
| 31 | + source_branch: main |
| 32 | + destination_branch: repo-sync |
| 33 | + github_token: ${{ secrets.CI_GITHUB_TOKEN }} |
| 34 | + |
| 35 | + - name: Ship pull request |
| 36 | + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 2023-11-20 |
| 37 | + with: |
| 38 | + github-token: ${{ secrets.CI_GITHUB_TOKEN }} |
| 39 | + result-encoding: string |
| 40 | + script: | |
| 41 | + const { owner, repo } = context.repo |
| 42 | + const head = 'github:repo-sync' |
| 43 | + const base = 'main' |
| 44 | +
|
| 45 | + async function closePullRequest(prNumber) { |
| 46 | + console.log('Closing pull request', prNumber) |
| 47 | + await github.rest.pulls.update({ |
| 48 | + owner, |
| 49 | + repo, |
| 50 | + pull_number: prNumber, |
| 51 | + state: 'closed' |
| 52 | + }) |
| 53 | + // Error loud here, so no try/catch |
| 54 | + console.log('Closed pull request', prNumber) |
| 55 | + } |
| 56 | +
|
| 57 | + console.log('Closing any existing pull requests') |
| 58 | + const { data: existingPulls } = await github.rest.pulls.list({ owner, repo, head, base }) |
| 59 | + if (existingPulls.length) { |
| 60 | + console.log('Found existing pull requests', existingPulls.map(pull => pull.number)) |
| 61 | + for (const pull of existingPulls) { |
| 62 | + await closePullRequest(pull.number) |
| 63 | + } |
| 64 | + console.log('Closed existing pull requests') |
| 65 | + } |
| 66 | +
|
| 67 | + try { |
| 68 | + const { data } = await github.rest.repos.compareCommits({ |
| 69 | + owner, |
| 70 | + repo, |
| 71 | + head, |
| 72 | + base, |
| 73 | + }) |
| 74 | + const { files } = data |
| 75 | + console.log(`File changes between ${head} and ${base}:`, files) |
| 76 | + if (!files.length) { |
| 77 | + console.log('No files changed, bailing') |
| 78 | + return |
| 79 | + } |
| 80 | + } catch (err) { |
| 81 | + console.error(`Unable to compute the files difference between ${head} and ${base}`, err.message) |
| 82 | + } |
| 83 | +
|
| 84 | + console.log('Creating a new pull request') |
| 85 | + const body = ` |
| 86 | + This is an automated pull request to sync changes between the public and private unified docs repos. |
| 87 | +
|
| 88 | + To preserve continuity across repos, _do not squash_ this pull request. |
| 89 | + ` |
| 90 | + let pull, pull_number |
| 91 | + try { |
| 92 | + const response = await github.rest.pulls.create({ |
| 93 | + owner, |
| 94 | + repo, |
| 95 | + head, |
| 96 | + base, |
| 97 | + title: 'Repo sync', |
| 98 | + body, |
| 99 | + }) |
| 100 | + pull = response.data |
| 101 | + pull_number = pull.number |
| 102 | + console.log('Created pull request successfully', pull.html_url) |
| 103 | + } catch (err) { |
| 104 | + // Don't error/alert if there's no commits to sync |
| 105 | + // Don't throw if > 100 pulls with same head_sha issue |
| 106 | + if (err.message?.includes('No commits') || err.message?.includes('same head_sha')) { |
| 107 | + console.log(err.message) |
| 108 | + return |
| 109 | + } |
| 110 | + throw err |
| 111 | + } |
| 112 | +
|
| 113 | + console.log('Locking conversations to prevent spam') |
| 114 | + try { |
| 115 | + await github.rest.issues.lock({ |
| 116 | + ...context.repo, |
| 117 | + issue_number: pull_number, |
| 118 | + lock_reason: 'spam' |
| 119 | + }) |
| 120 | + console.log('Locked the pull request to prevent spam') |
| 121 | + } catch (error) { |
| 122 | + console.error('Failed to lock the pull request.', error) |
| 123 | + // Don't fail the workflow |
| 124 | + } |
| 125 | +
|
| 126 | + console.log('Counting files changed') |
| 127 | + const { data: prFiles } = await github.rest.pulls.listFiles({ owner, repo, pull_number }) |
| 128 | + if (prFiles.length) { |
| 129 | + console.log(prFiles.length, 'files have changed') |
| 130 | + } else { |
| 131 | + console.log('No files changed, closing') |
| 132 | + await closePullRequest(pull_number) |
| 133 | + return |
| 134 | + } |
| 135 | +
|
| 136 | + console.log('Checking for merge conflicts') |
| 137 | + if (pull.mergeable_state === 'dirty') { |
| 138 | + console.log('Pull request has a conflict', pull.html_url) |
| 139 | + await closePullRequest(pull_number) |
| 140 | + throw new Error('Pull request has a conflict, please resolve manually') |
| 141 | + } |
| 142 | + console.log('No detected merge conflicts') |
| 143 | +
|
| 144 | + console.log('Merging the pull request') |
| 145 | + // Admin merge pull request to avoid squash |
| 146 | + await github.rest.pulls.merge({ |
| 147 | + owner, |
| 148 | + repo, |
| 149 | + pull_number, |
| 150 | + merge_method: 'merge', |
| 151 | + }) |
| 152 | + // Error loud here, so no try/catch |
| 153 | + console.log('Merged the pull request successfully') |
0 commit comments