|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Component update tasks (moved from publishRoslynCopilot.ts) |
| 3 | + *--------------------------------------------------------------------------------------------*/ |
| 4 | + |
| 5 | +import * as gulp from 'gulp'; |
| 6 | +import * as process from 'node:process'; |
| 7 | +import * as fs from 'fs'; |
| 8 | +import * as path from 'path'; |
| 9 | +import minimist from 'minimist'; |
| 10 | +import { spawnSync } from 'child_process'; |
| 11 | +import { getPackageJSON } from './packageJson'; |
| 12 | +import { Octokit } from '@octokit/rest'; |
| 13 | + |
| 14 | +type Options = { |
| 15 | + userName?: string; |
| 16 | + email?: string; |
| 17 | +}; |
| 18 | + |
| 19 | +function git(args: string[], printCommand = true): Promise<string> { |
| 20 | + if (printCommand) { |
| 21 | + console.log(`git ${args.join(' ')}`); |
| 22 | + } |
| 23 | + |
| 24 | + const git = spawnSync('git', args); |
| 25 | + if (git.status != 0) { |
| 26 | + const err = git.stderr ? git.stderr.toString() : ''; |
| 27 | + if (printCommand) { |
| 28 | + console.error(`Failed to execute git ${args.join(' ')}.`); |
| 29 | + } |
| 30 | + throw new Error(err || `git ${args.join(' ')} failed with code ${git.status}`); |
| 31 | + } |
| 32 | + |
| 33 | + const stdout = git.stdout ? git.stdout.toString() : ''; |
| 34 | + if (printCommand) { |
| 35 | + console.log(stdout); |
| 36 | + } |
| 37 | + return Promise.resolve(stdout); |
| 38 | +} |
| 39 | + |
| 40 | +gulp.task('publish roslyn copilot', async () => { |
| 41 | + const parsedArgs = minimist<Options>(process.argv.slice(2)); |
| 42 | + |
| 43 | + // Get staging directory from environment variable passed from pipeline |
| 44 | + const stagingDir = process.env['STAGING_DIRECTORY']; |
| 45 | + if (!stagingDir) { |
| 46 | + console.log('STAGING_DIRECTORY environment variable not set; skipping package.json update.'); |
| 47 | + return; |
| 48 | + } |
| 49 | + |
| 50 | + if (!fs.existsSync(stagingDir)) { |
| 51 | + console.log(`Staging directory not found at ${stagingDir}; skipping package.json update.`); |
| 52 | + return; |
| 53 | + } |
| 54 | + |
| 55 | + // Find the Roslyn zip file in the staging directory (we know it was copied here) |
| 56 | + const files = fs.readdirSync(stagingDir); |
| 57 | + const zipFile = files.find(file => /Roslyn\.LanguageServer.*\.zip$/i.test(file)); |
| 58 | + |
| 59 | + if (!zipFile) { |
| 60 | + console.log(`No Roslyn LanguageServer zip file found in ${stagingDir}; skipping package.json update.`); |
| 61 | + return; |
| 62 | + } |
| 63 | + |
| 64 | + const zipPath = path.join(stagingDir, zipFile); |
| 65 | + console.log(`Using zip file: ${zipPath}`); |
| 66 | + const zipName = zipFile; |
| 67 | + |
| 68 | + // Extract version from file name |
| 69 | + // Example: "Microsoft.VisualStudio.Copilot.Roslyn.LanguageServer-18.0.671-alpha.zip" |
| 70 | + let version: string | null = null; |
| 71 | + const m = zipName.match(/Microsoft\.VisualStudio\.Copilot\.Roslyn\.LanguageServer-(.+)\.zip$/i); |
| 72 | + if (m && m[1]) { |
| 73 | + version = m[1]; |
| 74 | + } |
| 75 | + |
| 76 | + if (!version) { |
| 77 | + console.log(`Could not extract version from file name ${zipName}; skipping.`); |
| 78 | + return; |
| 79 | + } |
| 80 | + |
| 81 | + console.log(`Extracted version: ${version}`); |
| 82 | + |
| 83 | + const pkg = getPackageJSON(); |
| 84 | + let updated = false; |
| 85 | + |
| 86 | + if (pkg.runtimeDependencies && Array.isArray(pkg.runtimeDependencies)) { |
| 87 | + for (let i = 0; i < pkg.runtimeDependencies.length; i++) { |
| 88 | + const dep = pkg.runtimeDependencies[i]; |
| 89 | + if (dep && dep.id === 'RoslynCopilot') { |
| 90 | + const oldUrl = dep.url as string; |
| 91 | + const newUrl = oldUrl.replace(/Microsoft\.VisualStudio\.Copilot\.Roslyn\.LanguageServer-[^/]+?\.zip/, `Microsoft.VisualStudio.Copilot.Roslyn.LanguageServer-${version}.zip`); |
| 92 | + if (oldUrl !== newUrl) { |
| 93 | + pkg.runtimeDependencies[i].url = newUrl; |
| 94 | + updated = true; |
| 95 | + console.log(`Updated RoslynCopilot url:\n ${oldUrl}\n-> ${newUrl}`); |
| 96 | + } else { |
| 97 | + console.log('RoslynCopilot url already up to date.'); |
| 98 | + } |
| 99 | + break; |
| 100 | + } |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + if (!updated) { |
| 105 | + console.log('No changes required to package.json; aborting PR creation.'); |
| 106 | + return; |
| 107 | + } |
| 108 | + |
| 109 | + // Persist package.json |
| 110 | + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n', { encoding: 'utf8' }); |
| 111 | + |
| 112 | + // Prepare git |
| 113 | + const safeVersion = version.replace(/[^A-Za-z0-9_.-]/g, '-'); |
| 114 | + const branch = `update/roslyn-copilot-${safeVersion}`; |
| 115 | + |
| 116 | + // Make this optional so it can be tested locally by using dev's information. In real CI user name and email are always supplied. |
| 117 | + if (parsedArgs.userName) { |
| 118 | + await git(['config', '--local', 'user.name', parsedArgs.userName]); |
| 119 | + } |
| 120 | + if (parsedArgs.email) { |
| 121 | + await git(['config', '--local', 'user.email', parsedArgs.email]); |
| 122 | + } |
| 123 | + |
| 124 | + await git(['checkout', '-b', branch]); |
| 125 | + await git(['add', 'package.json']); |
| 126 | + await git(['commit', '-m', `chore: update RoslynCopilot url to ${version}`]); |
| 127 | + |
| 128 | + const pat = process.env['GitHubPAT']; |
| 129 | + if (!pat) { |
| 130 | + throw 'No GitHub PAT found.'; |
| 131 | + } |
| 132 | + |
| 133 | + const remoteRepoAlias = 'targetRepo'; |
| 134 | + await git( |
| 135 | + [ |
| 136 | + 'remote', |
| 137 | + 'add', |
| 138 | + remoteRepoAlias, |
| 139 | + `https://${parsedArgs.userName}:${pat}@github.com/dotnet/vscode-csharp.git`, |
| 140 | + ], |
| 141 | + // Note: don't print PAT to console |
| 142 | + false |
| 143 | + ); |
| 144 | + await git(['fetch', remoteRepoAlias]); |
| 145 | + |
| 146 | + const lsRemote = await git(['ls-remote', remoteRepoAlias, 'refs/head/' + branch]); |
| 147 | + if (lsRemote.trim() !== '') { |
| 148 | + // If the localization branch of this commit already exists, don't try to create another one. |
| 149 | + console.log( |
| 150 | + `##vso[task.logissue type=error]${branch} already exists in dotnet/vscode-csharp. Skip pushing.` |
| 151 | + ); |
| 152 | + } else { |
| 153 | + await git(['push', '-u', remoteRepoAlias]); |
| 154 | + } |
| 155 | + |
| 156 | + // Create PR via Octokit |
| 157 | + try { |
| 158 | + const octokit = new Octokit({ auth: pat }); |
| 159 | + const listPullRequest = await octokit.rest.pulls.list({ |
| 160 | + owner: 'dotnet', |
| 161 | + repo: 'vscode-csharp', |
| 162 | + }); |
| 163 | + |
| 164 | + if (listPullRequest.status != 200) { |
| 165 | + throw `Failed get response from GitHub, http status code: ${listPullRequest.status}`; |
| 166 | + } |
| 167 | + |
| 168 | + const title = `Update RoslynCopilot url to ${version}`; |
| 169 | + if (listPullRequest.data.some((pr) => pr.title === title)) { |
| 170 | + console.log('Pull request with the same name already exists. Skip creation.'); |
| 171 | + return; |
| 172 | + } |
| 173 | + |
| 174 | + const body = `Automated update of RoslynCopilot url to ${version}`; |
| 175 | + |
| 176 | + console.log(`Creating PR against dotnet/vscode-csharp...`); |
| 177 | + const pullRequest = await octokit.rest.pulls.create({ |
| 178 | + owner: 'dotnet', |
| 179 | + repo: 'vscode-csharp', |
| 180 | + title: title, |
| 181 | + head: branch, |
| 182 | + base: 'main', |
| 183 | + body: body |
| 184 | + }); |
| 185 | + console.log(`Created pull request: ${pullRequest.data.html_url}`); |
| 186 | + } catch (e) { |
| 187 | + console.warn('Failed to create PR via Octokit:', e); |
| 188 | + } |
| 189 | +}); |
0 commit comments