Full Code of wei/pull for AI

master 197574c0f0d1 cached
41 files
65.3 KB
17.9k tokens
36 symbols
1 requests
Download .txt
Repository: wei/pull
Branch: master
Commit: 197574c0f0d1
Files: 41
Total size: 65.3 KB

Directory structure:
gitextract_6sspy7v9/

├── .devcontainer/
│   ├── Dockerfile
│   ├── pull-base/
│   │   ├── devcontainer.json
│   │   └── docker-compose.yml
│   └── pull-full/
│       ├── devcontainer.json
│       └── docker-compose.yml
├── .dockerignore
├── .github/
│   ├── CODE_OF_CONDUCT.md
│   ├── CONTRIBUTING.md
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   ├── pull.yml
│   └── workflows/
│       ├── auto-tag.yml
│       └── publish.yml
├── .gitignore
├── .hooks/
│   └── pre-commit
├── Dockerfile
├── LICENSE
├── README.md
├── _config.yml
├── deno.json
├── docker-compose.yml
├── scripts/
│   ├── full-sync.ts
│   └── manual-process.ts
├── src/
│   ├── app.ts
│   ├── configs/
│   │   ├── app-config.ts
│   │   ├── database.ts
│   │   └── redis.ts
│   ├── index.ts
│   ├── processor/
│   │   ├── index.ts
│   │   └── pull.ts
│   ├── router/
│   │   ├── index.ts
│   │   ├── repo-handler.ts
│   │   └── stats.ts
│   ├── utils/
│   │   ├── get-pull-config.ts
│   │   ├── get-repository-schedule.ts
│   │   ├── helpers.ts
│   │   ├── logger.ts
│   │   ├── schema.test.ts
│   │   └── schema.ts
│   └── worker.ts
└── static/
    └── hello.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .devcontainer/Dockerfile
================================================
ARG VARIANT=ubuntu
ARG DENO_VERSION=2.3.7

FROM denoland/deno:bin-${DENO_VERSION} AS deno
FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT}

COPY --from=deno /deno /usr/local/bin/deno


================================================
FILE: .devcontainer/pull-base/devcontainer.json
================================================
{
  "name": "[Pull App] Base Dev Environment",
  "dockerComposeFile": [
    "docker-compose.yml"
  ],
  "service": "dev",
  "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",

  "customizations": {
    "vscode": {
      "extensions": [
        "github.copilot",
        "denoland.vscode-deno",
        "mongodb.mongodb-vscode"
      ]
    }
  },

  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {
      "installDirectlyFromGitHubRelease": true,
      "version": "latest"
    },
    "ghcr.io/jckimble/devcontainer-features/ngrok:3": {
      "version": "stable"
    }
  },

  "forwardPorts": [
    "app:3000",
    "mongodb:27017",
    "redis:6379"
  ],

  "postStartCommand": {
    "git-config": "git config --global --add safe.directory /workspaces/${localWorkspaceFolderBasename}; git config --global core.autocrlf true; git config --global core.editor nano",
    "deno-hooks": "deno task install-hooks"
  }
}


================================================
FILE: .devcontainer/pull-base/docker-compose.yml
================================================
version: "3.8"

services:
  dev:
    build:
      context: ..
      dockerfile: Dockerfile
    volumes:
      - ../../..:/workspaces:cached

    # Overrides default command so things don't shut down after the process ends.
    command: sleep infinity

    # Create a network and connect the app container to it
    networks:
      - devcontainer-network

  mongodb:
    image: mongo:8
    restart: unless-stopped
    volumes:
      - mongodb-data:/data/db
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: mongodb_password
      # Connect using mongodb://root:mongodb_password@mongodb:27017/

    # Add "forwardPorts": ["mongodb:27017"] to **devcontainer.json** to forward MongoDB locally.
    # (Adding the "ports" property to this file will not forward from a Codespace.)
    networks:
      - devcontainer-network

  redis:
    image: redis:7.4
    restart: unless-stopped
    volumes:
      - redis-data:/data

    # Add "forwardPorts": ["redis:6379"] to **devcontainer.json** to forward Redis locally.
    # (Adding the "ports" property to this file will not forward from a Codespace.)
    networks:
      - devcontainer-network

networks:
  devcontainer-network:

volumes:
  mongodb-data:
  redis-data:


================================================
FILE: .devcontainer/pull-full/devcontainer.json
================================================
{
  "name": "[Pull App] Full Dev Environment (amd64)",
  "dockerComposeFile": [
    "../pull-base/docker-compose.yml",
    "docker-compose.yml"
  ],
  "service": "dev",
  "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",

  "customizations": {
    "vscode": {
      "extensions": [
        "github.copilot",
        "denoland.vscode-deno",
        "mongodb.mongodb-vscode"
      ]
    }
  },

  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {
      "installDirectlyFromGitHubRelease": true,
      "version": "latest"
    },
    "ghcr.io/jckimble/devcontainer-features/ngrok:3": {
      "version": "stable"
    }
  },

  "forwardPorts": [
    "app:3000",
    "mongodb:27017",
    "redis:6379",
    "mongo-express:8081",
    "redis-commander:8081",
    "bullboard:8083"
  ],

  "postStartCommand": {
    "git-config": "git config --global --add safe.directory /workspaces/${localWorkspaceFolderBasename}; git config --global core.autocrlf true; git config --global core.editor nano",
    "deno-hooks": "deno task install-hooks"
  }
}


================================================
FILE: .devcontainer/pull-full/docker-compose.yml
================================================
version: "3.8"

services:
  mongo-express:
    image: mongo-express
    restart: unless-stopped
    environment:
      ME_CONFIG_MONGODB_URL: mongodb://root:mongodb_password@mongodb:27017/
      ME_CONFIG_BASICAUTH: false

    # Add "forwardPorts": ["mongo-express:8081"] to **devcontainer.json** to forward Mongo-express locally.
    # (Adding the "ports" property to this file will not forward from a Codespace.)
    networks:
      - devcontainer-network

  redis-commander:
    image: rediscommander/redis-commander:latest
    restart: unless-stopped
    environment:
      REDIS_HOSTS: redis

    # Add "forwardPorts": ["redis-commander:8081"] to **devcontainer.json** to forward Redis-commander locally.
    # (Adding the "ports" property to this file will not forward from a Codespace.)
    networks:
      - devcontainer-network

  bullboard:
    image: addono/bull-board:latest
    restart: unless-stopped
    environment:
      REDIS_HOST: redis
      PORT: 8083

    # Add "forwardPorts": ["redis-commander:8081"] to **devcontainer.json** to forward Redis-commander locally.
    # (Adding the "ports" property to this file will not forward from a Codespace.)
    networks:
      - devcontainer-network


================================================
FILE: .dockerignore
================================================
node_modules
coverage
npm-debug.log
*.pem
.env
.*
docker-compose.yml
Dockerfile
*.md
.gitignore
.dockerignore


================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of
experience, nationality, personal appearance, race, religion, or sexual identity
and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

- The use of sexualized language or imagery and unwelcome sexual attention or
  advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
  address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening,
offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at github@weispot.com. The project team
will review and investigate all complaints, and will respond in a way that it
deems appropriate to the circumstances. The project team is obligated to
maintain confidentiality with regard to the reporter of an incident. Further
details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 1.4, available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: https://www.contributor-covenant.org
[version]: https://www.contributor-covenant.org/version/1/4/


================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing to Pull

Hi there! We're thrilled that you'd like to contribute to this project. Your
help is essential for keeping it great.

Please note that this project is released with a
[Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this
project you agree to abide by its terms.

## Development Environment

### Prerequisites

- [Deno](https://deno.land/#installation) 2
- MongoDB 8.0
- Redis 7.4

### Development Containers

We provide two development environments:

1. Base Environment:

```bash
devcontainer open .devcontainer/pull-base
```

2. Full Environment (includes monitoring tools which supports amd64 only):

```bash
devcontainer open .devcontainer/pull-full
```

The full environment includes additional tools:

- Mongo Express (port 8081)
- Redis Commander (port 8082)
- Bull Board (port 8083)

## Getting Started

1. Fork and clone the repository
2. Make sure you have [Deno](https://deno.land/#installation) installed
3. Install development hooks:
   ```bash
   deno task install-hooks
   ```
4. Set up your GitHub App. Follow the
   [Probot documentation](https://probot.github.io/docs/development/) for
   detailed instructions on creating a GitHub App.
5. Set up your environment variables by copying the `.env.example` file to
   `.env` and filling in the required values:
   ```sh
   cp .env.example .env
   ```
6. Start the development server:
   ```bash
   deno task dev
   ```
   Start the development worker:
   ```bash
   deno task worker
   ```

## Available Scripts

- `deno task dev`: Start the app in development mode
- `deno task dev:skip-full-sync`: Start without initial full sync
- `deno task worker`: Start the background worker
- `deno task test`: Run tests
- `deno task check`: Run linter and formatter checks
- `deno task full-sync`: Run full repository sync
- `deno task manual-process <owner>/<repo>`: Process specific repository

## Application Architecture

### Core Components

#### 1. Web Server (`src/index.ts`)

The main Express.js application that:

- Handles GitHub webhooks
- Serves API endpoints
- Initializes the scheduler

#### 2. Background Worker (`src/worker.ts`)

Processes repository sync jobs using BullMQ:

- Handles job concurrency
- Manages retries and failures
- Processes jobs based on priority

#### 3. Scheduler ([`@wei/probot-scheduler`](https://jsr.io/@wei/probot-scheduler))

Manages periodic repository checks:

- Schedules jobs using cron expressions
- Maintains job queues in Redis
- Handles job priorities and scheduling

#### 4. Database Layer

- **MongoDB**: Stores repository and installation data
- **Redis**: Manages job queues and caching
- **Connections**: Managed through `src/configs/database.ts` and
  `src/configs/redis.ts`

#### 5. Pull Processor (`src/processor/pull.ts`)

The Pull processor is the core component that handles repository
synchronization. Here's a detailed breakdown of its functionality:

```mermaid
graph TD
    A[Routine Check] --> B{Has Diff?}
    B -->|Yes| C{Existing PR?}
    B -->|No| D[Skip]
    C -->|Yes| E[Check Auto Merge]
    C -->|No| F[Create PR]
    F --> E
    E --> G{Mergeable?}
    G -->|Yes| H[Process Merge]
    G -->|No| I[Handle Conflict]
```

##### Main Components

1. **Routine Check (`routineCheck`):**

   - Iterates through configured rules in `pull.yml`
   - For each rule:
     - Normalizes base and upstream branch names
     - Checks for differences between branches
     - Creates or updates pull requests as needed

2. **Difference Detection (`hasDiff`):**

   - Compares commits between base and upstream branches
   - Handles special cases:
     - Large diffs that timeout
     - Missing branches
     - No common ancestor
   - Returns true if changes are detected

3. **Pull Request Management:**

   - **Updates (`getOpenPR`):**
     - Finds existing PRs created by the bot
     - Validates PR matches current sync rule
   - **Creation (`createPR`):**
     - Creates new pull request with standardized title
     - Assigns reviewers and labels
     - Updates PR body with tracking information

4. **Merge Process (`checkAutoMerge`, `processMerge`):**
   - Supports multiple merge methods:
     - `none`: No automatic merging
     - `merge`: Standard merge commit
     - `squash`: Squash and merge
     - `rebase`: Rebase and merge
     - `hardreset`: Force push to base branch
   - Handles merge conflicts:
     - Adds conflict label
     - Assigns conflict reviewers
     - Updates PR status

##### Error Handling

The processor implements robust error handling:

- Retries for mergeable status checks
- Graceful handling of GitHub API limitations
- Detailed logging for debugging
- Conflict resolution workflows

##### Best Practices

When modifying the Pull processor:

1. Maintain idempotency in operations
2. Implement proper error handling
3. Add detailed logging for debugging
4. Consider edge cases

## Submitting a Pull Request

1. Create a new branch: `git checkout -b my-branch-name`
2. Make your changes
3. Make sure the tests pass:
   ```bash
   deno test
   ```
4. Format your code:
   ```bash
   deno fmt
   ```
5. Run the linter:
   ```bash
   deno lint
   ```
6. Push to your fork and submit a pull request
7. Pat yourself on the back and wait for your pull request to be reviewed and
   merged

Here are a few things you can do that will increase the likelihood of your pull
request being accepted:

- Follow the style guide enforced by Deno's built-in formatter. You can format
  your code by running `deno fmt`
- Write and update tests
- Keep your changes as focused as possible. If there are multiple changes you
  would like to make that are not dependent upon each other, consider submitting
  them as separate pull requests
- Write a
  [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)

Work in Progress pull requests are also welcome to get feedback early on, or if
there is something blocked you.

## Resources

- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [GitHub Help](https://help.github.com)
- [Deno Manual](https://deno.land/manual)

## License

By contributing, you agree that your contributions will be licensed under its
MIT License.


================================================
FILE: .github/FUNDING.yml
================================================
github: wei


================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
  - package-ecosystem: "devcontainers"
    directory: "/"
    schedule:
      interval: "weekly"


================================================
FILE: .github/pull.yml
================================================
version: "1"
rules:
  - base: master
    upstream: wei:master
    mergeMethod: hardreset


================================================
FILE: .github/workflows/auto-tag.yml
================================================
name: Auto Tag Release on Version Change

on:
  workflow_dispatch:
  push:
    branches:
      - master
    paths:
      - "deno.json"

jobs:
  auto-tag:
    runs-on: ubuntu-latest

    permissions:
      contents: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6
        with:
          token: ${{ secrets.CR_PAT }}

      - name: Get version from deno.json
        id: get_version
        run: |
          VERSION=$(jq -r .version deno.json)
          if ! git ls-remote --tags origin | grep -q "refs/tags/v${VERSION}"; then
            echo "version=$VERSION" >> $GITHUB_OUTPUT
            echo "Version found: $VERSION"
          else
            echo "Version already exists: $VERSION"
          fi

      - name: Create Git Tag
        if: steps.get_version.outputs.version != ''
        run: |
          git tag v${{ steps.get_version.outputs.version }}
          git push origin v${{ steps.get_version.outputs.version }}

      - name: Create GitHub Release
        if: steps.get_version.outputs.version != ''
        uses: softprops/action-gh-release@v2
        with:
          tag_name: v${{ steps.get_version.outputs.version }}
          name: Release v${{ steps.get_version.outputs.version }}
          body: |
            🤖 GitHub App: [Pull](https://github.com/apps/pull)
          draft: false
          prerelease: ${{ contains(steps.get_version.outputs.version, 'alpha') || contains(steps.get_version.outputs.version, 'beta') || contains(steps.get_version.outputs.version, 'rc') }}


================================================
FILE: .github/workflows/publish.yml
================================================
name: Create and publish a Docker image

on:
  workflow_dispatch:
  pull_request:
  push:
    branches:
      - "master"
    tags:
      - "v*"

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      attestations: write
      id-token: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Log in to the Container registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.CR_PAT }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push Docker image
        id: push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

      - name: Generate artifact attestation
        if: github.event_name != 'pull_request'
        uses: actions/attest-build-provenance@v2
        with:
          subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
          subject-digest: ${{ steps.push.outputs.digest }}
          push-to-registry: true


================================================
FILE: .gitignore
================================================
node_modules
coverage
npm-debug.log
*.pem
.env


================================================
FILE: .hooks/pre-commit
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/hook.sh"

deno task check


================================================
FILE: Dockerfile
================================================
ARG DENO_VERSION=2.3.7
FROM denoland/deno:alpine-${DENO_VERSION}

ENV \
  ####################
  ###   Required   ###
  ####################
  APP_ID= \
  APP_NAME= \
  APP_SLUG= \
  WEBHOOK_SECRET= \
  PRIVATE_KEY= \
  ####################
  ###   Optional   ###
  ####################
  #SENTRY_DSN= \
  #GHE_HOST= \
  PORT=3000 \
  LOG_FORMAT=short \
  LOG_LEVEL=info \
  WEBHOOK_PATH=/api/github/webhooks \
  CONFIG_FILENAME=pull.yml \
  DEFAULT_MERGE_METHOD=hardreset \
  _=

# Set working directory
WORKDIR /app

# Copy dependency files
COPY deno.* .
RUN deno install

# Copy source code
COPY . .

# The app uses port 3000 by default
EXPOSE 3000

# Command to run the app
# CMD ["deno", "task", "dev"]
# CMD ["deno", "task", "worker"]


================================================
FILE: LICENSE
================================================
The MIT License

Copyright (c) 2024 Wei He <https://wei.mit-license.org>

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
<!-- deno-fmt-ignore-start -->

<div align="center">

<a href="https://wei.github.io/pull">
  <img src="https://prod.download/pull-social-svg" alt="Pull App">
</a>

</div>

<div align="center">

<a href="https://probot.github.io">
  <img src="https://badgen.net/badge/Probot/Featured/orange?icon=dependabot&cache=86400" alt="Probot Featured">
</a>
<a href="https://github.com/wei/pull">
  <img src="https://badgen.net/github/stars/wei/pull?label=Stars&icon=github&color=pink&cache=300" alt="GitHub Stars">
</a>

</div>

<div align="center">

<a href="https://github.com/apps/pull">
  <img src="https://badgen.net/https/pull.git.ci/badges/repos?color=cyan&cache=600" alt="Repositories">
</a>
<a href="https://github.com/apps/pull">
  <img src="https://badgen.net/https/pull.git.ci/badges/installations?color=blue&cache=600" alt="Installations">
</a>
<a href="https://github.com/issues?q=author:app/pull">
  <img src="https://badgen.net/https/pull.git.ci/badges/triggers?color=purple&cache=600" alt="Triggered #">
</a>

</div>

## Introduction

[![Version][version-badge]][version-url] [![Deno 2.0][deno-badge]][deno-url] [![TypeScript][ts-badge]][ts-url] [![License][license-badge]][license-url]

> 🤖 a GitHub App that keeps your forks up-to-date with upstream via automated
> pull requests.

_Can you help keep this open source service alive? **[💖 Please sponsor : )][pull-sponsor]**_

<!-- deno-fmt-ignore-end -->

## Features

- 🔄 **Automated Synchronization**: Ensures forks are updated by automatically
  creating pull requests to integrate new changes from upstream
- ⚙️ **Flexible Configuration**: Customize sync behavior through
  `.github/pull.yml` configuration to accommodate different merge strategies,
  including merge, squash, rebase, and hard reset
- 🕒 **Scheduled Updates**: Regularly checks for upstream changes periodically
  to ensure forks are always up-to-date
- 👥 **Team Integration**: Facilitates collaboration by automatically adding
  assignees and reviewers to pull requests, honoring branch protection rules and
  working seamlessly with pull request checks and reviews
- 🚀 **Enterprise Ready**: Supports GitHub Enterprise Server, ensuring a smooth
  integration process for enterprise-level projects

### Prerequisites

- Upstream must be in the same fork network.
- ⚠️ _Make a backup if you've made changes._

## Getting Started

**[⭐ Star this project][pull-repo]** (Highly recommended, starred users may
receive priority over other users)

### Basic Setup

- Just install
  **[<img src="https://prod.download/pull-18h-svg" valign="bottom"/> Pull app][pull-app]**.

Pull app will automatically watch and pull in upstream's default (master) branch
to yours using **hard reset** periodically. You can also manually
[trigger](#trigger-manually) it anytime.

### Advanced Configuration (with config file)

1. Create a new branch.
2. Setup the new branch as default branch under repository Settings > Branches.
3. Add `.github/pull.yml` to your default branch.

   #### Most Common
   (behaves the same as Basic Setup)
   ```yaml
   version: "1"
   rules:
     - base: master
       upstream: wei:master # change `wei` to the owner of upstream repo
       mergeMethod: hardreset
       mergeUnstable: true
   ```

   #### Advanced usage
   ```yaml
   version: "1"
   rules: # Array of rules
     - base: master # Required. Target branch
       upstream: wei:master # Required. Must be in the same fork network.
       mergeMethod: hardreset # Optional, one of [none, merge, squash, rebase, hardreset], Default: none.
       mergeUnstable: false # Optional, merge pull request even when the mergeable_state is not clean. Default: false
     - base: dev
       upstream: master # Required. Can be a branch in the same forked repo.
       assignees: # Optional
         - wei
       reviewers: # Optional
         - wei
       conflictReviewers: # Optional, on merge conflict assign a reviewer
         - wei
   label: ":arrow_heading_down: pull" # Optional
   conflictLabel: "merge-conflict" # Optional, on merge conflict assign a custom label, Default: merge-conflict
   ```

4. Go to `https://pull.git.ci/check/${owner}/${repo}` to validate your
   `.github/pull.yml`.
5. Install
   **[<img src="https://prod.download/pull-18h-svg" valign="bottom"/> Pull app][pull-app]**.

### Trigger Manually

You can manually trigger Pull by going to
`https://pull.git.ci/process/${owner}/${repo}`.

### For Upstream Repository Owners

For the most common use case (a single `master` branch), you can just direct
users to install Pull with no configurations. If you need a more advanced setup
(such as a `docs` branch in addition to `master`), consider adding
`.github/pull.yml` to your repository pointing to yourself (see example). This
will allow forks to install Pull and stay updated automatically.

Example (assuming `owner` is your user or organization name):

```yaml
version: "1"
rules:
  - base: master
    upstream: owner:master
    mergeMethod: hardreset
    mergeUnstable: true
  - base: docs
    upstream: owner:docs
    mergeMethod: hardreset
    mergeUnstable: true
```

## Contributing

See [CONTRIBUTING.md](./.github/CONTRIBUTING.md)

## License

[MIT](LICENSE) © [Wei He][pull-sponsor]

## Support

_Can you help keep this open source service alive?
**[💖 Please sponsor : )][pull-sponsor]**_

---

Made with ❤️ by [@wei](https://github.com/wei)

[version-badge]: https://badgen.net/https/pull.git.ci/badges/version?label=Version&color=green&cache=300
[version-url]: https://pull.git.ci/version
[deno-badge]: https://img.shields.io/badge/Deno%202.0-000000?logo=Deno&logoColor=ffffff
[deno-url]: https://deno.com
[ts-badge]: https://badgen.net/badge/_/TypeScript/blue?&label=&icon=typescript&cache=86400
[ts-url]: https://www.typescriptlang.org
[license-badge]: https://badgen.net/badge/License/MIT/black?cache=86400
[license-url]: https://wei.mit-license.org
[pull-app]: https://github.com/apps/pull
[pull-repo]: https://github.com/wei/pull
[pull-sponsor]: https://prod.download/pull-readme-sponsor


================================================
FILE: _config.yml
================================================
theme: jekyll-theme-cayman
plugins:
  - jemoji


================================================
FILE: deno.json
================================================
{
  "version": "2.0.0-alpha.4",
  "tasks": {
    "dev": "deno run --env-file --allow-all src/index.ts",
    "dev:skip-full-sync": "deno task dev --skip-full-sync",
    "worker": "deno run --env-file --allow-all src/worker.ts",
    "full-sync": "deno run --env-file --allow-all scripts/full-sync.ts",
    "manual-process": "deno run --env-file --allow-all scripts/manual-process.ts",
    "hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts",
    "install-hooks": "deno task hook install",
    "test": "deno test --allow-net --allow-env --allow-read",
    "check": "deno lint && deno fmt --check && deno check ."
  },
  "imports": {
    "@/": "./",
    "@octokit/plugin-rest-endpoint-methods": "npm:@octokit/plugin-rest-endpoint-methods@^13.2.6",
    "@probot/pino": "npm:@probot/pino@^2.5.0",
    "@std/assert": "jsr:@std/assert@^1.0.13",
    "@std/http": "jsr:@std/http@^1.0.18",
    "@wei/pluralize": "jsr:@wei/pluralize@^8.0.2",
    "@wei/probot-scheduler": "jsr:@wei/probot-scheduler@0.1.0-alpha.17",
    "bullmq": "npm:bullmq@^5.26.2",
    "express": "npm:express@^4.21.1",
    "ioredis": "npm:ioredis@^5.4.1",
    "mongoose": "npm:mongoose@^8.8.1",
    "pino": "npm:pino@^9.5.0",
    "probot": "npm:probot@^13.4.0",
    "zod": "npm:zod@^3.23.8"
  }
}


================================================
FILE: docker-compose.yml
================================================
services:
  app:
    build: .
    restart: unless-stopped
    ports:
      - "3000:3000"
    env_file:
      - ./.env
    depends_on:
      - mongodb
      - redis
    # volumes:
    #   - .:/app
    command: deno task dev
    ## Skip full-sync on startup:
    # command: deno task dev:skip-full-sync
    ## Run full-sync manually:
    # > docker exec -it pull-app-1 deno task full-sync

  worker:
    build: .
    restart: unless-stopped
    env_file:
      - ./.env
    depends_on:
      - mongodb
      - redis
      - app
    # volumes:
    #   - .:/app
    command: deno task worker
    deploy:
      mode: replicated
      replicas: 3
    # If running without swarm mode, you can use the following command to scale the worker:
    # > docker compose up -d --scale worker=3

  mongodb:
    image: mongo:8
    restart: unless-stopped
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: mongodb_password
    volumes:
      - mongodb-data:/data/db
    # ports:
    #   - "27017:27017"

  redis:
    image: redis:7.4
    restart: unless-stopped
    volumes:
      - redis-data:/data
    # ports:
    #   - "6379:6379"

  # mongo-express:
  #   image: mongo-express
  #   restart: unless-stopped
  #   depends_on:
  #     - mongodb
  #   environment:
  #     ME_CONFIG_MONGODB_URL: mongodb://root:mongodb_password@mongodb:27017/
  #     ME_CONFIG_BASICAUTH: false
  #   ports:
  #     - "8081:8081"

  # redis-commander:
  #   image: rediscommander/redis-commander:latest
  #   restart: unless-stopped
  #   environment:
  #     REDIS_HOSTS: redis
  #   depends_on:
  #     - redis
  #   ports:
  #     - "8082:8081"

  # bullboard:
  #   image: addono/bull-board:latest
  #   restart: unless-stopped
  #   environment:
  #     REDIS_HOST: redis
  #     PORT: 8083
  #   depends_on:
  #     - redis
  #   ports:
  #     - "8083:8083"

volumes:
  mongodb-data:
  redis-data:


================================================
FILE: scripts/full-sync.ts
================================================
import { createProbot } from "probot";
import { fullSync } from "@wei/probot-scheduler";
import logger from "@/src/utils/logger.ts";
import { connectMongoDB, disconnectMongoDB } from "@/src/configs/database.ts";
import { getRepositorySchedule } from "@/src/utils/get-repository-schedule.ts";
import { getRedisClient } from "@/src/configs/redis.ts";
import { appConfig } from "@/src/configs/app-config.ts";

async function main() {
  let exitCode = 0;
  let redisClient: ReturnType<typeof getRedisClient> | undefined;

  try {
    await connectMongoDB();
    redisClient = getRedisClient(`${appConfig.appSlug}-full-sync`);

    const probot = createProbot({ overrides: { log: logger } });
    await fullSync(probot, {
      redisClient,
      getRepositorySchedule,
    });
  } catch (error) {
    logger.error(error, "Error during full sync");
    exitCode = 1;
  } finally {
    await disconnectMongoDB();
    if (redisClient) {
      try {
        await redisClient.quit();
      } catch {
        // ignore
      }
    }
    Deno.exit(exitCode);
  }
}

if (import.meta.main) {
  await main();
}


================================================
FILE: scripts/manual-process.ts
================================================
import { connectMongoDB, disconnectMongoDB } from "@/src/configs/database.ts";
import { JobPriority, RepositoryModel } from "@wei/probot-scheduler";
import { createProbot } from "probot";
import logger from "@/src/utils/logger.ts";
import { getPullConfig } from "@/src/utils/get-pull-config.ts";
import { Pull } from "@/src/processor/pull.ts";

async function main(full_name: string) {
  let exitCode = 0;

  try {
    await connectMongoDB();

    logger.info(`🏃 Processing repo job ${full_name}`);

    try {
      // Get Octokit
      const probot = createProbot({ overrides: { log: logger } });

      const repoRecord = await RepositoryModel.findOne({ full_name });

      if (!repoRecord) {
        logger.error({ full_name }, `❌ Repo record not found`);
        throw new Error(`❌ Repo record not found`);
      }

      const { installation_id, owner: { login: owner }, name: repo } =
        repoRecord;

      const octokit = await probot.auth(installation_id);

      const config = await getPullConfig(octokit, logger, {
        installation_id,
        owner,
        repo,
        repository_id: 0,
        metadata: {
          cron: "",
          job_priority: JobPriority.Normal,
          repository_id: 0,
        },
      });
      if (!config) {
        logger.info(`⚠️ No config found, skipping`);
        return;
      }

      const pull = new Pull(octokit, { owner, repo, logger }, config);
      await pull.routineCheck();

      logger.info(`✅ Repo job processed successfully`);
    } catch (error) {
      logger.error(error, "❌ Repo job failed");
    }
  } catch (error) {
    logger.error(error, "Error processing");
    exitCode = 1;
  } finally {
    await disconnectMongoDB();
    Deno.exit(exitCode);
  }
}

if (import.meta.main) {
  const args = Deno.args;
  if (args.length !== 1) {
    logger.error(
      "Usage: deno task manual-process <owner>/<repo>",
    );
    Deno.exit(1);
  }

  await main(args[0]);
}


================================================
FILE: src/app.ts
================================================
import { Probot } from "probot";
import { createSchedulerApp, SchedulerAppOptions } from "@wei/probot-scheduler";

export default (app: Probot, opts: SchedulerAppOptions) => {
  createSchedulerApp(app, opts);
};


================================================
FILE: src/configs/app-config.ts
================================================
import { readEnvOptions } from "probot/lib/bin/read-env-options.js";
import denoJson from "@/deno.json" with { type: "json" };

function getAppConfig(env: Record<string, string> = Deno.env.toObject()) {
  return {
    ...readEnvOptions(env),
    version: denoJson.version,
    appName: env.APP_NAME || "Pull",
    appSlug: env.APP_SLUG || "pull",
    botName: `${env.APP_SLUG || "pull"}[bot]`,
    configFilename: env.CONFIG_FILENAME || "pull.yml",
    mongoDBUrl: env.MONGODB_URL,
    port: parseInt(env.PORT || "3000", 10),
    webhookPath: env.WEBHOOK_PATH,
    defaultMergeMethod: env.DEFAULT_MERGE_METHOD || "hardreset",
  };
}

export const appConfig = getAppConfig();


================================================
FILE: src/configs/database.ts
================================================
import mongoose from "mongoose";
import { appConfig } from "@/src/configs/app-config.ts";
import log from "@/src/utils/logger.ts";

export const connectMongoDB = async () => {
  if (mongoose.connection.readyState !== 1) {
    await mongoose.connect(appConfig.mongoDBUrl!);
  }
  if (mongoose.connection.readyState !== 1) {
    throw new Error("[MongoDB] Failed to connect");
  }
  log.info("[MongoDB] Connected");
};

export const disconnectMongoDB = async () => {
  await mongoose.disconnect();
  log.info("[MongoDB] Disconnected");
};


================================================
FILE: src/configs/redis.ts
================================================
import { appConfig } from "@/src/configs/app-config.ts";
import { Redis } from "ioredis";

export const getRedisClient = (name?: string) => {
  const redisClient = new Redis(appConfig.redisConfig!, {
    maxRetriesPerRequest: null,
    name,
  });
  return redisClient;
};


================================================
FILE: src/index.ts
================================================
import express from "express";
import { createNodeMiddleware, createProbot } from "probot";
import { createSchedulerService } from "@wei/probot-scheduler";
import createSchedulerApp from "@/src/app.ts";
import { appConfig } from "@/src/configs/app-config.ts";
import log from "@/src/utils/logger.ts";
import { connectMongoDB, disconnectMongoDB } from "@/src/configs/database.ts";
import { getRedisClient } from "@/src/configs/redis.ts";
import createRouter from "@/src/router/index.ts";
import { getRepositorySchedule } from "@/src/utils/get-repository-schedule.ts";

const args = Deno.args;
const skipFullSync = args.includes("--skip-full-sync");

await connectMongoDB();

const redisClient = getRedisClient(`${appConfig.appSlug}-app`);

const probot = createProbot({
  overrides: {
    log,
  },
});
const schedulerApp = createSchedulerApp.bind(null, probot, {
  // Optional: Skip the initial full sync
  skipFullSync,

  redisClient,

  // Define custom repository scheduling
  getRepositorySchedule,
});
const schedulerService = createSchedulerService(probot, {
  redisClient,
  getRepositorySchedule,
});

const server = express();
const gitHubWebhookPath = appConfig.webhookPath || "/api/github/webhooks";
server.use(
  gitHubWebhookPath,
  createNodeMiddleware(schedulerApp, {
    probot,
    webhooksPath: "/",
  }),
);
server.use("/", createRouter(probot, schedulerService));

server.listen(appConfig.port, () => {
  log.info(`[Express] Server is running on port ${appConfig.port}`);
});

Deno.addSignalListener("SIGINT", () => handleAppTermination("SIGINT"));
Deno.addSignalListener("SIGTERM", () => handleAppTermination("SIGTERM"));

function handleAppTermination(signal: string) {
  log.info(`[${signal}] Signal received: closing MongoDB connection`);
  disconnectMongoDB();
  try {
    // Close Redis connection to avoid lingering connections
    redisClient.quit();
  } catch {
    // ignore
  }
  log.info("[MongoDB] Connection closed due to app termination");
  Deno.exit(0);
}


================================================
FILE: src/processor/index.ts
================================================
import type { Job } from "bullmq";
import type { SchedulerJobData } from "@wei/probot-scheduler";
import type { Logger, Probot, ProbotOctokit } from "probot";
import logger from "@/src/utils/logger.ts";
import { getPullConfig } from "@/src/utils/get-pull-config.ts";
import { Pull } from "@/src/processor/pull.ts";

const TIMEOUT = 60 * 1000;

function createTimeoutPromise(log: Logger) {
  return new Promise((_, reject) => {
    setTimeout(() => {
      log.warn("⏰ Job timed out after 1 minute");
      reject(new Error("Job timed out after 1 minute"));
    }, TIMEOUT);
  });
}

async function processRepo(
  octokit: ProbotOctokit,
  jobData: SchedulerJobData,
  log: Logger,
) {
  const { owner, repo } = jobData;

  const config = await getPullConfig(octokit, log, jobData);
  if (!config) {
    log.info(`⚠️ No config found, skipping`);
    return;
  }

  const pull = new Pull(octokit, { owner, repo, logger: log }, config);
  await pull.routineCheck();
}

export function getRepoProcessor(probot: Probot) {
  return async function RepoJobProcessor(job: Job<SchedulerJobData>) {
    const log = logger.child({
      jobId: job.id,
      jobData: job.data,
    });

    log.info("🏃 Processing repo job");

    try {
      const octokit = await probot.auth(job.data.installation_id);

      await Promise.race([
        processRepo(octokit, job.data, log),
        createTimeoutPromise(log),
      ]);

      log.info(`✅ Repo job processed successfully`);
    } catch (error) {
      log.error(error, "❌ Repo job failed");
    }
  };
}


================================================
FILE: src/processor/pull.ts
================================================
import { type Logger, ProbotOctokit } from "probot";
import pluralize from "@wei/pluralize";
import {
  type PullConfig,
  pullConfigSchema,
  type PullRule,
} from "@/src/utils/schema.ts";
import type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
import { appConfig } from "@/src/configs/app-config.ts";
import { logger as pullLogger } from "@/src/utils/logger.ts";
import { getPRBody, getPRTitle, timeout } from "@/src/utils/helpers.ts";

interface PullOptions {
  owner: string;
  repo: string;
  logger?: Logger;
}

type PullRequestData =
  RestEndpointMethodTypes["pulls"]["create"]["response"]["data"];

export class Pull {
  private github: ProbotOctokit;
  private owner: string;
  private repo: string;
  private fullName: string;
  private logger: Logger;
  private config: PullConfig;

  constructor(
    github: ProbotOctokit,
    { owner, repo, logger = pullLogger }: PullOptions,
    config: PullConfig,
  ) {
    this.github = github;
    this.owner = owner;
    this.repo = repo;
    this.fullName = `${owner}/${repo}`;
    this.logger = logger.child({
      owner,
      repo,
      full_name: this.fullName,
    });

    const result = pullConfigSchema.safeParse(config);
    if (!result.success) {
      throw new Error("Invalid config");
    }

    this.config = result.data;
  }

  async routineCheck(): Promise<void> {
    this.logger.info(
      { config: this.config },
      `Routine Check - ${pluralize("rule", this.config.rules.length, true)}`,
    );

    for (const rule of this.config.rules) {
      this.logger.debug({ rule }, `Routine Check for rule`);
      const { base, upstream, assignees, reviewers } = rule;
      const normalizedBase = base.toLowerCase();
      const normalizedUpstream = upstream.toLowerCase().replace(
        `${this.owner.toLowerCase()}:`,
        "",
      );

      if (normalizedBase === normalizedUpstream) {
        this.logger.debug(
          `${base} is same as ${upstream}`,
        );
        continue;
      }

      if (!(await this.hasDiff(base, upstream))) {
        this.logger.debug(
          `${base} is in sync with ${upstream}`,
        );
        continue;
      }

      const openPR = await this.getOpenPR(base, upstream);
      if (openPR) {
        this.logger.debug(
          `Found a PR from ${upstream} to ${base}`,
        );
        await this.checkAutoMerge(openPR);
      } else {
        this.logger.info(
          `Creating PR from ${upstream} to ${base}`,
        );
        const newPR = await this.createPR(base, upstream, assignees, reviewers);
        await this.checkAutoMerge(newPR);
      }
    }
  }

  private async checkAutoMerge(
    incomingPR: PullRequestData | null,
    config: { isMergeableMaxRetries?: number } = {},
  ): Promise<boolean> {
    if (!incomingPR) return false;

    const prNumber = incomingPR.number;
    this.logger.debug(
      `#${prNumber} Checking auto merged pull request`,
    );

    const rule: PullRule | undefined = this.config.rules.find((r) =>
      r.base.toLowerCase() === incomingPR.base.ref.toLowerCase() &&
      (r.upstream.toLowerCase() === incomingPR.head.label.toLowerCase() ||
        r.upstream.toLowerCase() === incomingPR.head.ref.toLowerCase())
    );

    if (!rule) {
      this.logger.debug(
        `#${prNumber} No rule found`,
      );
      return false;
    }

    if (incomingPR.mergeable === false) {
      await this.handleMergeConflict(prNumber, rule);
      return false;
    }

    if (
      rule.mergeMethod !== "none" &&
      incomingPR.state === "open" &&
      incomingPR.user.login === appConfig.botName
    ) {
      return await this.processMerge(prNumber, incomingPR, rule, config);
    }

    this.logger.debug(
      `#${prNumber} Skip processing`,
    );
    return false;
  }

  private async handleMergeConflict(
    prNumber: number,
    rule?: PullRule,
  ): Promise<void> {
    this.logger.debug(
      `#${prNumber} mergeable:false`,
    );

    try {
      await this.github.issues.getLabel({
        owner: this.owner,
        repo: this.repo,
        name: this.config.conflictLabel,
      });
    } catch {
      await this.addLabel(
        this.config.conflictLabel,
        "ff0000",
        "Resolve conflicts manually",
      );
    }

    await this.github.issues.update({
      owner: this.owner,
      repo: this.repo,
      issue_number: prNumber,
      labels: [this.config.label, this.config.conflictLabel],
      body: getPRBody(this.fullName, prNumber),
    });

    if (rule?.conflictReviewers?.length) {
      await this.addReviewers(prNumber, rule.conflictReviewers);
    }
  }

  private async processMerge(
    prNumber: number,
    incomingPR: PullRequestData,
    rule: PullRule,
    config: { isMergeableMaxRetries?: number },
  ): Promise<boolean> {
    const mergeableStatus = await this.getMergeableStatus(
      prNumber,
      incomingPR,
      rule,
      config,
    );

    if (!mergeableStatus?.mergeable) return false;

    if (rule.mergeMethod === "hardreset") {
      try {
        this.logger.debug(
          `#${prNumber} Performing hard reset`,
        );
        await this.hardResetCommit(incomingPR.base.ref, incomingPR.head.sha);
        this.logger.info(
          `#${prNumber} Hard reset successful`,
        );
        return true;
      } catch (err) {
        this.logger.error(
          { err },
          `#${prNumber} Hard reset failed`,
        );
        return false;
      }
    } else if (rule.mergeMethod === "none") {
      this.logger.debug(
        `#${prNumber} Merge method is none, skip merging`,
      );
      return true;
    } else {
      let mergeMethod = rule.mergeMethod;
      if (mergeMethod === "rebase" && !mergeableStatus.rebaseable) {
        mergeMethod = "merge";
      }
      await this.mergePR(prNumber, mergeMethod);
      this.logger.info(
        `#${prNumber} Auto merged pull request using ${mergeMethod}`,
      );
      return true;
    }
  }

  private async getMergeableStatus(
    prNumber: number,
    incomingPR: PullRequestData,
    rule: PullRule,
    config: { isMergeableMaxRetries?: number },
  ) {
    const isInitiallyMergeable = incomingPR.mergeable &&
      (incomingPR.mergeable_state === "clean" ||
        (incomingPR.mergeable_state === "unstable" && rule.mergeUnstable));

    if (isInitiallyMergeable) {
      return incomingPR;
    }

    return await this.isMergeable(prNumber, {
      maxRetries: config.isMergeableMaxRetries,
    });
  }

  private async hasDiff(base: string, upstream: string): Promise<boolean> {
    try {
      const comparison = await this.github.repos.compareCommits({
        owner: this.owner,
        repo: this.repo,
        head: upstream,
        base,
        per_page: 1,
      });
      return comparison.data.total_commits > 0;
    } catch (e) {
      if (e instanceof Error) {
        if (e.message.match(/this diff is taking too long to generate/i)) {
          return true;
        } else if (e.message.match(/not found/i)) {
          this.logger.debug(
            `${this.owner}:${base}...${upstream} Not found`,
          );
          return false;
        } else if (e.message.match(/no common ancestor/i)) {
          this.logger.debug(
            `${this.owner}:${base}...${upstream} No common ancestor`,
          );
          return false;
        }
      }
      this.logger.error(
        {
          err: e,
          head: upstream,
        },
        `Unable to fetch diff`,
      );
      return false;
    }
  }

  private async getOpenPR(base: string, head: string) {
    this.logger.debug(
      `Checking for open PRs from ${head} to ${base} created by ${appConfig.botName}`,
    );

    const res = await this.github.issues.listForRepo({
      owner: this.owner,
      repo: this.repo,
      creator: appConfig.botName,
      per_page: 100,
    });

    if (res.data.length > 0) {
      this.logger.debug(
        `Found ${res.data.length} open ${pluralize("PR", res.data.length, true)} from ${appConfig.botName}`,
      );

      for (const issue of res.data) {
        const pr = await this.github.pulls.get({
          owner: this.owner,
          repo: this.repo,
          pull_number: issue.number,
        });

        if (
          pr.data.user.login === appConfig.botName &&
          pr.data.base.label.replace(`${this.owner}:`, "") ===
            base.replace(`${this.owner}:`, "") &&
          pr.data.head.label.replace(`${this.owner}:`, "") ===
            head.replace(`${this.owner}:`, "")
        ) {
          this.logger.debug(
            `Found open PR #${pr.data.number} from ${head} to ${base} created by ${appConfig.botName}`,
          );
          return pr.data;
        }
      }
    }

    this.logger.debug(
      `No open PR found from ${head} to ${base} created by ${appConfig.botName}`,
    );
    return null;
  }

  private async createPR(
    base: string,
    upstream: string,
    assignees: string[],
    reviewers: string[],
  ) {
    try {
      const createdPR = await this.github.pulls.create({
        owner: this.owner,
        repo: this.repo,
        head: upstream,
        base,
        maintainer_can_modify: false,
        title: getPRTitle(base, upstream),
        body: getPRBody(this.fullName),
      });

      const prNumber = createdPR.data.number;
      this.logger.debug(
        `#${prNumber} Created pull request`,
      );

      try {
        await this.github.issues.lock({
          owner: this.owner,
          repo: this.repo,
          issue_number: prNumber,
        });
      } catch (err) {
        this.logger.error(
          { err },
          `#${prNumber} Lock failed`,
        );
      }

      await this.github.issues.update({
        owner: this.owner,
        repo: this.repo,
        issue_number: prNumber,
        assignees,
        labels: [this.config.label],
        body: getPRBody(this.fullName, prNumber),
      });

      await this.addReviewers(prNumber, reviewers);
      this.logger.debug(
        `#${prNumber} Updated pull request`,
      );

      const pr = await this.github.pulls.get({
        owner: this.owner,
        repo: this.repo,
        pull_number: prNumber,
      });

      return pr.data;
    } catch (err) {
      this.logger.error(
        { err },
        `Create PR from ${upstream} failed`,
      );
      return null;
    }
  }

  private async isMergeable(
    prNumber: number,
    config: { maxRetries?: number } = {},
  ): Promise<PullRequestData | null> {
    const maxRetries = config.maxRetries || 3;
    let attempts = 0;

    while (attempts++ < maxRetries) {
      const pr = await this.github.pulls.get({
        owner: this.owner,
        repo: this.repo,
        pull_number: prNumber,
      });

      this.logger.debug(
        `#${prNumber} Mergeability is ${pr.data.mergeable_state}`,
      );

      if (
        typeof pr.data.mergeable === "boolean" &&
        pr.data.mergeable_state !== "unknown"
      ) {
        return pr.data.mergeable && pr.data.mergeable_state === "clean"
          ? pr.data
          : null;
      }

      // Wait a bit to see if the mergeable state changes
      await timeout(4500);
    }
    return null;
  }

  private async addReviewers(
    prNumber: number | undefined,
    allReviewers: string[] | undefined,
  ): Promise<void> {
    if (!prNumber || !allReviewers?.length) return;

    const reviewers = allReviewers.filter((r) => !r.includes("/"));
    const teamReviewers = allReviewers
      .filter((r) => r.includes("/"))
      .map((r) => r.split("/")[1]);

    await this.github.pulls.requestReviewers({
      owner: this.owner,
      repo: this.repo,
      pull_number: prNumber,
      team_reviewers: teamReviewers,
      reviewers,
    });
  }

  private async addLabel(
    label: string | undefined,
    color = "ededed",
    description = "",
  ): Promise<void> {
    if (!label) return;

    await this.github.issues.createLabel({
      owner: this.owner,
      repo: this.repo,
      name: label,
      color,
      description,
    });
  }

  private async mergePR(
    prNumber: number | undefined,
    mergeMethod: "merge" | "squash" | "rebase" = "merge",
  ): Promise<void> {
    if (!prNumber) return;

    await this.github.pulls.merge({
      owner: this.owner,
      repo: this.repo,
      pull_number: prNumber,
      merge_method: mergeMethod,
    });
  }

  private async hardResetCommit(
    baseRef: string | undefined,
    sha: string,
  ): Promise<void> {
    if (!baseRef || !sha) return;

    await this.github.git.updateRef({
      owner: this.owner,
      repo: this.repo,
      ref: `heads/${baseRef}`,
      sha,
      force: true,
    });
  }
}


================================================
FILE: src/router/index.ts
================================================
import type { Request, Response } from "express";
import type { Probot } from "probot";
import express from "express";
import { createSchedulerService } from "@wei/probot-scheduler";
import { appConfig } from "@/src/configs/app-config.ts";
import getStatsHandlers from "@/src/router/stats.ts";
import getRepoHandlers from "@/src/router/repo-handler.ts";

const createRouter = (
  app: Probot,
  schedulerService: ReturnType<typeof createSchedulerService>,
) => {
  const router = express.Router();

  router.get("/", (_req: Request, res: Response) => {
    res.redirect("https://wei.github.io/pull");
  });

  router.get("/version", (_req: Request, res: Response) => {
    res.json({ name: appConfig.appName, version: appConfig.version });
  });

  router.get("/ping", (_req: Request, res: Response) => {
    res.json({ status: "pong" });
  });

  const { probotStatsHandler } = getStatsHandlers(app);
  router.get("/probot/stats", probotStatsHandler);

  const { checkHandler, processHandler } = getRepoHandlers(
    app,
    schedulerService,
  );
  router.get("/check/:owner/:repo", checkHandler);
  router.get("/process/:owner/:repo", processHandler);
  router.post("/process/:owner/:repo", processHandler);

  return router;
};

export default createRouter;


================================================
FILE: src/router/repo-handler.ts
================================================
import type { Request, Response } from "express";
import type { Probot } from "probot";
import { appConfig } from "@/src/configs/app-config.ts";
import { getPullConfig } from "@/src/utils/get-pull-config.ts";
import {
  createSchedulerService,
  JobPriority,
  RepositoryModel,
} from "@wei/probot-scheduler";

function getRepoHandlers(
  app: Probot,
  schedulerService: ReturnType<typeof createSchedulerService>,
) {
  async function checkHandler(req: Request, res: Response) {
    const full_name = `${req.params.owner}/${req.params.repo}`;
    app.log.info({ full_name }, `Checking ${appConfig.configFilename}`);

    try {
      // Get Octokit
      const repoRecord = await RepositoryModel.findOne({ full_name });

      if (!repoRecord) {
        app.log.error({ full_name }, `❌ Repo record not found`);
        throw new Error(`❌ Repo record not found`);
      }

      const {
        installation_id,
        id: repository_id,
        owner: { login: owner },
        name: repo,
      } = repoRecord;

      const octokit = await app.auth(installation_id);
      const config = await getPullConfig(octokit, app.log, {
        installation_id,
        owner,
        repo,
        repository_id,
        metadata: {
          cron: "",
          job_priority: JobPriority.Normal,
          repository_id,
        },
      });

      if (!config) {
        return res.status(404).json({
          status: "error",
          message: `Configuration file '${appConfig.configFilename}' not found`,
        });
      }

      res.json(config);
    } catch (error) {
      app.log.error(error);
      res.status(500).json({
        status: "error",
        message: error instanceof Error
          ? error.message
          : "Unknown error occurred",
      });
    }
  }

  async function processHandler(req: Request, res: Response) {
    const full_name = `${req.params.owner}/${req.params.repo}`;
    app.log.info({ full_name }, `Processing`);

    try {
      await schedulerService.processRepository({ fullName: full_name }, true);

      res.json({ status: "queued" });
    } catch (error) {
      app.log.error(error);
      res.status(500).json({
        status: "error",
        message: error instanceof Error
          ? error.message
          : "Unknown error occurred",
      });
    }
  }

  return {
    checkHandler,
    processHandler,
  };
}

export default getRepoHandlers;


================================================
FILE: src/router/stats.ts
================================================
import type { Request, Response } from "express";
import { Probot } from "probot";

function getStatsHandlers(_app: Probot) {
  async function probotStatsHandler(_req: Request, res: Response) {
    const response = await fetch(
      "https://raw.githack.com/pull-app/stats/master/stats.json",
    );
    const data = await response.json();
    res.json(data);
  }

  return {
    probotStatsHandler,
  };
}

export default getStatsHandlers;


================================================
FILE: src/utils/get-pull-config.ts
================================================
import type { Logger, ProbotOctokit } from "probot";
import { appConfig } from "@/src/configs/app-config.ts";
import { SchedulerJobData } from "@wei/probot-scheduler";
import { PullConfig, pullConfigSchema } from "@/src/utils/schema.ts";
import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";

async function getLivePullConfig(
  octokit: ProbotOctokit,
  log: Logger,
  jobData: SchedulerJobData,
): Promise<PullConfig | null> {
  log.debug(`⚙️ Fetching live config`);

  const { owner, repo } = jobData;

  const { config } = await octokit.config.get({
    owner,
    repo,
    path: `.github/${appConfig.configFilename}`,
  });

  // Log config if found
  if (!config || !config.version) {
    log.warn("⚠️ No config found");
    return null;
  } else {
    log.info({ config }, "⚙️ Config found");
  }

  const result = pullConfigSchema.safeParse(config);
  if (!result.success) {
    throw new Error("Invalid config");
  }

  return result.data;
}

function getDefaultPullConfig(
  repository: RestEndpointMethodTypes["repos"]["get"]["response"]["data"],
  log: Logger,
): PullConfig | null {
  log.debug(`⚙️ Fetching default config`);

  if (repository.fork && repository.parent) {
    const upstreamOwner = repository.parent.owner &&
      repository.parent.owner.login;
    const defaultBranch = repository.parent.default_branch;

    if (upstreamOwner && defaultBranch) {
      log.debug(
        `Using default config ${defaultBranch}...${upstreamOwner}:${defaultBranch}`,
      );

      const defaultConfig = {
        version: "1",
        rules: [
          {
            base: `${defaultBranch}`,
            upstream: `${upstreamOwner}:${defaultBranch}`,
            mergeMethod: appConfig.defaultMergeMethod,
            mergeUnstable: true,
          },
        ],
      };

      const result = pullConfigSchema.safeParse(defaultConfig);
      if (!result.success) {
        throw new Error("Invalid default config");
      }

      return result.data;
    }
  }

  return null;
}

export async function getPullConfig(
  octokit: ProbotOctokit,
  log: Logger,
  jobData: SchedulerJobData,
): Promise<PullConfig | null> {
  log.info(`⚙️ Fetching config`);

  const { owner, repo } = jobData;

  const { data: repository } = await octokit.rest.repos.get({ owner, repo });

  if (repository.archived) {
    log.debug(`⚠️ Repository is archived, skipping`);
    return null; // TODO Cancel scheduled job
  }

  let config = await getLivePullConfig(octokit, log, jobData);
  if (!config && !repository.fork) {
    return null; // TODO Cancel scheduled job
  } else if (!config) {
    config = getDefaultPullConfig(repository, log);
  }

  return config;
}


================================================
FILE: src/utils/get-repository-schedule.ts
================================================
import {
  JobPriority,
  type RepositoryMetadataSchemaType,
  type RepositorySchemaType,
} from "@wei/probot-scheduler";
import { getRandomCronSchedule } from "@/src/utils/helpers.ts";

// deno-lint-ignore require-await
export async function getRepositorySchedule(
  repository: RepositorySchemaType,
  currentMetadata?: RepositoryMetadataSchemaType,
) {
  return {
    repository_id: repository.id,
    cron: currentMetadata?.cron ?? getRandomCronSchedule(),
    job_priority: currentMetadata?.job_priority ?? JobPriority.Normal,
  };
}


================================================
FILE: src/utils/helpers.ts
================================================
import { appConfig } from "@/src/configs/app-config.ts";

export const getRandomCronSchedule = () => {
  // Every 6 hours at a random minute
  const randomMinute = Math.floor(Math.random() * 60);
  const randomHour1 = Math.floor(Math.random() * 6);
  const randomHour2 = randomHour1 + 6;
  const randomHour3 = randomHour2 + 6;
  const randomHour4 = randomHour3 + 6;
  return `${randomMinute} ${randomHour1},${randomHour2},${randomHour3},${randomHour4} * * *`;
};

export const timeout = (ms: number): Promise<void> =>
  new Promise((resolve) => setTimeout(resolve, ms));

export const getPRTitle = (ref: string, upstream: string): string =>
  `[pull] ${ref} from ${upstream}`;

export const getPRBody = (fullName: string, prNumber?: number): string =>
  (prNumber
    ? `See [Commits](/${fullName}/pull/${prNumber}/commits) and [Changes](/${fullName}/pull/${prNumber}/files) for more details.`
    : `See Commits and Changes for more details.`) +
  `\n\n-----\nCreated by [<img src="https://prod.download/pull-18h-svg" valign="bottom"/> **pull[bot]**](https://github.com/wei/pull) (v${appConfig.version})` +
  "\n\n_Can you help keep this open source service alive? **[💖 Please sponsor : )](https://prod.download/pull-pr-sponsor)**_";


================================================
FILE: src/utils/logger.ts
================================================
import { appConfig } from "@/src/configs/app-config.ts";
import { pino } from "pino";
import { getTransformStream, type LogLevel } from "@probot/pino";

export const createLogger = (
  {
    name,
    logFormat = "pretty",
    logLevel = "info",
    logLevelInString = true,
    logMessageKey = "msg",
  }: {
    name: string;
    logFormat?: "json" | "pretty";
    logLevel?: LogLevel | "silent";
    logLevelInString?: boolean;
    logMessageKey?: string;
  },
) => {
  const transform = getTransformStream({
    logFormat: logFormat,
    logLevelInString: logLevelInString,
  });
  transform.pipe(pino.destination(1));

  const log = pino(
    {
      name,
      level: logLevel,
      messageKey: logMessageKey,
    },
    transform,
  );
  return log;
};

export const logger = createLogger({
  name: appConfig.appName,
  logFormat: appConfig.logFormat,
  logLevel: appConfig.logLevel,
  logLevelInString: appConfig.logLevelInString,
  logMessageKey: appConfig.logMessageKey,
});

export default logger;


================================================
FILE: src/utils/schema.test.ts
================================================
import { PullConfig, pullConfigSchema } from "@/src/utils/schema.ts";
import { assertEquals } from "@std/assert";

// deno-fmt-ignore
const validConfigs = [
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master' }] },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', autoMerge: true }], label: 'pull' },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', autoMerge: true }], label: 'pull', conflictLabel: 'merge-conflict' },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', autoMerge: true, assignees: ['wei'] }] },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', autoMerge: false, reviewers: ['wei'] }] },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', autoMerge: false, reviewers: ['wei'], conflictReviewers: ['saurabh702'] }] },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', mergeMethod: 'squash', mergeUnstable: true }] },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', mergeMethod: 'hardreset', assignees: ['wei'] }] },
] as const;

// deno-fmt-ignore
const invalidConfigs = [
  {},
  { rules: {} },
  { version: '' },
  { version: '1' },
  { version: '1', rules: [] },
  { version: '1', rules: [{ base: 'master' }] },
  { version: 1, rules: [{ base: 'master', upstream: 'upstream:master' }] },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master' }], label: 1 },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master' }], label: 1, conflictLabel: 2 },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master' }], label: '', conflictLabel: '' },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master' }], label: 'pull', conflictLabel: 1 },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master' }], label: 'pull', conflictLabel: '' },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', assignees: '' }] },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', reviewers: '' }] },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', reviewers: '', conflictReviewers: '' }] },
  { version: '1', rules: [{ base: 'master', upstream: '' }] },
  { version: '1', rules: [{ base: 'master', autoMerge: 1 }] },
  { version: '1', rules: [{ base: 'master', autoMerge: '' }] },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', mergeMethod: '' }] },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', mergeMethod: 'invalid' }] },
  { version: '1', rules: [{ base: 'master', upstream: 'upstream:master', mergeMethod: true }] },
  { version: '2', rules: [{ base: 'master', upstream: 'upstream:master', mergeMethod: "hardreset" }] },
] as const;

Deno.test("schema defaults", () => {
  const result = pullConfigSchema.parse({
    version: "1",
    rules: [{ base: "master", upstream: "upstream:master" }],
  });

  const expected = {
    version: "1",
    rules: [
      {
        base: "master",
        upstream: "upstream:master",
        mergeMethod: "none",
        mergeUnstable: false,
        assignees: [],
        reviewers: [],
        conflictReviewers: [],
      },
    ],
    label: ":arrow_heading_down: pull",
    conflictLabel: "merge-conflict",
  } as PullConfig;
  assertEquals(result, expected);
});

for (const config of validConfigs) {
  Deno.test(`Valid config: ${JSON.stringify(config)}`, () => {
    const result = pullConfigSchema.safeParse(config);
    assertEquals(
      result.success,
      true,
      `Expected config to be valid, but got error: ${JSON.stringify(result)}`,
    );
  });
}

for (const config of invalidConfigs) {
  Deno.test(`Invalid config: ${JSON.stringify(config)}`, () => {
    const result = pullConfigSchema.safeParse(config);
    assertEquals(
      result.success,
      false,
      "Expected config to be invalid, but it was valid",
    );
  });
}


================================================
FILE: src/utils/schema.ts
================================================
import { z } from "zod";

const pullMergeMethodEnum = z.enum([
  "none",
  "merge",
  "squash",
  "rebase",
  "hardreset",
]);

const pullRuleSchema = z.object({
  base: z.string().min(1).describe("Destination local branch"),
  upstream: z.string().min(1).describe("Upstream owner:branch"),
  mergeMethod: pullMergeMethodEnum.default("none").describe(
    "Auto merge pull request using this merge method. one of [none, merge, squash, rebase, hardreset], Default: none",
  ),
  mergeUnstable: z.boolean().default(false).describe(
    "Merge pull request even when the mergeable state is not clean",
  ),
  assignees: z.array(z.string()).default([]).describe(
    "Assignees for the pull requests",
  ),
  reviewers: z.array(z.string()).default([]).describe(
    "Reviewers for the pull requests",
  ),
  conflictReviewers: z.array(z.string()).default([]).describe(
    "Merge Conflict Reviewers for the pull requests",
  ),
});

const pullConfigSchema = z.object({
  version: z.string().regex(/^1$/).describe(
    'Version number (string), must be "1"',
  ),
  rules: z.array(pullRuleSchema).min(1).describe("Rules for pull requests"),
  label: z.string().min(1).default(":arrow_heading_down: pull").describe(
    "Label for the pull requests",
  ),
  conflictLabel: z.string().min(1).default("merge-conflict").describe(
    "Label for merge conflicts",
  ),
});

// Export types derived from the schema
type PullConfig = z.infer<typeof pullConfigSchema>;
type PullRule = z.infer<typeof pullRuleSchema>;
type PullMergeMethod = z.infer<typeof pullMergeMethodEnum>;

export {
  type PullConfig,
  pullConfigSchema,
  type PullMergeMethod,
  type PullRule,
};


================================================
FILE: src/worker.ts
================================================
import { createSchedulerWorker } from "@wei/probot-scheduler";
import { Redis } from "ioredis";
import { appConfig } from "@/src/configs/app-config.ts";
import { getRepoProcessor } from "@/src/processor/index.ts";
import { createProbot } from "probot";

const probot = createProbot();
const RepoJobProcessor = getRepoProcessor(probot);

const redisClient = new Redis(appConfig.redisConfig!, {
  maxRetriesPerRequest: null,
  name: `${appConfig.appSlug}-worker`,
});

const MAX_RETAINED_JOBS = 1000;
const JOB_RETENTION_SECONDS = 3600; // 1 hour

const worker = createSchedulerWorker(
  RepoJobProcessor,
  {
    connection: redisClient,
    concurrency: 10,
    removeOnComplete: {
      count: MAX_RETAINED_JOBS,
      age: JOB_RETENTION_SECONDS,
    },
    removeOnFail: {
      count: MAX_RETAINED_JOBS,
      age: JOB_RETENTION_SECONDS,
    },
  },
);

worker.on("completed", (job) => {
  console.log(`Job ${job.id} completed successfully`);
});

worker.on("failed", (job, err) => {
  console.error(`Job ${job?.id} failed: ${err.message}`);
});

const gracefulShutdown = async (signal: string) => {
  console.log(`Received ${signal}, closing worker...`);
  await worker.close();
  try {
    await redisClient.quit();
  } catch {
    // ignore
  }
  Deno.exit(0);
};

Deno.addSignalListener("SIGINT", () => gracefulShutdown("SIGINT"));
Deno.addSignalListener("SIGTERM", () => gracefulShutdown("SIGTERM"));


================================================
FILE: static/hello.js
================================================
console.log("Hello, world!");
Download .txt
gitextract_6sspy7v9/

├── .devcontainer/
│   ├── Dockerfile
│   ├── pull-base/
│   │   ├── devcontainer.json
│   │   └── docker-compose.yml
│   └── pull-full/
│       ├── devcontainer.json
│       └── docker-compose.yml
├── .dockerignore
├── .github/
│   ├── CODE_OF_CONDUCT.md
│   ├── CONTRIBUTING.md
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   ├── pull.yml
│   └── workflows/
│       ├── auto-tag.yml
│       └── publish.yml
├── .gitignore
├── .hooks/
│   └── pre-commit
├── Dockerfile
├── LICENSE
├── README.md
├── _config.yml
├── deno.json
├── docker-compose.yml
├── scripts/
│   ├── full-sync.ts
│   └── manual-process.ts
├── src/
│   ├── app.ts
│   ├── configs/
│   │   ├── app-config.ts
│   │   ├── database.ts
│   │   └── redis.ts
│   ├── index.ts
│   ├── processor/
│   │   ├── index.ts
│   │   └── pull.ts
│   ├── router/
│   │   ├── index.ts
│   │   ├── repo-handler.ts
│   │   └── stats.ts
│   ├── utils/
│   │   ├── get-pull-config.ts
│   │   ├── get-repository-schedule.ts
│   │   ├── helpers.ts
│   │   ├── logger.ts
│   │   ├── schema.test.ts
│   │   └── schema.ts
│   └── worker.ts
└── static/
    └── hello.js
Download .txt
SYMBOL INDEX (36 symbols across 12 files)

FILE: scripts/full-sync.ts
  function main (line 9) | async function main() {

FILE: scripts/manual-process.ts
  function main (line 8) | async function main(full_name: string) {

FILE: src/configs/app-config.ts
  function getAppConfig (line 4) | function getAppConfig(env: Record<string, string> = Deno.env.toObject()) {

FILE: src/index.ts
  function handleAppTermination (line 56) | function handleAppTermination(signal: string) {

FILE: src/processor/index.ts
  constant TIMEOUT (line 8) | const TIMEOUT = 60 * 1000;
  function createTimeoutPromise (line 10) | function createTimeoutPromise(log: Logger) {
  function processRepo (line 19) | async function processRepo(
  function getRepoProcessor (line 36) | function getRepoProcessor(probot: Probot) {

FILE: src/processor/pull.ts
  type PullOptions (line 13) | interface PullOptions {
  type PullRequestData (line 19) | type PullRequestData =
  class Pull (line 22) | class Pull {
    method constructor (line 30) | constructor(
    method routineCheck (line 53) | async routineCheck(): Promise<void> {
    method checkAutoMerge (line 98) | private async checkAutoMerge(
    method handleMergeConflict (line 141) | private async handleMergeConflict(
    method processMerge (line 176) | private async processMerge(
    method getMergeableStatus (line 226) | private async getMergeableStatus(
    method hasDiff (line 245) | private async hasDiff(base: string, upstream: string): Promise<boolean> {
    method getOpenPR (line 282) | private async getOpenPR(base: string, head: string) {
    method createPR (line 327) | private async createPR(
    method isMergeable (line 392) | private async isMergeable(
    method addReviewers (line 425) | private async addReviewers(
    method addLabel (line 445) | private async addLabel(
    method mergePR (line 461) | private async mergePR(
    method hardResetCommit (line 475) | private async hardResetCommit(

FILE: src/router/repo-handler.ts
  function getRepoHandlers (line 11) | function getRepoHandlers(

FILE: src/router/stats.ts
  function getStatsHandlers (line 4) | function getStatsHandlers(_app: Probot) {

FILE: src/utils/get-pull-config.ts
  function getLivePullConfig (line 7) | async function getLivePullConfig(
  function getDefaultPullConfig (line 38) | function getDefaultPullConfig(
  function getPullConfig (line 78) | async function getPullConfig(

FILE: src/utils/get-repository-schedule.ts
  function getRepositorySchedule (line 9) | async function getRepositorySchedule(

FILE: src/utils/schema.ts
  type PullConfig (line 45) | type PullConfig = z.infer<typeof pullConfigSchema>;
  type PullRule (line 46) | type PullRule = z.infer<typeof pullRuleSchema>;
  type PullMergeMethod (line 47) | type PullMergeMethod = z.infer<typeof pullMergeMethodEnum>;

FILE: src/worker.ts
  constant MAX_RETAINED_JOBS (line 15) | const MAX_RETAINED_JOBS = 1000;
  constant JOB_RETENTION_SECONDS (line 16) | const JOB_RETENTION_SECONDS = 3600;
Condensed preview — 41 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (72K chars).
[
  {
    "path": ".devcontainer/Dockerfile",
    "chars": 201,
    "preview": "ARG VARIANT=ubuntu\r\nARG DENO_VERSION=2.3.7\r\n\r\nFROM denoland/deno:bin-${DENO_VERSION} AS deno\r\nFROM mcr.microsoft.com/vsc"
  },
  {
    "path": ".devcontainer/pull-base/devcontainer.json",
    "chars": 944,
    "preview": "{\n  \"name\": \"[Pull App] Base Dev Environment\",\n  \"dockerComposeFile\": [\n    \"docker-compose.yml\"\n  ],\n  \"service\": \"dev\""
  },
  {
    "path": ".devcontainer/pull-base/docker-compose.yml",
    "chars": 1249,
    "preview": "version: \"3.8\"\n\nservices:\n  dev:\n    build:\n      context: ..\n      dockerfile: Dockerfile\n    volumes:\n      - ../../.."
  },
  {
    "path": ".devcontainer/pull-full/devcontainer.json",
    "chars": 1067,
    "preview": "{\n  \"name\": \"[Pull App] Full Dev Environment (amd64)\",\n  \"dockerComposeFile\": [\n    \"../pull-base/docker-compose.yml\",\n "
  },
  {
    "path": ".devcontainer/pull-full/docker-compose.yml",
    "chars": 1213,
    "preview": "version: \"3.8\"\n\nservices:\n  mongo-express:\n    image: mongo-express\n    restart: unless-stopped\n    environment:\n      M"
  },
  {
    "path": ".dockerignore",
    "chars": 110,
    "preview": "node_modules\ncoverage\nnpm-debug.log\n*.pem\n.env\n.*\ndocker-compose.yml\nDockerfile\n*.md\n.gitignore\n.dockerignore\n"
  },
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "chars": 3231,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "chars": 6298,
    "preview": "# Contributing to Pull\n\nHi there! We're thrilled that you'd like to contribute to this project. Your\nhelp is essential f"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 12,
    "preview": "github: wei\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 571,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/pull.yml",
    "chars": 89,
    "preview": "version: \"1\"\nrules:\n  - base: master\n    upstream: wei:master\n    mergeMethod: hardreset\n"
  },
  {
    "path": ".github/workflows/auto-tag.yml",
    "chars": 1536,
    "preview": "name: Auto Tag Release on Version Change\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - master\n    paths:\n     "
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 1471,
    "preview": "name: Create and publish a Docker image\n\non:\n  workflow_dispatch:\n  pull_request:\n  push:\n    branches:\n      - \"master\""
  },
  {
    "path": ".gitignore",
    "chars": 47,
    "preview": "node_modules\ncoverage\nnpm-debug.log\n*.pem\n.env\n"
  },
  {
    "path": ".hooks/pre-commit",
    "chars": 68,
    "preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/hook.sh\"\n\ndeno task check\n"
  },
  {
    "path": "Dockerfile",
    "chars": 741,
    "preview": "ARG DENO_VERSION=2.3.7\nFROM denoland/deno:alpine-${DENO_VERSION}\n\nENV \\\n  ####################\n  ###   Required   ###\n  "
  },
  {
    "path": "LICENSE",
    "chars": 1097,
    "preview": "The MIT License\n\nCopyright (c) 2024 Wei He <https://wei.mit-license.org>\n\nPermission is hereby granted, free of charge, "
  },
  {
    "path": "README.md",
    "chars": 6030,
    "preview": "<!-- deno-fmt-ignore-start -->\n\n<div align=\"center\">\n\n<a href=\"https://wei.github.io/pull\">\n  <img src=\"https://prod.dow"
  },
  {
    "path": "_config.yml",
    "chars": 47,
    "preview": "theme: jekyll-theme-cayman\nplugins:\n  - jemoji\n"
  },
  {
    "path": "deno.json",
    "chars": 1305,
    "preview": "{\n  \"version\": \"2.0.0-alpha.4\",\n  \"tasks\": {\n    \"dev\": \"deno run --env-file --allow-all src/index.ts\",\n    \"dev:skip-fu"
  },
  {
    "path": "docker-compose.yml",
    "chars": 1912,
    "preview": "services:\n  app:\n    build: .\n    restart: unless-stopped\n    ports:\n      - \"3000:3000\"\n    env_file:\n      - ./.env\n  "
  },
  {
    "path": "scripts/full-sync.ts",
    "chars": 1098,
    "preview": "import { createProbot } from \"probot\";\nimport { fullSync } from \"@wei/probot-scheduler\";\nimport logger from \"@/src/utils"
  },
  {
    "path": "scripts/manual-process.ts",
    "chars": 1947,
    "preview": "import { connectMongoDB, disconnectMongoDB } from \"@/src/configs/database.ts\";\nimport { JobPriority, RepositoryModel } f"
  },
  {
    "path": "src/app.ts",
    "chars": 212,
    "preview": "import { Probot } from \"probot\";\nimport { createSchedulerApp, SchedulerAppOptions } from \"@wei/probot-scheduler\";\n\nexpor"
  },
  {
    "path": "src/configs/app-config.ts",
    "chars": 675,
    "preview": "import { readEnvOptions } from \"probot/lib/bin/read-env-options.js\";\nimport denoJson from \"@/deno.json\" with { type: \"js"
  },
  {
    "path": "src/configs/database.ts",
    "chars": 537,
    "preview": "import mongoose from \"mongoose\";\nimport { appConfig } from \"@/src/configs/app-config.ts\";\nimport log from \"@/src/utils/l"
  },
  {
    "path": "src/configs/redis.ts",
    "chars": 273,
    "preview": "import { appConfig } from \"@/src/configs/app-config.ts\";\nimport { Redis } from \"ioredis\";\n\nexport const getRedisClient ="
  },
  {
    "path": "src/index.ts",
    "chars": 1994,
    "preview": "import express from \"express\";\nimport { createNodeMiddleware, createProbot } from \"probot\";\nimport { createSchedulerServ"
  },
  {
    "path": "src/processor/index.ts",
    "chars": 1543,
    "preview": "import type { Job } from \"bullmq\";\nimport type { SchedulerJobData } from \"@wei/probot-scheduler\";\nimport type { Logger, "
  },
  {
    "path": "src/processor/pull.ts",
    "chars": 12651,
    "preview": "import { type Logger, ProbotOctokit } from \"probot\";\nimport pluralize from \"@wei/pluralize\";\nimport {\n  type PullConfig,"
  },
  {
    "path": "src/router/index.ts",
    "chars": 1263,
    "preview": "import type { Request, Response } from \"express\";\nimport type { Probot } from \"probot\";\nimport express from \"express\";\ni"
  },
  {
    "path": "src/router/repo-handler.ts",
    "chars": 2400,
    "preview": "import type { Request, Response } from \"express\";\nimport type { Probot } from \"probot\";\nimport { appConfig } from \"@/src"
  },
  {
    "path": "src/router/stats.ts",
    "chars": 442,
    "preview": "import type { Request, Response } from \"express\";\nimport { Probot } from \"probot\";\n\nfunction getStatsHandlers(_app: Prob"
  },
  {
    "path": "src/utils/get-pull-config.ts",
    "chars": 2696,
    "preview": "import type { Logger, ProbotOctokit } from \"probot\";\nimport { appConfig } from \"@/src/configs/app-config.ts\";\nimport { S"
  },
  {
    "path": "src/utils/get-repository-schedule.ts",
    "chars": 539,
    "preview": "import {\n  JobPriority,\n  type RepositoryMetadataSchemaType,\n  type RepositorySchemaType,\n} from \"@wei/probot-scheduler\""
  },
  {
    "path": "src/utils/helpers.ts",
    "chars": 1235,
    "preview": "import { appConfig } from \"@/src/configs/app-config.ts\";\n\nexport const getRandomCronSchedule = () => {\n  // Every 6 hour"
  },
  {
    "path": "src/utils/logger.ts",
    "chars": 1010,
    "preview": "import { appConfig } from \"@/src/configs/app-config.ts\";\nimport { pino } from \"pino\";\nimport { getTransformStream, type "
  },
  {
    "path": "src/utils/schema.test.ts",
    "chars": 3979,
    "preview": "import { PullConfig, pullConfigSchema } from \"@/src/utils/schema.ts\";\nimport { assertEquals } from \"@std/assert\";\n\n// de"
  },
  {
    "path": "src/utils/schema.ts",
    "chars": 1657,
    "preview": "import { z } from \"zod\";\n\nconst pullMergeMethodEnum = z.enum([\n  \"none\",\n  \"merge\",\n  \"squash\",\n  \"rebase\",\n  \"hardreset"
  },
  {
    "path": "src/worker.ts",
    "chars": 1409,
    "preview": "import { createSchedulerWorker } from \"@wei/probot-scheduler\";\nimport { Redis } from \"ioredis\";\nimport { appConfig } fro"
  },
  {
    "path": "static/hello.js",
    "chars": 30,
    "preview": "console.log(\"Hello, world!\");\n"
  }
]

About this extraction

This page contains the full source code of the wei/pull GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 41 files (65.3 KB), approximately 17.9k tokens, and a symbol index with 36 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!