Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 43 additions & 34 deletions .agents/skills/merge/resolve-conflicts.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,70 @@
# Resolve Merge Conflicts
# Verify Merge Conflict Resolutions

Resolve merge conflicts from merging `main` into the `next` branch.
Verify that the automated conflict resolution (which kept the "ours"/next side for JSON and YAML files) was correct, and fix any issues.

**SCOPE: Do not spawn tasks/sub-agents.**

## Prerequisites

- **`prNumber`** — The PR number for the merge PR.
- **`branch`** — The branch name (e.g., `ci/merge-main-to-next`).
- The working directory is the repo root, checked out on the merge branch with conflict markers present.
- The working directory is the repo root, checked out on the merge branch.
- Dependencies are installed and packages are built.
- Conflict markers in JSON/YAML files have already been stripped by the GitHub Action, keeping the "ours" (next) side. Source code files (`.ts`, `.js`, `.md`, `.astro`) may still have conflict markers.

## Strategy
## Background

When resolving conflicts, follow these rules based on file type:
The GitHub Action strips conflict markers from JSON/YAML files before `pnpm install` can run, always keeping the `next` branch side. This is usually correct (next has pre-release versions that must be preserved), but it may discard important changes from `main` — like new dependencies, updated dependency versions, or config changes from bug fixes.

### Changeset files (`.changeset/*.md`)
## Steps

- If a changeset file exists on `main` but not on `next`, it was likely already released. **Delete it.** The clean-changesets skill will handle this more thoroughly, but removing obvious ones here unblocks the merge.
- If both branches modified the same changeset, prefer the `next` version since it's the one targeting the upcoming major.
### Step 1: Resolve any remaining conflict markers

### `package.json` and version files
Check for conflict markers in source code files:

- **Prefer `next` branch versions.** The `next` branch has the pre-release versions (alpha/beta) which should not be overwritten by `main`'s stable versions.
- For dependency version updates from `main`, accept those — they are typically patches/minors that should be forward-ported.
```bash
grep -r "<<<<<<< " --include="*.ts" --include="*.js" --include="*.mjs" --include="*.cjs" --include="*.md" --include="*.astro" . 2>/dev/null | grep -v node_modules
```

### `pnpm-lock.yaml`
If any exist, resolve them following these rules:
- **Prefer `main` for bug fixes.** If `main` fixed a bug, that fix should carry over to `next`.
- **Prefer `next` for API changes.** If `next` changed an API, keep the `next` version and adapt the `main` fix if needed.
- When in doubt, prefer `next` — it's the forward-looking branch.

- Do not try to manually resolve lockfile conflicts. Instead:
1. Accept the `next` version: `git checkout --theirs pnpm-lock.yaml`
2. The lockfile will be regenerated after `pnpm install` later.
After resolving, `git add` each file.

### Source code (`packages/**/*.ts`, `packages/**/*.js`)
### Step 2: Review what was lost from main

- **Prefer `main` for bug fixes.** If `main` fixed a bug, that fix should carry over to `next`.
- **Prefer `next` for API changes.** If `next` changed an API (new major version features), keep the `next` version and adapt the `main` fix to work with it if needed.
- When in doubt, prefer `next` — it's the forward-looking branch.
Compare what `main` had in the conflicted files against what we kept:

### Test files
```bash
# List files that were modified on both branches (potential conflict sites)
git diff --name-only origin/next...origin/main -- '*.json' '*.yaml' '*.yml'
```

- If tests conflict, prefer `next` and adapt. Tests on `next` may test new major-version behavior.
For each file, compare the `main` version to the current version:

### Configuration files (`.changeset/config.json`, `tsconfig.json`, etc.)
```bash
git diff origin/main -- <file>
```

- Prefer `next` unless `main` has a clear fix that should be forward-ported.
Look for:
- **New dependencies** added on `main` that are missing from `next` — these should be added
- **Dependency version bumps** on `main` that are higher than what's on `next` — these should typically be accepted (they're patches/minors)
- **New scripts or config entries** added on `main` — these should be forward-ported
- **Version fields** (`"version":`) — keep `next`'s pre-release versions, do NOT use `main`'s stable versions

## Steps
### Step 3: Apply corrections

If you find changes from `main` that should have been kept:
1. Apply those specific changes to the current files
2. Run `pnpm install --no-frozen-lockfile` if you modified any `package.json` files
3. `git add` the modified files

### Step 4: Do NOT commit

1. Run `git diff --name-only --diff-filter=U` to list all conflicted files.
2. For each conflicted file, read the file and resolve the conflict following the strategy above.
3. After resolving each file, run `git add <file>` to mark it as resolved.
4. After all files are resolved, verify no conflict markers remain: `grep -r "<<<<<<< " --include="*.ts" --include="*.js" --include="*.json" --include="*.yaml" --include="*.yml" --include="*.md" .`
5. After conflicts are resolved, regenerate the lockfile and install dependencies:
```bash
pnpm install --no-frozen-lockfile
```
6. Do NOT commit — the orchestrator will handle committing.
The orchestrator will handle committing.

## Output

Return the list of files that were resolved and whether all conflicts were successfully handled.
Return whether the conflict resolutions were correct and what files (if any) needed corrections.
64 changes: 28 additions & 36 deletions .flue/workflows/merge-fix/WORKFLOW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,25 @@ export const args = v.object({
export default async function mergeFix(flue: FlueClient, { prNumber }: v.InferOutput<typeof args>) {
const branch = 'ci/merge-main-to-next';

// Step 1: Check for merge conflicts (unresolved conflict markers in files)
const conflictCheck = await flue.shell(
'git diff --check HEAD 2>&1 || grep -r "<<<<<<< " --include="*.ts" --include="*.js" --include="*.json" --include="*.yaml" --include="*.yml" --include="*.md" --include="*.mjs" --include="*.cjs" . 2>/dev/null | head -20',
);
const hasConflicts = conflictCheck.stdout.includes('<<<<<<<');

// Step 2: Resolve conflicts if any
if (hasConflicts) {
await flue.skill('merge/resolve-conflicts.md', {
args: { prNumber, branch },
result: v.object({
resolved: v.pipe(
v.boolean(),
v.description('true if all merge conflicts were resolved successfully'),
),
filesResolved: v.pipe(
v.array(v.string()),
v.description('List of files where conflicts were resolved'),
),
}),
});
}

// Step 3: Ensure dependencies are installed
// If conflicts were resolved, the resolve-conflicts skill already ran pnpm install.
// If not, we still need to install deps before proceeding.
if (!hasConflicts) {
await flue.shell('pnpm install --no-frozen-lockfile');
}
// Step 1: Verify conflict resolutions and resolve any remaining source code conflicts.
// JSON/YAML conflicts were pre-stripped by the GitHub Action (keeping next side).
// This skill checks that nothing important from main was lost, and resolves
// any remaining conflict markers in .ts/.js/.md/.astro files.
const verifyResult = await flue.skill('merge/resolve-conflicts.md', {
args: { prNumber, branch },
result: v.object({
correct: v.pipe(
v.boolean(),
v.description('true if all conflict resolutions were correct or have been fixed'),
),
correctedFiles: v.pipe(
v.array(v.string()),
v.description('List of files that needed corrections after verification'),
),
}),
});

// Step 4: Remove stale changesets that were already released on main
// Step 2: Remove stale changesets that were already released on main
await flue.skill('merge/clean-changesets.md', {
args: { prNumber },
result: v.object({
Expand All @@ -67,7 +55,7 @@ export default async function mergeFix(flue: FlueClient, { prNumber }: v.InferOu
}),
});

// Step 5: Run tests and fix failures
// Step 3: Run tests and fix failures
const fixResult = await flue.skill('merge/fix-tests.md', {
args: { prNumber },
result: v.object({
Expand All @@ -85,13 +73,13 @@ export default async function mergeFix(flue: FlueClient, { prNumber }: v.InferOu
}),
});

// Step 6: Commit and push all changes
// Step 4: Commit and push all changes
const status = await flue.shell('git status --porcelain');
if (status.stdout.trim()) {
await flue.shell('git add -A');

const commitParts = [];
if (hasConflicts) commitParts.push('resolve merge conflicts');
if (verifyResult.correctedFiles.length > 0) commitParts.push('fix merge conflict resolutions');
if (fixResult.fixedFiles.length > 0) commitParts.push('fix test failures');
const commitMsg =
commitParts.length > 0
Expand All @@ -107,9 +95,13 @@ export default async function mergeFix(flue: FlueClient, { prNumber }: v.InferOu
}
}

// Step 7: Post a summary comment on the PR
// Step 5: Post a summary comment on the PR
const summaryParts = [];
if (hasConflicts) summaryParts.push('- Resolved merge conflicts');
if (verifyResult.correctedFiles.length > 0) {
summaryParts.push(
`- Fixed conflict resolutions in: ${verifyResult.correctedFiles.join(', ')}`,
);
}
if (fixResult.fixedFiles.length > 0) {
summaryParts.push(`- Fixed test failures in: ${fixResult.fixedFiles.join(', ')}`);
}
Expand Down Expand Up @@ -141,7 +133,7 @@ ${fixResult.testsPass ? 'All tests pass — this PR should be ready for review.'
return {
pushed: true,
testsPass: fixResult.testsPass,
hasConflicts,
correctedFiles: verifyResult.correctedFiles,
remainingFailures: fixResult.remainingFailures,
};
}
45 changes: 37 additions & 8 deletions .github/workflows/merge-fix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ on:

env:
IMAGE: ghcr.io/${{ github.repository }}/flue-sandbox
PNPM_STORE_DIR: .pnpm-store

concurrency:
group: merge-fix
Expand Down Expand Up @@ -91,6 +90,37 @@ jobs:
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Strip conflict markers from JSON/YAML files
if: steps.pr.outputs.number != '' && steps.attempts.outputs.skip != 'true'
# Merge conflicts in package.json files make pnpm install impossible.
# Strip conflict markers (keeping "ours"/next side) so pnpm can parse
# the workspace. The Flue verify skill will check via git diff whether
# anything important from main was lost and patch it back in.
run: |
node -e '
const fs = require("fs");
const path = require("path");
let count = 0;
function fix(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
fix(full);
} else if (/\.(json|yaml|yml)$/.test(entry.name)) {
let c = fs.readFileSync(full, "utf8");
if (c.includes("<<<<<<<")) {
c = c.replace(/^<{7}.*\n([\s\S]*?)^={7}\n[\s\S]*?^>{7}.*\n/gm, "$1");
fs.writeFileSync(full, c);
console.log("Stripped conflicts:", full);
count++;
}
}
}
}
fix(".");
console.log(count + " file(s) fixed");
'

- name: Setup PNPM
if: steps.pr.outputs.number != '' && steps.attempts.outputs.skip != 'true'
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
Expand All @@ -102,14 +132,13 @@ jobs:
node-version: 24.15.0
cache: pnpm

- name: Install root deps only
- name: Install deps
if: steps.pr.outputs.number != '' && steps.attempts.outputs.skip != 'true'
run: pnpm install --no-frozen-lockfile

- name: Build
if: steps.pr.outputs.number != '' && steps.attempts.outputs.skip != 'true'
# Only install root workspace deps (where @flue/cli lives).
# Cannot install the full monorepo here because merge conflicts
# in package.json files would break pnpm install.
# Full install and build happen inside the Flue sandbox after
# conflicts are resolved.
run: pnpm install --no-frozen-lockfile --filter .
run: pnpm build

- name: Log in to GHCR
if: steps.pr.outputs.number != '' && steps.attempts.outputs.skip != 'true'
Expand Down
Loading