diff --git a/.github/workflows/mypy_primer.yml b/.github/workflows/mypy_primer.yml new file mode 100644 index 000000000000..08e4b117d3c8 --- /dev/null +++ b/.github/workflows/mypy_primer.yml @@ -0,0 +1,99 @@ +name: Run mypy_primer + +on: + # Only run on PR, since we diff against main + pull_request: + paths: + - "**/*.pyi" + - ".github/workflows/mypy_primer.yml" + - ".github/workflows/mypy_primer_comment.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + mypy_primer: + name: Run + runs-on: ubuntu-latest + strategy: + matrix: + shard-index: [0] # e.g. change this to [0, 1, 2] and --num-shards below to 3 + fail-fast: false + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + path: numpy_to_test + fetch-depth: 0 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + with: + python-version: "3.12" + - name: Install dependencies + run: pip install git+https://github.com/hauntsaninja/mypy_primer.git + - name: Run mypy_primer + shell: bash + run: | + cd numpy_to_test + MYPY_VERSION=$(grep mypy== requirements/test_requirements.txt | sed -n 's/mypy==\([^;]*\).*/\1/p') + + echo "new commit" + git checkout $GITHUB_SHA + git rev-list --format=%s --max-count=1 HEAD + + MERGE_BASE=$(git merge-base $GITHUB_SHA origin/$GITHUB_BASE_REF) + git worktree add ../numpy_base $MERGE_BASE + cd ../numpy_base + + echo "base commit" + git rev-list --format=%s --max-count=1 HEAD + + echo '' + cd .. + # fail action if exit code isn't zero or one + # TODO: note that we don't build numpy, so if a project attempts to use the + # numpy mypy plugin, we may see some issues involving version skew. + ( + mypy_primer \ + --new v${MYPY_VERSION} --old v${MYPY_VERSION} \ + --known-dependency-selector numpy \ + --old-prepend-path numpy_base --new-prepend-path numpy_to_test \ + --num-shards 1 --shard-index ${{ matrix.shard-index }} \ + --debug \ + --output concise \ + | tee diff_${{ matrix.shard-index }}.txt + ) || [ $? -eq 1 ] + - if: ${{ matrix.shard-index == 0 }} + name: Save PR number + run: | + echo ${{ github.event.pull_request.number }} | tee pr_number.txt + - name: Upload mypy_primer diff + PR number + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ matrix.shard-index == 0 }} + with: + name: mypy_primer_diffs-${{ matrix.shard-index }} + path: | + diff_${{ matrix.shard-index }}.txt + pr_number.txt + - name: Upload mypy_primer diff + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: ${{ matrix.shard-index != 0 }} + with: + name: mypy_primer_diffs-${{ matrix.shard-index }} + path: diff_${{ matrix.shard-index }}.txt + + join_artifacts: + name: Join artifacts + runs-on: ubuntu-latest + needs: [mypy_primer] + permissions: + contents: read + steps: + - name: Merge artifacts + uses: actions/upload-artifact/merge@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + with: + name: mypy_primer_diffs + pattern: mypy_primer_diffs-* + delete-merged: true diff --git a/.github/workflows/mypy_primer_comment.yml b/.github/workflows/mypy_primer_comment.yml new file mode 100644 index 000000000000..e484589ad446 --- /dev/null +++ b/.github/workflows/mypy_primer_comment.yml @@ -0,0 +1,96 @@ +name: Comment with mypy_primer diff + +on: + workflow_run: + workflows: + - Run mypy_primer + types: + - completed + +permissions: + contents: read + pull-requests: write + +jobs: + comment: + name: Comment PR from mypy_primer + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Hide old comments + uses: kanga333/comment-hider@c12bb20b48aeb8fc098e35967de8d4f8018fffdf # v0.4.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + issue_number: ${{ steps.post-comment.outputs.result }} + + - name: Download diffs + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const fs = require('fs'); + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{ github.event.workflow_run.id }}, + }); + const [matchArtifact] = artifacts.data.artifacts.filter((artifact) => + artifact.name == "mypy_primer_diffs"); + + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: "zip", + }); + fs.writeFileSync("diff.zip", Buffer.from(download.data)); + + - run: unzip diff.zip + - run: | + cat diff_*.txt | tee fulldiff.txt + + - name: Post comment + id: post-comment + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const MAX_CHARACTERS = 50000 + const MAX_CHARACTERS_PER_PROJECT = MAX_CHARACTERS / 3 + + const fs = require('fs') + let data = fs.readFileSync('fulldiff.txt', { encoding: 'utf8' }) + + function truncateIfNeeded(original, maxLength) { + if (original.length <= maxLength) { + return original + } + let truncated = original.substring(0, maxLength) + // further, remove last line that might be truncated + truncated = truncated.substring(0, truncated.lastIndexOf('\n')) + let lines_truncated = original.split('\n').length - truncated.split('\n').length + return `${truncated}\n\n... (truncated ${lines_truncated} lines) ...` + } + + const projects = data.split('\n\n') + // don't let one project dominate + data = projects.map(project => truncateIfNeeded(project, MAX_CHARACTERS_PER_PROJECT)).join('\n\n') + // posting comment fails if too long, so truncate + data = truncateIfNeeded(data, MAX_CHARACTERS) + + console.log("Diff from mypy_primer:") + console.log(data) + + let body + if (data.trim()) { + body = 'Diff from [mypy_primer](https://github.com/hauntsaninja/mypy_primer), ' + body += 'showing the effect of this PR on type check results on a corpus of open source code:\n```diff\n' + body += data + '```' + const prNumber = parseInt(fs.readFileSync("pr_number.txt", { encoding: "utf8" })) + await github.rest.issues.createComment({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + body + }) + } + return prNumber