Skip to content

ci: sync next with main automatically #1

ci: sync next with main automatically

ci: sync next with main automatically #1

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.

Check failure on line 198 in .github/workflows/sync-next-with-main.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/sync-next-with-main.yml

Invalid workflow file

You have an error in your yaml syntax on line 198
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.')
}