name: Sync typeshed

# How this works:
#
# 1. A Linux worker:
#    a. Checks out Ruff and typeshed
#    b. Deletes the vendored typeshed stdlib stubs from Ruff
#    c. Copies the latest versions of the stubs from typeshed
#    d. Uses docstring-adder to sync all docstrings available on Linux
#    e. Creates a new branch on the upstream astral-sh/ruff repository
#    f. Commits the changes it's made and pushes them to the new upstream branch
# 2. Once the Linux worker is done, a Windows worker:
#    a. Checks out the branch created by the Linux worker
#    b. Syncs all docstrings available on Windows that are not available on Linux
#    c. Commits the changes and pushes them to the same upstream branch
# 3. Once the Windows worker is done, a MacOS worker:
#    a. Checks out the branch created by the Linux worker
#    b. Syncs all docstrings available on MacOS that are not available on Linux or Windows
#    c. Commits the changes and pushes them to the same upstream branch
#    d. Creates a PR against the `main` branch using the branch all three workers have pushed to
# 4. If any of steps 1-3 failed, an issue is created in the `astral-sh/ruff` repository

on:
  workflow_dispatch:
  schedule:
    # Run on the 1st and the 15th of every month:
    - cron: "0 0 1,15 * *"

env:
  FORCE_COLOR: 1
  GH_TOKEN: ${{ github.token }}

  # The name of the upstream branch that the first worker creates,
  # and which all three workers push to.
  UPSTREAM_BRANCH: typeshedbot/sync-typeshed

jobs:
  # Sync typeshed stubs, and sync all docstrings available on Linux.
  # Push the changes to a new branch on the upstream repository.
  sync:
    name: Sync typeshed
    runs-on: ubuntu-latest
    timeout-minutes: 20
    # Don't run the cron job on forks:
    if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        name: Checkout Ruff
        with:
          path: ruff
          persist-credentials: true
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        name: Checkout typeshed
        with:
          repository: python/typeshed
          path: typeshed
          persist-credentials: false
      - name: Setup git
        run: |
          git config --global user.name typeshedbot
          git config --global user.email '<>'
      - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
      - name: Sync typeshed stubs
        run: |
          rm -rf ruff/crates/ty_vendored/vendor/typeshed
          mkdir ruff/crates/ty_vendored/vendor/typeshed
          cp typeshed/README.md ruff/crates/ty_vendored/vendor/typeshed
          cp typeshed/LICENSE ruff/crates/ty_vendored/vendor/typeshed

          # The pyproject.toml file is needed by a later job for the black configuration.
          # It's deleted before creating the PR.
          cp typeshed/pyproject.toml ruff/crates/ty_vendored/vendor/typeshed

          cp -r typeshed/stdlib ruff/crates/ty_vendored/vendor/typeshed/stdlib
          rm -rf ruff/crates/ty_vendored/vendor/typeshed/stdlib/@tests
          git -C typeshed rev-parse HEAD > ruff/crates/ty_vendored/vendor/typeshed/source_commit.txt
          cd ruff
          git checkout -b typeshedbot/sync-typeshed
          git add .
          git commit -m "Sync typeshed. Source commit: https://github.com/python/typeshed/commit/$(git -C ../typeshed rev-parse HEAD)" --allow-empty
      - name: Sync Linux docstrings
        if: ${{ success() }}
        run: |
          cd ruff
          ./scripts/codemod_docstrings.sh
          git commit -am "Sync Linux docstrings" --allow-empty
      - name: Push the changes
        id: commit
        if: ${{ success() }}
        run: git -C ruff push --force --set-upstream origin "${UPSTREAM_BRANCH}"

  # Checkout the branch created by the sync job,
  # and sync all docstrings available on Windows that are not available on Linux.
  # Commit the changes and push them to the same branch.
  docstrings-windows:
    runs-on: windows-latest
    timeout-minutes: 20
    needs: [sync]

    # Don't run the cron job on forks.
    # The job will also be skipped if the sync job failed, because it's specified in `needs` above,
    # and we haven't used `always()` in the `if` condition here
    # (https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#example-requiring-successful-dependent-jobs)
    if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}

    permissions:
      contents: write
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        name: Checkout Ruff
        with:
          persist-credentials: true
          ref: ${{ env.UPSTREAM_BRANCH}}
      - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
      - name: Setup git
        run: |
          git config --global user.name typeshedbot
          git config --global user.email '<>'
      - name: Sync Windows docstrings
        id: docstrings
        shell: bash
        run: ./scripts/codemod_docstrings.sh
      - name: Commit the changes
        if: ${{ steps.docstrings.outcome == 'success' }}
        run: |
          git commit -am "Sync Windows docstrings" --allow-empty
          git push

  # Checkout the branch created by the sync job,
  # and sync all docstrings available on macOS that are not available on Linux or Windows.
  # Push the changes to the same branch and create a PR against the `main` branch using that branch.
  docstrings-macos-and-pr:
    runs-on: macos-latest
    timeout-minutes: 20
    needs: [sync, docstrings-windows]

    # Don't run the cron job on forks.
    # The job will also be skipped if the sync or docstrings-windows jobs failed,
    # because they're specified in `needs` above and we haven't used an `always()` condition in the `if` here
    # (https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#example-requiring-successful-dependent-jobs)
    if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}

    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        name: Checkout Ruff
        with:
          persist-credentials: true
          ref: ${{ env.UPSTREAM_BRANCH}}
      - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
      - name: Setup git
        run: |
          git config --global user.name typeshedbot
          git config --global user.email '<>'
      - name: Sync macOS docstrings
        run: ./scripts/codemod_docstrings.sh
      - name: Commit and push the changes
        if: ${{ success() }}
        run: |
          git commit -am "Sync macOS docstrings" --allow-empty

          # Here we just reformat the codemodded stubs so that they are
          # consistent with the other typeshed stubs around them.
          # Typeshed formats code using black in their CI, so we just invoke
          # black on the stubs the same way that typeshed does.
          uvx black crates/ty_vendored/vendor/typeshed/stdlib --config crates/ty_vendored/vendor/typeshed/pyproject.toml || true
          git commit -am "Format codemodded docstrings" --allow-empty

          rm crates/ty_vendored/vendor/typeshed/pyproject.toml
          git commit -am "Remove pyproject.toml file"

          git push
      - name: Create a PR
        if: ${{ success() }}
        run: |
          gh pr list --repo "$GITHUB_REPOSITORY" --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
          gh pr create --title "[ty] Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "ty"

  create-issue-on-failure:
    name: Create an issue if the typeshed sync failed
    runs-on: ubuntu-latest
    needs: [sync, docstrings-windows, docstrings-macos-and-pr]
    if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && (needs.sync.result == 'failure' || needs.docstrings-windows.result == 'failure' || needs.docstrings-macos-and-pr.result == 'failure') }}
    permissions:
      issues: write
    steps:
      - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            await github.rest.issues.create({
              owner: "astral-sh",
              repo: "ruff",
              title: `Automated typeshed sync failed on ${new Date().toDateString()}`,
              body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
              labels: ["bug", "ty"],
            })
