chore: make pr-pinger a workspace extension (#312757)

* chore: make pr-pinger a workspace extension

* Update .vscode/extensions/vscode-pr-pinger/src/extension.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Johannes Rieken
2026-04-27 16:09:20 +02:00
committed by GitHub
parent dd05fac328
commit 7af2c643af
8 changed files with 573 additions and 1 deletions

View File

@@ -7,7 +7,6 @@
"github.vscode-pull-request-github",
"ms-vscode.vscode-github-issue-notebooks",
"ms-vscode.extension-test-runner",
"jrieken.vscode-pr-pinger",
"typescriptteam.native-preview",
"ms-vscode.ts-customized-language-service"
]

View File

@@ -0,0 +1,5 @@
.gitignore
.vscode/**
.vscode-test/**
src/**
**/tsconfig.json

269
.vscode/extensions/vscode-pr-pinger/package-lock.json generated vendored Normal file
View File

@@ -0,0 +1,269 @@
{
"name": "vscode-pr-pinger",
"version": "0.0.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "vscode-pr-pinger",
"version": "0.0.6",
"license": "MIT",
"dependencies": {
"@octokit/graphql": "^5.0.1"
},
"engines": {
"vscode": "^1.67.0"
}
},
"node_modules/@octokit/endpoint": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.2.tgz",
"integrity": "sha512-8/AUACfE9vpRpehE6ZLfEtzkibe5nfsSwFZVMsG8qabqRt1M81qZYUFRZa1B8w8lP6cdfDJfRq9HWS+MbmR7tw==",
"dependencies": {
"@octokit/types": "^7.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/graphql": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.1.tgz",
"integrity": "sha512-sxmnewSwAixkP1TrLdE6yRG53eEhHhDTYUykUwdV9x8f91WcbhunIHk9x1PZLALdBZKRPUO2HRcm4kezZ79HoA==",
"dependencies": {
"@octokit/request": "^6.0.0",
"@octokit/types": "^7.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/openapi-types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.10.0.tgz",
"integrity": "sha512-wPQDpTyy35D6VS/lekXDaKcxy6LI2hzcbmXBnP180Pdgz3dXRzoHdav0w09yZzzWX8HHLGuqwAeyMqEPtWY2XA=="
},
"node_modules/@octokit/request": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.1.tgz",
"integrity": "sha512-gYKRCia3cpajRzDSU+3pt1q2OcuC6PK8PmFIyxZDWCzRXRSIBH8jXjFJ8ZceoygBIm0KsEUg4x1+XcYBz7dHPQ==",
"dependencies": {
"@octokit/endpoint": "^7.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^7.0.0",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/request-error": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.1.tgz",
"integrity": "sha512-ym4Bp0HTP7F3VFssV88WD1ZyCIRoE8H35pXSKwLeMizcdZAYc/t6N9X9Yr9n6t3aG9IH75XDnZ6UeZph0vHMWQ==",
"dependencies": {
"@octokit/types": "^7.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/types": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.4.0.tgz",
"integrity": "sha512-ln1GW0p72+P8qeRjHGj0wyR5ePy6slqvczveOqonMk1w1UWua6UgrkwFzv6S0vKWjSqt/ijYXF1ehNVRRRSvLA==",
"dependencies": {
"@octokit/openapi-types": "^13.10.0"
}
},
"node_modules/deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
},
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}
},
"dependencies": {
"@octokit/endpoint": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.2.tgz",
"integrity": "sha512-8/AUACfE9vpRpehE6ZLfEtzkibe5nfsSwFZVMsG8qabqRt1M81qZYUFRZa1B8w8lP6cdfDJfRq9HWS+MbmR7tw==",
"requires": {
"@octokit/types": "^7.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/graphql": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.1.tgz",
"integrity": "sha512-sxmnewSwAixkP1TrLdE6yRG53eEhHhDTYUykUwdV9x8f91WcbhunIHk9x1PZLALdBZKRPUO2HRcm4kezZ79HoA==",
"requires": {
"@octokit/request": "^6.0.0",
"@octokit/types": "^7.0.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/openapi-types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.10.0.tgz",
"integrity": "sha512-wPQDpTyy35D6VS/lekXDaKcxy6LI2hzcbmXBnP180Pdgz3dXRzoHdav0w09yZzzWX8HHLGuqwAeyMqEPtWY2XA=="
},
"@octokit/request": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.1.tgz",
"integrity": "sha512-gYKRCia3cpajRzDSU+3pt1q2OcuC6PK8PmFIyxZDWCzRXRSIBH8jXjFJ8ZceoygBIm0KsEUg4x1+XcYBz7dHPQ==",
"requires": {
"@octokit/endpoint": "^7.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^7.0.0",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/request-error": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.1.tgz",
"integrity": "sha512-ym4Bp0HTP7F3VFssV88WD1ZyCIRoE8H35pXSKwLeMizcdZAYc/t6N9X9Yr9n6t3aG9IH75XDnZ6UeZph0vHMWQ==",
"requires": {
"@octokit/types": "^7.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
},
"@octokit/types": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.4.0.tgz",
"integrity": "sha512-ln1GW0p72+P8qeRjHGj0wyR5ePy6slqvczveOqonMk1w1UWua6UgrkwFzv6S0vKWjSqt/ijYXF1ehNVRRRSvLA==",
"requires": {
"@octokit/openapi-types": "^13.10.0"
}
},
"deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
},
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
},
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"requires": {
"wrappy": "1"
}
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}
}
}

View File

@@ -0,0 +1,42 @@
{
"name": "vscode-pr-pinger",
"displayName": "VSCode PR Pinger",
"description": "PR Pinger shows a PR in the statusbar for you to review (VS Code Team Only)",
"engines": {
"vscode": "^1.67.0"
},
"version": "0.0.6",
"publisher": "ms-vscode",
"repository": {
"type": "git",
"url": "https://github.com/microsoft/vscode.git"
},
"main": "out/extension.js",
"activationEvents": [
"onStartupFinished"
],
"contributes": {
"configuration": {
"title": "PR Pinger",
"properties": {
"prpinger.presentation": {
"type": "string",
"default": "short",
"enum": [
"number",
"short"
],
"description": "How PR are shown in the status bar - either as number or without vowels"
}
}
}
},
"scripts": {
"watch": "gulp watch-extension:vscode-pr-pinger",
"compile": "gulp compile-extension:vscode-pr-pinger"
},
"license": "MIT",
"dependencies": {
"@octokit/graphql": "^5.0.1"
}
}

View File

@@ -0,0 +1,241 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { graphql } from '@octokit/graphql';
export function activate(context: vscode.ExtensionContext) {
const item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1);
context.subscriptions.push(item);
let session: vscode.AuthenticationSession | undefined;
context.subscriptions.push(vscode.commands.registerCommand('pr.promptLogin', async () => {
session = await vscode.authentication.getSession('github', ['repo'], { createIfNone: true });
updateItem();
}));
context.subscriptions.push(vscode.commands.registerCommand('pr.show', async (pr: PrInfo) => {
vscode.env.openExternal(vscode.Uri.parse(pr.url));
item.hide();
}));
let currentShow: vscode.Disposable | undefined = undefined;
context.subscriptions.push(new vscode.Disposable(() => currentShow?.dispose()));
// monitor focus/unfocus to "nudge harder" after a context switch
let lastGone: number | undefined;
context.subscriptions.push(vscode.window.onDidChangeWindowState(e => {
if (!e.focused) {
// gone
lastGone = Date.now();
return;
}
if (lastGone && Date.now() - lastGone > 1000 * 60 * 5) {
// back after 5 minutes
updateItem(true);
}
}));
async function updateItem(afterAway?: boolean) {
const currentSession = session;
if (!currentSession) {
item.text = 'Not Logged In';
item.command = 'pr.promptLogin';
item.show();
return;
}
if (currentShow) {
// already showing a PR
return;
}
const tooLongAgo = Date.now() - 1000 * 60 * 60 * 24 * 4;
const data: Query = await graphql(query, { headers: { authorization: `Bearer ${currentSession.accessToken}` } });
const prs = data.repository.pullRequests.edges.map(edge => edge.node)
.filter(needsTeamReview) // from team
.filter(pr => pr.author.login !== currentSession.account.label) // not YOU
.map(pr => ({ pr, date: new Date(pr.createdAt) }))
.filter(({ date }) => date.getTime() > tooLongAgo) // not dated
.sort((a, b) => b.date.getTime() - a.date.getTime()); // sorted
if (prs.length === 0) {
return;
}
// Unless forced you only need to review with a certain chance
if (!afterAway) {
const chance = Math.ceil(prs.length / 7);
if (Math.random() > chance) {
return;
}
}
// pick a random PR so that not everyone gets the same one
const n = Math.floor(Math.random() * prs.length);
const pr = prs[n].pr;
item.text = makeLabel(pr);
item.tooltip = new vscode.MarkdownString(`[${pr.title}](${pr.url}) needs your review. Thanks $(heart-filled)`, true);
item.command = { command: 'pr.show', title: 'Show PR', arguments: [pr] };
item.backgroundColor = afterAway ? new vscode.ThemeColor('statusBarItem.warningBackground') : undefined;
item.show();
// refresh every 20 seconds
const checkHandle = setInterval(async function () {
const data: Check = await graphql(check, {
pr: pr.number,
headers: { authorization: `Bearer ${currentSession.accessToken}` },
});
if (!needsTeamReview(data.repository.pullRequest)) {
currentShow?.dispose();
}
}, 1000 * 20);
currentShow = new vscode.Disposable(() => {
clearInterval(checkHandle);
currentShow = undefined;
item.hide();
});
}
vscode.authentication.getSession('github', ['repo']).then(_session => {
session = _session;
updateItem();
});
const handle = setInterval(updateItem, 1000 * 60 * 10); // update every 10 minutes
context.subscriptions.push(new vscode.Disposable(() => clearInterval(handle)));
}
function makeLabel(pr: PrInfo): string {
const style = vscode.workspace.getConfiguration('prpinger').get<'short' | 'number'>('presentation');
if (style === 'number') {
return `$(git-pull-request) #${pr.number}`;
}
const ignore = new Set(['`', '.', ':']);
const vowels = new Set(['a', 'e', 'i', 'o', 'u']);
const newTitle: string[] = [];
for (const ch of pr.title) {
if (ignore.has(ch)) {
break;
}
if (newTitle.length > 8 && !/\w/.test(ch)) {
break;
}
if (newTitle.length > 15) {
break;
}
if (!vowels.has(ch)) {
newTitle.push(ch);
}
}
return `$(git-pull-request) ${newTitle.join('')}`;
}
function needsTeamReview(pr: PrInfo): boolean {
return pr.authorAssociation === 'MEMBER'
&& !pr.isDraft
&& pr.reviewRequests.totalCount === 0
&& pr.reviews.totalCount === 0
&& pr.assignees.nodes.length === 1 && pr.assignees.nodes[0].login === pr.author.login; // our PR bot assigns the poster as owner -> we use that as filter
}
type PrInfo = {
number: number;
author: { login: string };
authorAssociation: 'MEMBER' | string;
assignees: { nodes: { login: string }[] };
createdAt: string;
isDraft: string;
reviewRequests: { totalCount: number };
reviews: { totalCount: number };
title: string;
url: string;
};
// ---- query
type Query = {
repository: {
pullRequests: {
edges: { node: PrInfo }[];
};
};
};
const query = `{
repository(owner: "microsoft", name: "vscode") {
pullRequests(
first: 30
states: OPEN
orderBy: {field: CREATED_AT, direction: DESC}
) {
edges {
node {
title
number
url
createdAt
authorAssociation
author {
login
}
assignees(first:5) {
nodes {
login
}
}
isDraft
reviewRequests(last: 1) {
totalCount
}
reviews(last:1) {
totalCount
}
}
}
}
}
}`;
// --- check
type Check = {
repository: {
pullRequest: PrInfo;
};
};
const check = `query validate($pr: Int!) {
repository(owner: "microsoft", name: "vscode") {
pullRequest(number: $pr) {
authorAssociation
author {
login
}
assignees(first:5) {
nodes {
login
}
}
isDraft
reviewRequests(last: 1) {
totalCount
}
reviews(last: 1) {
totalCount
}
}
}
}`;

View File

@@ -0,0 +1,14 @@
{
"extends": "../../../extensions/tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./out",
"types": [
"node"
]
},
"include": [
"src/**/*",
"../../../src/vscode-dts/vscode.d.ts"
]
}

View File

@@ -97,6 +97,7 @@ const compilations = [
'.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json',
'.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json',
'.vscode/extensions/vscode-extras/tsconfig.json',
'.vscode/extensions/vscode-pr-pinger/tsconfig.json',
];
const getBaseUrl = (out: string) => `https://main.vscode-cdn.net/sourcemaps/${commit}/${out}`;

View File

@@ -63,6 +63,7 @@ export const dirs = [
'.vscode/extensions/vscode-selfhost-import-aid',
'.vscode/extensions/vscode-selfhost-test-provider',
'.vscode/extensions/vscode-extras',
'.vscode/extensions/vscode-pr-pinger',
];
if (existsSync(`${import.meta.dirname}/../../.build/distro/npm`)) {