Repository navigation
ci: sync next with main automatically #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Sync Next With Main | ||
| on: | ||
| push: | ||
| branches: | ||
| - next | ||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| issues: write | ||
| jobs: | ||
| merge-main-into-next: | ||
| name: Merge main into next | ||
| if: > | ||
| github.event.sender.login != 'prisma-bot' && | ||
| github.event.sender.login != 'prisma-bot[bot]' && | ||
| github.event.sender.login != 'prismabots' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| ref: next | ||
| fetch-depth: 0 | ||
| - name: Identify triggering pull request | ||
| id: pr | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const { owner, repo } = context.repo | ||
| const commitSha = context.sha | ||
| const { data } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ | ||
| owner, | ||
| repo, | ||
| commit_sha: commitSha, | ||
| }) | ||
| if (!data.length) { | ||
| core.info(`No pull request found for commit ${commitSha}`) | ||
| return | ||
| } | ||
| const pr = data[0] | ||
| core.setOutput('number', pr.number.toString()) | ||
| core.setOutput('url', pr.html_url) | ||
| core.setOutput('title', pr.title) | ||
| - name: Configure git | ||
| run: | | ||
| git config user.email "prismabots@gmail.com" | ||
| git config user.name "prisma-bot" | ||
| git fetch origin main | ||
| - name: Check if sync is required | ||
| id: divergence | ||
| run: | | ||
| if git merge-base --is-ancestor origin/main HEAD; then | ||
| echo "needs=false" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "needs=true" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| - name: Merge main (no commit) | ||
| id: merge | ||
| if: steps.divergence.outputs.needs == 'true' | ||
| run: | | ||
| set -o pipefail | ||
| if git merge --no-ff --no-commit origin/main; then | ||
| echo "status=clean" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "status=conflict" >> "$GITHUB_OUTPUT" | ||
| conflicts=$(git diff --name-only --diff-filter=U) | ||
| if [ -n "$conflicts" ]; then | ||
| { | ||
| echo 'conflicts<<EOF' | ||
| echo "$conflicts" | ||
| echo 'EOF' | ||
| } >> "$GITHUB_OUTPUT" | ||
| fi | ||
| fi | ||
| - name: Analyse merge conflicts | ||
| id: conflict_analysis | ||
| if: steps.merge.outputs.status == 'conflict' | ||
| run: | | ||
| set -eo pipefail | ||
| conflicts="${{ steps.merge.outputs.conflicts }}" | ||
| if [ -z "$conflicts" ]; then | ||
| echo "analysis_path=" >> "$GITHUB_OUTPUT" | ||
| echo "auto=false" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| printf '%s\n' "$conflicts" > conflicts.txt | ||
| mapfile -t files < conflicts.txt | ||
| node .github/scripts/check-engines-conflicts.js "${files[@]}" > conflict-analysis.json | ||
| AUTO=$(node -e "const fs=require('fs');const data=JSON.parse(fs.readFileSync('conflict-analysis.json','utf8'));console.log(data.autoResolvable ? 'true' : 'false');") | ||
| echo "analysis_path=conflict-analysis.json" >> "$GITHUB_OUTPUT" | ||
| echo "auto=$AUTO" >> "$GITHUB_OUTPUT" | ||
| - name: Auto resolve engines conflicts | ||
| if: steps.conflict_analysis.outputs.auto == 'true' | ||
| run: | | ||
| set -eo pipefail | ||
| conflicts="${{ steps.merge.outputs.conflicts }}" | ||
| printf '%s\n' "$conflicts" > conflicts.txt | ||
| declare -a lockfiles=() | ||
| while IFS= read -r file; do | ||
| [ -z "$file" ] && continue | ||
| if [[ "$file" == *"package.json" ]]; then | ||
| git checkout --ours -- "$file" | ||
| git add "$file" | ||
| elif [[ "$file" == *"pnpm-lock.yaml" ]]; then | ||
| git checkout --theirs -- "$file" | ||
| lockfiles+=("$file") | ||
| fi | ||
| done < conflicts.txt | ||
| if [ ${#lockfiles[@]} -gt 0 ]; then | ||
| corepack enable | ||
| pnpm install --lockfile-only --ignore-scripts | ||
| for file in "${lockfiles[@]}"; do | ||
| git add "$file" | ||
| done | ||
| fi | ||
| - name: Abort merge (manual resolution required) | ||
| if: steps.merge.outputs.status == 'conflict' && steps.conflict_analysis.outputs.auto != 'true' | ||
| run: git merge --abort | ||
| - name: Create merge commit | ||
| id: commit | ||
| if: steps.divergence.outputs.needs == 'true' && (steps.merge.outputs.status == 'clean' || steps.conflict_analysis.outputs.auto == 'true') | ||
| run: | | ||
| set -eo pipefail | ||
| git status | ||
| if git diff --name-only --diff-filter=U | grep -q .; then | ||
| echo "Merge still has unresolved conflicts" | ||
| exit 1 | ||
| fi | ||
| if git diff --cached --quiet; then | ||
| echo "No staged changes after merge attempt, nothing to commit." | ||
| exit 0 | ||
| fi | ||
| if [ -n "${{ steps.pr.outputs.number }}" ]; then | ||
| msg="chore: merge main into next (#${{ steps.pr.outputs.number }})" | ||
| else | ||
| msg="chore: merge main into next" | ||
| fi | ||
| git commit -m "$msg" | ||
| echo "commit_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | ||
| - name: Push merge commit | ||
| if: steps.commit.outputs.commit_sha != '' | ||
| env: | ||
| PUSH_TOKEN: ${{ secrets.BOT_TOKEN != '' && secrets.BOT_TOKEN || github.token }} | ||
| run: | | ||
| git remote set-url origin "https://x-access-token:${PUSH_TOKEN}@github.com/${{ github.repository }}.git" | ||
| git push origin HEAD:next | ||
| - name: Comment on unresolved conflicts | ||
| if: steps.merge.outputs.status == 'conflict' && steps.conflict_analysis.outputs.auto != 'true' && steps.pr.outputs.number != '' | ||
| uses: actions/github-script@v7 | ||
| env: | ||
| ANALYSIS_PATH: ${{ steps.conflict_analysis.outputs.analysis_path }} | ||
| MERGE_TRIGGER_SHA: ${{ github.sha }} | ||
| with: | ||
| script: | | ||
| const marker = `<!-- sync-next-conflict:${process.env.MERGE_TRIGGER_SHA} -->` | ||
| const { owner, repo } = context.repo | ||
| const issue_number = Number.parseInt('${{ steps.pr.outputs.number }}', 10) | ||
| let files = [] | ||
| if (process.env.ANALYSIS_PATH) { | ||
| const fs = require('fs') | ||
| const data = JSON.parse(fs.readFileSync(process.env.ANALYSIS_PATH, 'utf8')) | ||
| files = data.files || [] | ||
| } | ||
| const lines = files.length | ||
| ? files.map((file) => { | ||
| const details = file.diffPackages?.length | ||
| ? ` (${file.diffPackages.join(', ')})` | ||
| : '' | ||
| return `- \`${file.path}\`${details}` | ||
| }) | ||
| : ['- (no detailed conflict data available)'] | ||
| const body = `${marker} | ||
| Hi there! I tried to merge \`main\` into \`next\` automatically after this PR was merged, but Git reported conflicts. | ||
| Conflicting files: | ||
| ${lines.join('\n')} | ||
| Please pull the latest \`next\`, merge \`main\` locally, resolve the conflicts, and push the updated branch. Ping us if you need a hand!` | ||
| const { data: comments } = await github.rest.issues.listComments({ | ||
| owner, | ||
| repo, | ||
| issue_number, | ||
| per_page: 100, | ||
| }) | ||
| if (!comments.some((comment) => comment.body?.includes(marker))) { | ||
| await github.rest.issues.createComment({ | ||
| owner, | ||
| repo, | ||
| issue_number, | ||
| body, | ||
| }) | ||
| } else { | ||
| core.info('Conflict comment already exists, skipping.') | ||
| } | ||
| - name: Comment on auto-resolved engines conflicts | ||
| if: steps.commit.outputs.commit_sha != '' && steps.conflict_analysis.outputs.auto == 'true' && steps.pr.outputs.number != '' | ||
| uses: actions/github-script@v7 | ||
| env: | ||
| ANALYSIS_PATH: ${{ steps.conflict_analysis.outputs.analysis_path }} | ||
| MERGE_TRIGGER_SHA: ${{ github.sha }} | ||
| MERGE_COMMIT_SHA: ${{ steps.commit.outputs.commit_sha }} | ||
| with: | ||
| script: | | ||
| const marker = `<!-- sync-next-auto:${process.env.MERGE_TRIGGER_SHA} -->` | ||
| const { owner, repo } = context.repo | ||
| const issue_number = Number.parseInt('${{ steps.pr.outputs.number }}', 10) | ||
| const fs = require('fs') | ||
| let files = [] | ||
| if (process.env.ANALYSIS_PATH) { | ||
| const data = JSON.parse(fs.readFileSync(process.env.ANALYSIS_PATH, 'utf8')) | ||
| files = data.files || [] | ||
| } | ||
| const lines = files | ||
| .filter((file) => (file.diffPackages || []).length > 0) | ||
| .map((file) => `- \`${file.path}\` (${file.diffPackages.join(', ')})`) | ||
| const link = `https://github.com/${owner}/${repo}/commit/${process.env.MERGE_COMMIT_SHA}` | ||
| const conflicts = lines.length ? lines.join('\n') : '- package.json / pnpm-lock.yaml' | ||
| const body = `${marker} | ||
| Heads up! We merged \`main\` into \`next\` automatically after this PR, kept the \`next\` dependency versions, and regenerated the lockfile from \`main\` via \`pnpm install --lockfile-only --ignore-scripts\`. | ||
| Resolved conflicts: | ||
| ${conflicts} | ||
| Merge commit: ${link}` | ||
| const { data: comments } = await github.rest.issues.listComments({ | ||
| owner, | ||
| repo, | ||
| issue_number, | ||
| per_page: 100, | ||
| }) | ||
| if (!comments.some((comment) => comment.body?.includes(marker))) { | ||
| await github.rest.issues.createComment({ | ||
| owner, | ||
| repo, | ||
| issue_number, | ||
| body, | ||
| }) | ||
| } else { | ||
| core.info('Auto-resolution comment already exists, skipping.') | ||
| } | ||
| - name: Wait for merge commit checks | ||
| id: wait | ||
| if: steps.commit.outputs.commit_sha != '' | ||
| env: | ||
| COMMIT_SHA: ${{ steps.commit.outputs.commit_sha }} | ||
| GITHUB_OWNER: ${{ github.repository_owner }} | ||
| GITHUB_REPO: ${{ github.event.repository.name }} | ||
| GITHUB_TOKEN: ${{ secrets.BOT_TOKEN != '' && secrets.BOT_TOKEN || github.token }} | ||
| TIMEOUT_MINUTES: '90' | ||
| run: | | ||
| node .github/scripts/wait-for-commit-checks.js > check-result.json | ||
| status=$(node -e "const fs=require('fs');const data=JSON.parse(fs.readFileSync('check-result.json','utf8'));console.log(data.status);") | ||
| echo "status=$status" >> "$GITHUB_OUTPUT" | ||
| if [ "$status" = "failure" ]; then | ||
| echo "failure_path=check-result.json" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| - name: Comment on failing checks | ||
| if: steps.wait.outputs.status == 'failure' && steps.pr.outputs.number != '' | ||
| uses: actions/github-script@v7 | ||
| env: | ||
| FAILURE_PATH: ${{ steps.wait.outputs.failure_path }} | ||
| MERGE_COMMIT_SHA: ${{ steps.commit.outputs.commit_sha }} | ||
| MERGE_TRIGGER_SHA: ${{ github.sha }} | ||
| with: | ||
| script: | | ||
| const marker = `<!-- sync-next-checks:${process.env.MERGE_COMMIT_SHA} -->` | ||
| const { owner, repo } = context.repo | ||
| const issue_number = Number.parseInt('${{ steps.pr.outputs.number }}', 10) | ||
| const fs = require('fs') | ||
| let failures = [] | ||
| if (process.env.FAILURE_PATH) { | ||
| const data = JSON.parse(fs.readFileSync(process.env.FAILURE_PATH, 'utf8')) | ||
| failures = data.failures || [] | ||
| } | ||
| if (!failures.length) { | ||
| core.info('No failing checks to report.') | ||
| return | ||
| } | ||
| const lines = failures.map( | ||
| (failure) => | ||
| `- [${failure.name}](${failure.url}) — ${failure.conclusion}`, | ||
| ) | ||
| const body = `${marker} | ||
| The automatic merge of \`main\` into \`next\` succeeded, but some checks failed on the merge commit (${process.env.MERGE_COMMIT_SHA}): | ||
| ${lines.join('\n')} | ||
| Please investigate and fix the failing workflow(s).` | ||
| const { data: comments } = await github.rest.issues.listComments({ | ||
| owner, | ||
| repo, | ||
| issue_number, | ||
| per_page: 100, | ||
| }) | ||
| if (!comments.some((comment) => comment.body?.includes(marker))) { | ||
| await github.rest.issues.createComment({ | ||
| owner, | ||
| repo, | ||
| issue_number, | ||
| body, | ||
| }) | ||
| } else { | ||
| core.info('Check-failure comment already exists, skipping.') | ||
| } | ||