# wei/pull syncs a fork by overwriting it, so know what you are installing

> wei/pull is a GitHub App that watches an upstream in your fork network and pushes the result into your fork as pull requests or hard resets. This looks at what each config key actually changes, what the default install does to local commits, and which parts of the rule format are still undefined.

**wei/pull** — 🤖 Keep your forks up-to-date via automated PRs

- Repository: https://github.com/wei/pull
- Website: https://github.com/apps/pull
- Stars: 7,234 · Forks: 795
- Language: TypeScript
- License: MIT
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/wei-pull

## Hard reset is the default, and it drops anything you committed locally

Install the Pull app on a fork with no configuration file and it does exactly one thing: it watches the upstream default master branch and hard resets your fork branch to match. That is the behavior set out under Basic Setup, and the default is repeated outside the user docs in the self hosting environment, where DEFAULT_MERGE_METHOD=hardreset sits in the Dockerfile ENV block. A hard reset is not a patch and not a merge. It points your branch at the upstream commit and discards whatever your branch held that upstream did not. The project puts one warning in the prerequisites, and it deserves a second read: make a backup if you have made changes. If your fork carries commits on top of upstream, or you rebased, amended, or cherry picked locally, the scheduled run does not negotiate any of it. It overwrites. The consequence for a reader is that the default install is only safe on a fork whose branches nobody edits by hand, which is the same property that makes it a good bot dependency and a bad habit for humans.

## Upstream has to sit in the same fork network, with one documented exception

The first prerequisite is structural rather than configurable: upstream must be in the same fork network. That rules out an unrelated repository, a colleague copy that happens to hold the same code, or a mirror you made yourself. The upstream field encodes this as owner:branch, and the sample rule writes `upstream: wei:master` with a comment telling you to change `wei` to the owner of the upstream repo. One documented wrinkle sits in the advanced example, where a dev rule uses `upstream: master` with no owner prefix and a comment saying it can be a branch in the same forked repo. So a rule is allowed to source from another branch of your own fork rather than from the parent project, which matters for repositories that carry generated or separately patched branches. What the app cannot do is bridge fork networks. If the code you want to track was never a GitHub fork relationship, or the fork was detached and recreated along the way, no rule in `.github/pull.yml` will make the service watch it, and the documentation gives you no fallback for that case.

## The config file has to live on your default branch, which forces a branch swap first

Adding a config file is not a matter of dropping `.github/pull.yml` onto master. The advanced setup is an ordered sequence with a dependency inside it: create a new branch, set that branch as the default branch under repository Settings > Branches, then add `.github/pull.yml` to your default branch, and only then install the app. The most common block is the shortest version of that file, and it behaves the same as the basic setup with no file at all.

```yaml
version: "1"
rules:
  - base: master
    upstream: wei:master # change `wei` to the owner of upstream repo
    mergeMethod: hardreset
    mergeUnstable: true
```

Each rule names a `base` branch as its target, and the file is read from the default branch rather than from the branch being synced. The practical consequence is that the branch holding your rules is also the branch the app updates, so configuration travels with the sync instead of sitting on a branch nobody touches. Because the branch switch in repository settings happens before the file exists, there is a window where the default branch has no config at all, and the docs do not say what the scheduler does with a repository in that state.

## mergeMethod accepts five values while the service default is hardreset

Every rule carries a mergeMethod, and the comment in the advanced block lists the entire vocabulary: none, merge, squash, rebase, hardreset, with none as the documented default. The catch is that the per rule default and the server default are different things. DEFAULT_MERGE_METHOD=hardreset in the Dockerfile and in .env.example governs the self hosted service, while a rule that omits mergeMethod resolves to none. Each value produces a different history from the same upstream feed: merge keeps a merge commit, squash collapses incoming commits into one, rebase replays them, hardreset throws the old commits away. That is four distinct commit graphs, and the choice is per rule rather than per repository, so a docs branch can hardreset while a dev branch squashes and a second rule feeds the same fork from an upstream of your own. What the format cannot express is conditional policy. A repository that wants squash on its stable branch and rebase on its work branches has to spell out both rules, because nothing in the rule reads branch state to choose a method.

## mergeUnstable is the only switch standing between a conflict and a pull request

mergeable_state is the field GitHub exposes on a pull request, and mergeUnstable reads it. Set to true, the service opens the pull request even when the mergeable state is not clean, which is the conflicted case. Left at false, the documented default, a conflicted sync produces no pull request at all. It is one flag, scoped to one rule, so a repository can push through conflicts on one branch and refuse them on another. The feature list also says Pull adds assignees and reviewers automatically and honors branch protection rules while working with pull request checks and reviews. Read together, those two capabilities pull in opposite directions. A branch protected against direct pushes will not take a hard reset, and a rule pairing hardreset with mergeUnstable true is asking for precisely that push. The upstream owner example does exactly this on both master and docs. What the service cannot do is report the refusal back to you in a way the docs describe. When mergeable_state is not clean and mergeUnstable is false, the absence of a pull request is the only signal you get.

## conflictReviewer shows up in the sample with no value and no explanation

Read the advanced usage block closely and the last keys of the dev rule are assignees, reviewers, and then a bare `conflictReviewer` with no value and no colon after it. The file hands you a key name and no contract. There is no comment saying what it holds, no note about whether it takes a list the way assignees and reviewers do, and no stated default anywhere in the documentation. It also carries no description of when it fires, which matters because the conflicted syncs it is named after are the same ones mergeUnstable false drops on the floor. If you are weighing whether to depend on it, the responsible reading is that it is unspecified. The rest of the rule set is documented tightly enough to configure without testing, and this key is the exception. The practical failure mode for a reader is quiet: a mistyped owner in the upstream field, a base branch that no longer exists, or an undefined key all produce a repository that looks configured but produces no pull requests, and none of those failures are described in the docs.

## Two web endpoints do all the work, and nothing is documented about rollback

The service exposes two browser endpoints doing opposite jobs. `https://pull.git.ci/check/${owner}/${repo}` validates a `.github/pull.yml` before you install the app, and `https://pull.git.ci/process/${owner}/${repo}` triggers a run by hand. That is the whole documented control surface. What happens after a bad sync is not covered anywhere. A hard reset that overwrote a branch can only be walked back with git if you made the backup the prerequisites ask for, and the docs say nothing about how a failed or partially applied sync is surfaced, whether a completed reset is recorded anywhere, or how you would tell a run that silently skipped from one that succeeded. The worker side hints at the scale of the problem without answering it. The compose file runs three worker replicas and ships a full sync task, `docker exec -it pull-app-1 deno task full-sync`, next to a startup variant, `deno task dev:skip-full-sync`, that skips full sync on boot. Those tasks exist because state lives in MongoDB and Redis, and neither store holds a copy of your git history.

## Self hosting means Deno on Alpine plus MongoDB 8, Redis 7.4, and a worker pool

Installing the hosted app is one click at the GitHub Apps page. Running the service yourself pulls in considerably more. The compose file defines four services: an app exposed on port 3000 running `deno task dev`, a worker service running `deno task worker` with three replicas, `mongo:8` with root credentials taken from MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD, and `redis:7.4`. The image is Deno on Alpine built from ARG DENO_VERSION=2.3.7. To start the stack and scale the worker without swarm mode:

```bash
docker compose up -d --scale worker=3
```

Configuration lives in a `.env` file that the compose app service loads through env_file. The required keys are the GitHub App identity and the two datastores: APP_ID, APP_NAME, APP_SLUG, PRIVATE_KEY, WEBHOOK_SECRET, MONGODB_URL, and REDIS_URL. Optional keys include WEBHOOK_PROXY_URL, described as a smee.io tunnel for receiving webhook events in local development, plus LOG_LEVEL, SENTRY_DSN, and PORT. The repository also ships .devcontainer/, .hooks/, deno.lock, and a static/ directory, and the compose file keeps commented mongo-express, redis-commander, and bullboard blocks for inspecting state. One versioning detail matters before you commit to this: the newest releases are v2.0.0-alpha.2, v2.0.0-alpha.3, and v2.0.0-alpha.4, so v2 has not left alpha, and the last push to the repository was April 13, 2026.

## Conclusion

wei/pull fits workflows that treat a fork as a read-only mirror of upstream, and it fits badly anywhere a fork is a branch you edit by hand, because the default install hard resets your branches and the docs offer no rollback path. Before installing, confirm your upstream is still in the same fork network, read the mergeMethod and mergeUnstable values you are actually relying on, and treat conflictReviewer as undefined until someone documents it. The v2 line is still shipping alpha releases and the last push to the repository was April 13, 2026, so a self-hosted instance is yours to keep running.

## FAQ

### Do I need a .github/pull.yml file for wei/pull to work?

No. Installing the Pull app with no configuration watches the upstream default master branch and hard resets your branch to it on a schedule. The file is only needed when you want multiple rules, a merge method other than hardreset, assignees and reviewers, or more than one upstream branch.

### Can wei/pull sync a fork whose upstream is not in the fork network?

No. The prerequisite is that upstream must be in the same fork network, so unrelated repositories cannot be tracked. A rule can also source from another branch of your own fork when the upstream value omits the owner prefix, as in the dev rule of the advanced example.

### How do I self host wei/pull instead of installing the hosted app?

Copy .env.example to .env, fill in APP_ID, APP_NAME, APP_SLUG, PRIVATE_KEY, WEBHOOK_SECRET, MONGODB_URL and REDIS_URL, then bring the stack up with docker compose. The app listens on port 3000, workers run as separate replicas, and the full sync task can be triggered with deno task full-sync.

### What happens when a wei/pull sync hits a conflict?

With mergeUnstable at its default of false, no pull request is opened at all. Setting mergeUnstable to true in a rule makes the service open the pull request even when the mergeable_state is not clean, which is what the hardreset examples in the docs rely on.

### Which merge methods does wei/pull support, and which is the default?

The mergeMethod field accepts none, merge, squash, rebase, and hardreset, and it defaults to none for a rule that omits it. Separately, DEFAULT_MERGE_METHOD=hardreset in the Dockerfile and .env.example sets the service level default used when no rule specifies one.

## Sources

- [License: MIT](https://github.com/wei/pull/blob/master/LICENSE)
- [Project website](https://github.com/apps/pull)
- [README](https://github.com/wei/pull/blob/master/README.md)
- [Releases](https://github.com/wei/pull/releases)
- [wei/pull on GitHub](https://github.com/wei/pull)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/wei-pull
