{"id":20954573,"url":"https://github.com/duolingo/pulldozer","last_synced_at":"2025-10-11T20:12:10.266Z","repository":{"id":43007396,"uuid":"179922084","full_name":"duolingo/pulldozer","owner":"duolingo","description":"CLI tool for batch editing multiple repos :tractor:","archived":false,"fork":false,"pushed_at":"2024-12-10T17:04:55.000Z","size":205,"stargazers_count":51,"open_issues_count":2,"forks_count":5,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-06-06T12:05:54.375Z","etag":null,"topics":["bash","cli","codemod","git","github","pr"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/duolingo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2019-04-07T05:27:15.000Z","updated_at":"2025-04-17T20:50:30.000Z","dependencies_parsed_at":"2024-05-31T03:44:40.912Z","dependency_job_id":"d3cbd7ef-9996-4360-b2fd-420b09112885","html_url":"https://github.com/duolingo/pulldozer","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/duolingo/pulldozer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/duolingo%2Fpulldozer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/duolingo%2Fpulldozer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/duolingo%2Fpulldozer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/duolingo%2Fpulldozer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/duolingo","download_url":"https://codeload.github.com/duolingo/pulldozer/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/duolingo%2Fpulldozer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279008579,"owners_count":26084480,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-11T02:00:06.511Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bash","cli","codemod","git","github","pr"],"created_at":"2024-11-19T01:15:13.928Z","updated_at":"2025-10-11T20:12:10.261Z","avatar_url":"https://github.com/duolingo.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Pulldozer\n\nPulldozer is a simple CLI tool for batch editing multiple GitHub repos.\n\nYou give Pulldozer a transformation script and it spits out pull requests. [Duolingo](https://www.duolingo.com/) has used Pulldozer to create well over 9000 PRs to date!\n\n## Usage\n\nClone this repo onto any Unix machine that has [`curl`](https://formulae.brew.sh/formula/curl) and [`jq`](https://formulae.brew.sh/formula/jq). Set your `GITHUB_TOKEN` environment variable to an [access token](https://github.com/settings/tokens) with `repo` scope and [SSO enabled](https://docs.github.com/en/github/authenticating-to-github/authorizing-a-personal-access-token-for-use-with-saml-single-sign-on). To perform a batch edit (a.k.a. _codemod_):\n\n1.  Create a script file that defines a `COMMIT_MESSAGE` string, `transform` function, and `REPOS` list.\n\n    \u003cdetails\u003e\u003csummary\u003eClick here to see a Shell example\u003c/summary\u003e\n\n    ```sh\n    COMMIT_MESSAGE='Fix \"langauge\" typos'\n\n    transform() {\n      # Write your transformation logic inside this function. GitHub org name\n      # and repo name are passed into this `transform` function as vars $1 and\n      # $2, respectively.\n      echo \"Hello world from $1/$2\" \u003e README.md\n\n      # Pulldozer provides a `replace_all` helper function for replacing text\n      # across all repo files. It's basically glorified sed.\n      replace_all 'langauge' 'language'\n\n      # Advanced `replace_all` example: regex, capture grouping, multi-line\n      # matching, and file path filtering\n      replace_all '(\\nprotobuf==)\\S+' '\\13.19.4' 'requirements\\.(in|txt)$'\n    }\n\n    REPOS='\n    artnc/dotfiles\n    duolingo/halflife-regression\n    duolingo/rtl-viewpager\n    '\n\n    # Optional but recommended: Markdown to include in pull request descriptions\n    DESCRIPTION='[Correct spelling](https://en.wiktionary.org/wiki/language)'\n    # Optional: Markdown that will be posted as a comment on PRs\n    COMMENT='This is a pull request comment'\n    # Optional: Declare this variable to enable GitHub auto-merge on PRs\n    AUTOMERGE=1\n    ```\n\n    By default, Pulldozer will interpret your script as POSIX shell. If you want to use Bash instead, just run `bash ./pulldozer` instead of `./pulldozer` during step 2 below.\n\n    \u003c/details\u003e\n    \u003cdetails\u003e\u003csummary\u003eClick here to see a JavaScript example\u003c/summary\u003e\n\n    ```js\n    const COMMIT_MESSAGE = 'Fix \"langauge\" typos';\n\n    // Write your transformation logic inside this function. GitHub org name and\n    // repo name are passed in as parameters.\n    const transform = async (org, repo) =\u003e {\n      const { writeFile } = require(\"fs\").promises;\n      await writeFile(\"README.md\", `Hello world from ${org}/${repo}`);\n\n      // Pulldozer provides a `replace_all` helper function (no need to import)\n      // for replacing text across all repo files. This helper is easier than\n      // traversing the repo yourself, and it also respects .gitignore.\n      await replace_all(\"langauge\", \"language\");\n\n      // Advanced `replace_all` example: regex, capture grouping, multiline\n      // matching, editing only a subset of repo files (optional third param)\n      await replace_all(\n        /(\\nprotobuf==)\\S+/,\n        \"$13.19.4\",\n        /requirements\\.(in|txt)$/,\n      );\n    };\n\n    const REPOS = [\n      \"artnc/dotfiles\",\n      \"duolingo/halflife-regression\",\n      \"duolingo/rtl-viewpager\",\n    ];\n\n    // Optional but recommended: Markdown to include in pull request descriptions\n    const DESCRIPTION =\n      \"[Correct spelling](https://en.wiktionary.org/wiki/language)\";\n    // Optional: Markdown that will be posted as a comment on PRs\n    const COMMENT = \"This is a pull request comment\";\n    // Optional: Declare this variable to enable GitHub auto-merge on PRs\n    const AUTOMERGE = 1;\n    ```\n\n    \u003c/details\u003e\n    \u003cdetails\u003e\u003csummary\u003eClick here to see a Python example\u003c/summary\u003e\n\n    ```py\n    COMMIT_MESSAGE = 'Fix \"langauge\" typos'\n\n    # Write your transformation logic inside this function. GitHub org name and\n    # repo name are passed in as parameters.\n    def transform(org, repo):\n        with open(\"README.md\", \"w\") as f:\n            f.write(f\"Hello world from {org}/{repo}\")\n\n        # Pulldozer provides a `replace_all` helper function (no need to import)\n        # for replacing text across all repo files. This helper is easier than\n        # using `os.walk` and `re.sub`, and it also respects .gitignore.\n        replace_all(\"langauge\", \"language\")\n\n        # Advanced `replace_all` example: regex, capture grouping, multiline\n        # matching, editing only a subset of repo files (optional third param)\n        replace_all(r\"(\\nprotobuf==)\\S+\", r\"\\13.19.4\", r\"requirements\\.(in|txt)$\")\n\n    REPOS = [\n        \"artnc/dotfiles\",\n        \"duolingo/halflife-regression\",\n        \"duolingo/rtl-viewpager\",\n    ]\n\n    # Optional but recommended: Markdown to include in pull request descriptions\n    DESCRIPTION = \"[Correct spelling](https://en.wiktionary.org/wiki/language)\"\n    # Optional: Markdown that will be posted as a comment on PRs\n    COMMENT = \"This is a pull request comment\"\n    # Optional: Declare this variable to enable GitHub auto-merge on PRs\n    AUTOMERGE = 1\n    ```\n\n    \u003c/details\u003e\n\n1.  Run `./pulldozer /path/to/your/script`. Pulldozer will generate a preview diff and ask for confirmation before creating PRs.\n\n## Demo video\n\n![Recording](demo.gif)\n\n_Duolingo is hiring! Apply at https://www.duolingo.com/careers_\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fduolingo%2Fpulldozer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fduolingo%2Fpulldozer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fduolingo%2Fpulldozer/lists"}