Path: blob/main/extensions/copilot/build/pr-check-cache-files.ts
13383 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { exec } from 'child_process';6import { promisify } from 'util';78const execAsync = promisify(exec);910type Commit = {11readonly sha: string;12readonly committer: {13readonly login: string;14};15readonly commit: {16readonly verification: {17readonly verified: boolean;18readonly reason: string;19};20};21readonly files: readonly PullRequestFile[];22}2324type PullRequestFile = {25readonly filename: string;26readonly status: string;27}2829type PullRequestCommit = {30readonly sha: string;31}3233const collaborators = [34"aeschli", "aiday-mar", "alexdima", "alexr00", "amunger", "anthonykim1", "bamurtaugh", "benibenj", "benvillalobos", "bhavyaus",35"binderjoe", "bpasero", "bryanchen-d", "burkeholland", "chrmarti", "connor4312", "cwebster-99", "dbaeumer", "deepak1556",36"devinvalenciano", "digitarald", "dileepyavan", "dineshc-msft", "dmitrivMS", "DonJayamanne", "egamma", "eleanorjboyd", "eli-w-king",37"hawkticehurst", "hediet", "isidorn", "jo-oikawa", "joaomoreno", "joshspicer", "jrieken", "jruales", "justschen", "karthiknadig",38"kieferrm", "kkbrooks", "kycutler", "lramos15", "lszomoru", "luabud", "meganrogge", "minsa110", "mjbvz", "mrleemurray", "nguyenchristy",39"ntrogh", "olguzzar", "osortega", "pierceboggan", "pwang347", "rebornix", "roblourens", "rzhao271", "sandy081", "sbatten", "TylerLeonhardt",40"Tyriar", "ulugbekna", "vijayupadya", "Yoyokrazy"41];4243// TODO@lszomoru - Investigate issues with the `/collaborators` endpoint44// async function getCollaborators(repository: string): Promise<readonly string[]> {45// const { stdout, stderr } = await execAsync(46// `gh api -H "Accept: application/vnd.github+json" /repos/${repository}/collaborators --paginate`, { maxBuffer: 25 * 1024 * 1024 });4748// if (stderr) {49// throw new Error(`Error fetching repository collaborators - ${stderr}`);50// }5152// return JSON.parse(stdout) as ReadonlyArray<string>;53// }5455async function getCommit(repository: string, sha: string): Promise<Commit> {56const { stdout, stderr } = await execAsync(57`gh api -H "Accept: application/vnd.github+json" /repos/${repository}/commits/${sha}`, { maxBuffer: 25 * 1024 * 1024 });5859if (stderr) {60throw new Error(`Error fetching commit ${sha} - ${stderr}`);61}6263return JSON.parse(stdout) as Commit;64}6566async function getPullRequestFiles(repository: string, pullRequestNumber: string): Promise<readonly PullRequestFile[]> {67const { stdout, stderr } = await execAsync(68`gh api -H "Accept: application/vnd.github+json" /repos/${repository}/pulls/${pullRequestNumber}/files --paginate`, { maxBuffer: 25 * 1024 * 1024 });6970if (stderr) {71throw new Error(`Error fetching pull request files - ${stderr}`);72}7374return JSON.parse(stdout) as readonly PullRequestFile[];75}7677async function getPullRequestCommits(repository: string, pullRequestNumber: string): Promise<readonly string[]> {78const { stdout, stderr } = await execAsync(79`gh api -H "Accept: application/vnd.github+json" /repos/${repository}/pulls/${pullRequestNumber}/commits --paginate`, { maxBuffer: 25 * 1024 * 1024 });8081if (stderr) {82throw new Error(`Error fetching pull request commits - ${stderr}`);83}8485return JSON.parse(stdout).map((commit: PullRequestCommit) => commit.sha);86}8788async function checkDatabaseFile(files: ReadonlyArray<PullRequestFile>): Promise<boolean> {89const baseFile = files.find(f => f.filename.toLowerCase() === 'test/simulation/cache/base.sqlite');90if (!baseFile) {91console.log('✅ Pull request does not contain the base file.');92return true;93}9495const statusCheck = baseFile.status === 'modified';96console.log(`🔍 Pull request contains the base file. Checking status...`);97console.log(` - 🗄️ ${baseFile.filename}; Status: ${baseFile.status} ${statusCheck ? '✅' : '⛔'}`);9899return statusCheck;100}101102async function checkDatabaseLayerFiles(repository: string, pullRequestNumber: string, files: readonly PullRequestFile[])103: Promise<{ statusCheck: boolean; verifiedCheck: boolean; collaboratorCheck: boolean }> {104const layerFiles = files.filter(f => f.filename.toLowerCase().startsWith('test/simulation/cache/layers/'));105106if (layerFiles.length === 0) {107console.log('✅ Pull request does not contain any layer files.');108return { statusCheck: true, verifiedCheck: true, collaboratorCheck: true };109}110111// Get collaborators and commits for the pull request112// const collaborators = await getCollaborators(repository);113const pullRequestCommits = await getPullRequestCommits(repository, pullRequestNumber);114const commitsWithDetails = await Promise.all(pullRequestCommits.map(sha => getCommit(repository, sha)));115116let statusCheckResult = true, verifiedCheckResult = true, collaboratorCheckResult = true;117console.log(`🔍 Pull request contains ${layerFiles.length} layer files. Checking status and author...`);118119for (const file of layerFiles) {120const statusCheck = file.status === 'added' || file.status === 'removed';121console.log(` - 🗄️ ${file.filename}`);122console.log(` - Status: ${file.status} ${statusCheck ? '✅' : '⛔'}`);123124if (!statusCheck) {125statusCheckResult = false;126}127128// List of commits that contain the file129const commits = commitsWithDetails.filter(c =>130c.files.some(f => f.filename === file.filename));131132console.log(` - Commit(s):`);133for (const commit of commits) {134const collaboratorCheck = collaborators.find(c => c === commit.committer.login);135const verifiedCheck = commit.commit.verification.verified && commit.commit.verification.reason === 'valid';136console.log(` - ${commit.sha} by ${commit.committer.login}. Collaborator: ${collaboratorCheck ? '✅' : '⛔'} Verified: ${verifiedCheck ? '✅' : '⛔'}`);137138if (!verifiedCheck) {139verifiedCheckResult = false;140}141if (!collaboratorCheck) {142collaboratorCheckResult = false;143}144}145}146147return { statusCheck: statusCheckResult, verifiedCheck: verifiedCheckResult, collaboratorCheck: collaboratorCheckResult };148}149150async function main() {151try {152const repository = process.env['REPOSITORY'];153const pullRequestNumber = process.env['PULL_REQUEST'];154155if (!repository || !pullRequestNumber) {156throw new Error('Missing required environment variables: REPOSITORY or PULL_REQUEST');157}158159console.log(`🔍 Checking pull request #${pullRequestNumber} in repository "${repository}"...`);160161// Get a list of files in the pull request162const files = await getPullRequestFiles(repository, pullRequestNumber);163164// 1. Check base file status165const baseCheckResult = await checkDatabaseFile(files);166167// 2. Check cache layer file(s) status and author168const layerCheckResult = await checkDatabaseLayerFiles(repository, pullRequestNumber, files);169170if (!baseCheckResult) {171throw new Error('Base file can only be modified in a pull request.');172}173if (!layerCheckResult.statusCheck) {174throw new Error('Cache layer files can only be added or deleted, never modified');175}176if (!layerCheckResult.verifiedCheck || !layerCheckResult.collaboratorCheck) {177throw new Error('Cache layer files can only be added by VS Code team members with signed commits');178}179} catch (error) {180console.log('::error::⛔', error);181process.exit(1);182}183}184185if (require.main === module) {186main();187}188189190