Skip to content

Commit 44c49db

Browse files
committed
Add automated PR creation for Roslyn Copilot updates using a gulp task
1 parent fd38eee commit 44c49db

File tree

3 files changed

+206
-2
lines changed

3 files changed

+206
-2
lines changed

azure-pipelines/publish-roslyn-copilot.yml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ extends:
2929
image: 1ESPT-Windows2022
3030
os: windows
3131
templateContext:
32-
type: releaseJob
3332
isProduction: false #change this
3433
inputs:
3534
- input: pipelineArtifact
@@ -38,7 +37,11 @@ extends:
3837
destinationPath: $(Pipeline.Workspace)/artifacts
3938

4039
steps:
41-
- checkout: none
40+
- checkout: self
41+
clean: true
42+
submodules: true
43+
fetchTags: false
44+
fetchDepth: 0
4245

4346
- task: CopyFiles@2
4447
displayName: 'Copy files from Zip folder to staging directory'
@@ -56,3 +59,14 @@ extends:
5659
Destination: "AzureBlob"
5760
storage: "$(AzStorage)"
5861
ContainerName: "$(AzContainerName)"
62+
63+
- pwsh: |
64+
npm install
65+
npm install -g gulp
66+
displayName: 'Install tools'
67+
68+
- pwsh: gulp 'publish roslyn copilot' --userName abhitejjohn --email [email protected]
69+
displayName: 'Create component update PR'
70+
env:
71+
GitHubPAT: $(GitHubPAT)
72+
STAGING_DIRECTORY: $(Build.ArtifactStagingDirectory)/staging

gulpfile.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ require('./tasks/debuggerTasks');
1212
require('./tasks/snapTasks');
1313
require('./tasks/signingTasks');
1414
require('./tasks/profilingTasks');
15+
require('./tasks/componentUpdateTasks');

tasks/componentUpdateTasks.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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

Comments
 (0)