Skip to content

Add step in publish pipeline to create PR to azure-sdk-for-net #7426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/http-client-csharp/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Contributing

## PR Process

When making changes to `@typespec/http-client-csharp`, the downstream effects on the Azure SDK for .NET need to be considered.

### Automated PR Creation

The publishing pipeline for `@typespec/http-client-csharp` includes automated steps to create a PR in the [azure-sdk-for-net](https://github.com/Azure/azure-sdk-for-net) repository to update the dependency on `Microsoft.TypeSpec.Generator.ClientModel`.

The process works as follows:

1. Create your PR to the [microsoft/typespec](https://github.com/microsoft/typespec) repository for `@typespec/http-client-csharp`
2. After your PR is merged and a release happens, the publishing pipeline will:
a. Publish the NuGet packages
b. Automatically create a PR in [azure-sdk-for-net](https://github.com/Azure/azure-sdk-for-net) to update the dependency

3. The automated PR in azure-sdk-for-net will:
- Update the package references in Directory.Packages.props
- Include a reference to the original TypeSpec PR
- Include details about the changes

4. Once the PR in azure-sdk-for-net is merged, the update is complete

### Manual Process (if automation fails)

If the automated PR creation fails, you can manually create the PR following these steps:

1. Clone the azure-sdk-for-net repository
2. Create a new branch
3. Update the package references in Directory.Packages.props
4. Create a PR with a description referencing the TypeSpec PR
25 changes: 25 additions & 0 deletions packages/http-client-csharp/eng/pipeline/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,28 @@ extends:
inputs:
useGlobalJson: true
workingDirectory: $(Build.SourcesDirectory)/packages/http-client-csharp

- stage: CreateAzureSdkForNetPR
displayName: Create PR for azure-sdk-for-net
dependsOn: CSharp_Publish
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.SourceBranchName'], 'main'))
pool:
name: $(LINUXPOOL)
image: $(LINUXVMIMAGE)
os: linux
jobs:
- job: CreatePR
steps:
- checkout: self

- template: /eng/tsp-core/pipelines/templates/install.yml

- script: |
node ./packages/internal-build-utils/cmd/cli.js create-azure-sdk-for-net-pr \
--packagePath $(Build.SourcesDirectory)/packages/http-client-csharp \
--pullRequestUrl "https://github.com/microsoft/typespec/pull/$(System.PullRequest.PullRequestNumber)" \
--packageUrl "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js-test-autorest/nuget/v3/flat2/Microsoft.TypeSpec.Generator.ClientModel/$(packageVersion)" \
--githubToken $(azure-sdk-build-github-token)
displayName: Create PR in azure-sdk-for-net
env:
packageVersion: $(Build.BuildNumber)
32 changes: 32 additions & 0 deletions packages/internal-build-utils/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import yargs from "yargs";
import { generateThirdPartyNotice } from "./generate-third-party-notice.js";
import { bumpVersionsForPR, bumpVersionsForPrerelease } from "./prerelease.js";
import { createAzureSdkForNetPr } from "./create-azure-sdk-for-net-pr.js";

main().catch((e) => {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -48,5 +49,36 @@ async function main() {
demandOption: true,
}),
(args) => bumpVersionsForPR(args.workspaceRoot, args.pr, args.buildNumber),
)
.command(
"create-azure-sdk-for-net-pr",
"Create PR in azure-sdk-for-net to update http-client-csharp dependency",
(cmd) =>
cmd
.option("packagePath", {
type: "string",
description: "Path to the http-client-csharp package",
demandOption: true,
})
.option("pullRequestUrl", {
type: "string",
description: "URL of the PR in typespec repository",
demandOption: true,
})
.option("packageUrl", {
type: "string",
description: "URL to the published NuGet package",
demandOption: true,
})
.option("githubToken", {
type: "string",
description: "GitHub token for authentication",
demandOption: true,
})
.option("branchName", {
type: "string",
description: "Branch name to create in azure-sdk-for-net",
}),
(args) => createAzureSdkForNetPr(args),
).argv;
}
148 changes: 148 additions & 0 deletions packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/* eslint-disable no-console */
import { execSync } from "child_process";
import { mkdirSync, writeFileSync, readFileSync } from "fs";
import { join, resolve } from "path";
import { DefaultHttpClientFetch } from "./http-client.js";

interface Options {
/**
* Path to the http-client-csharp package
*/
packagePath: string;

/**
* Pull request URL of the PR in typespec repository
*/
pullRequestUrl: string;

/**
* Direct URL to the published NuGet package
*/
packageUrl: string;

/**
* GitHub token for authentication
*/
githubToken: string;

/**
* Branch name to create in azure-sdk-for-net
*/
branchName?: string;
}

/**
* Creates a PR in the azure-sdk-for-net repository to update the http-client-csharp dependency
*/
export async function createAzureSdkForNetPr(options: Options): Promise<void> {
const { packagePath, pullRequestUrl, packageUrl, githubToken } = options;
console.log(`Creating PR for azure-sdk-for-net to update dependency on http-client-csharp`);

// Create temp folder for repo
const tempDir = join(process.cwd(), "temp", "azure-sdk-for-net");
mkdirSync(tempDir, { recursive: true });
console.log(`Created temp directory at ${tempDir}`);

try {
// Clone the repository
console.log(`Cloning azure-sdk-for-net repository...`);
execSync(`git clone https://github.com/Azure/azure-sdk-for-net.git ${tempDir}`, {

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium

This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 10 days ago

To fix the issue, we will replace the use of execSync with execFileSync and pass the arguments separately instead of interpolating them into the shell command. This approach avoids shell interpretation of the tempDir value, mitigating the risk of command injection or unintended behavior.

Specifically:

  1. Replace the execSync call on line 49 with execFileSync.
  2. Pass the git command and its arguments as separate parameters to execFileSync.
  3. Ensure that the tempDir value is passed as an argument, not interpolated into the command string.

Suggested changeset 1
packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts b/packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts
--- a/packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts
+++ b/packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts
@@ -1,3 +1,3 @@
 /* eslint-disable no-console */
-import { execSync } from "child_process";
+import { execFileSync, execSync } from "child_process";
 import { mkdirSync, writeFileSync, readFileSync } from "fs";
@@ -48,3 +48,3 @@
     console.log(`Cloning azure-sdk-for-net repository...`);
-    execSync(`git clone https://github.com/Azure/azure-sdk-for-net.git ${tempDir}`, {
+    execFileSync("git", ["clone", "https://github.com/Azure/azure-sdk-for-net.git", tempDir], {
       stdio: "inherit",
EOF
@@ -1,3 +1,3 @@
/* eslint-disable no-console */
import { execSync } from "child_process";
import { execFileSync, execSync } from "child_process";
import { mkdirSync, writeFileSync, readFileSync } from "fs";
@@ -48,3 +48,3 @@
console.log(`Cloning azure-sdk-for-net repository...`);
execSync(`git clone https://github.com/Azure/azure-sdk-for-net.git ${tempDir}`, {
execFileSync("git", ["clone", "https://github.com/Azure/azure-sdk-for-net.git", tempDir], {
stdio: "inherit",
Copilot is powered by AI and may make mistakes. Always verify output.
stdio: "inherit",
});

// Read package info
const packageJsonPath = resolve(packagePath, "package.json");
const packageJsonContent = JSON.parse(readFileSync(packageJsonPath, "utf8"));
const packageVersion = packageJsonContent.version;
console.log(`Using package version: ${packageVersion}`);

// Generate branch name if not provided
const branchName = options.branchName || `typespec/update-http-client-${packageVersion}`;
console.log(`Using branch name: ${branchName}`);

// Create a new branch
console.log(`Creating branch ${branchName}...`);
execSync(`git checkout -b ${branchName}`, {

Check warning

Code scanning / CodeQL

Unsafe shell command constructed from library input Medium

This string concatenation which depends on
library input
is later used in a
shell command
.

Copilot Autofix

AI 10 days ago

To fix the issue, we should avoid directly interpolating the branchName into the shell command string. Instead, we can use the execFileSync method from the child_process module, which allows us to pass arguments as an array. This approach avoids shell interpretation of special characters in the input, mitigating the risk of shell injection.

Specifically:

  1. Replace the execSync call on line 65 with execFileSync, passing the branchName as an argument in an array.
  2. Ensure that the branchName is validated or sanitized before use to prevent any unintended behavior.

Suggested changeset 1
packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts b/packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts
--- a/packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts
+++ b/packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts
@@ -1,3 +1,3 @@
 /* eslint-disable no-console */
-import { execSync } from "child_process";
+import { execSync, execFileSync } from "child_process";
 import { mkdirSync, writeFileSync, readFileSync } from "fs";
@@ -64,3 +64,3 @@
     console.log(`Creating branch ${branchName}...`);
-    execSync(`git checkout -b ${branchName}`, {
+    execFileSync("git", ["checkout", "-b", branchName], {
       stdio: "inherit",
EOF
@@ -1,3 +1,3 @@
/* eslint-disable no-console */
import { execSync } from "child_process";
import { execSync, execFileSync } from "child_process";
import { mkdirSync, writeFileSync, readFileSync } from "fs";
@@ -64,3 +64,3 @@
console.log(`Creating branch ${branchName}...`);
execSync(`git checkout -b ${branchName}`, {
execFileSync("git", ["checkout", "-b", branchName], {
stdio: "inherit",
Copilot is powered by AI and may make mistakes. Always verify output.
stdio: "inherit",
cwd: tempDir,
});

// Update the dependency in Directory.Packages.props (this is the file that usually contains dependency versions in Azure SDK)
console.log(`Updating dependency version in Directory.Packages.props...`);
const propsFilePath = join(tempDir, "Directory.Packages.props");
const propsFileContent = readFileSync(propsFilePath, "utf8");

// Update the appropriate package reference in the file
const updatedContent = propsFileContent.replace(
/<PackageVersion Include="Microsoft\.TypeSpec\.Generator\.ClientModel".*?>(.*?)<\/PackageVersion>/g,
`<PackageVersion Include="Microsoft.TypeSpec.Generator.ClientModel">${packageVersion}</PackageVersion>`
);

// Write the updated file back
writeFileSync(propsFilePath, updatedContent);

// Commit the changes
console.log(`Committing changes...`);
execSync(`git add Directory.Packages.props`, {
stdio: "inherit",
cwd: tempDir,
});
execSync(`git commit -m "Update Microsoft.TypeSpec.Generator.ClientModel to ${packageVersion}"`, {
stdio: "inherit",
cwd: tempDir,
});

// Push the branch
console.log(`Pushing branch to remote...`);
// Using HTTPS with token for auth
const remoteUrl = `https://${githubToken}@github.com/Azure/azure-sdk-for-net.git`;

Check warning

Code scanning / CodeQL

Unsafe shell command constructed from library input Medium

This string concatenation which depends on
library input
is later used in a
shell command
.

Copilot Autofix

AI 10 days ago

To fix the issue, we should avoid directly embedding the githubToken into the shell command string. Instead, we can use a safer API like child_process.execFile to pass the arguments as an array, which avoids interpretation by the shell. Since execFile does not support inline authentication in the URL, we can use the git command's -c option to set the http.extraheader configuration for authentication.

This approach ensures that the githubToken is not interpreted by the shell, mitigating the risk of command injection.


Suggested changeset 1
packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts b/packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts
--- a/packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts
+++ b/packages/internal-build-utils/src/create-azure-sdk-for-net-pr.ts
@@ -97,4 +97,4 @@
     // Using HTTPS with token for auth
-    const remoteUrl = `https://${githubToken}@github.com/Azure/azure-sdk-for-net.git`;
-    execSync(`git push ${remoteUrl} ${branchName}`, {
+    const remoteUrl = "https://github.com/Azure/azure-sdk-for-net.git";
+    execSync("git", ["-c", `http.extraheader=Authorization: Bearer ${githubToken}`, "push", remoteUrl, branchName], {
       stdio: "inherit",
EOF
@@ -97,4 +97,4 @@
// Using HTTPS with token for auth
const remoteUrl = `https://${githubToken}@github.com/Azure/azure-sdk-for-net.git`;
execSync(`git push ${remoteUrl} ${branchName}`, {
const remoteUrl = "https://github.com/Azure/azure-sdk-for-net.git";
execSync("git", ["-c", `http.extraheader=Authorization: Bearer ${githubToken}`, "push", remoteUrl, branchName], {
stdio: "inherit",
Copilot is powered by AI and may make mistakes. Always verify output.
execSync(`git push ${remoteUrl} ${branchName}`, {

Check warning

Code scanning / CodeQL

Unsafe shell command constructed from library input Medium

This string concatenation which depends on
library input
is later used in a
shell command
.
This string concatenation which depends on
library input
is later used in a
shell command
.
stdio: "inherit",
cwd: tempDir,
});

// Create PR using GitHub API
console.log(`Creating PR in Azure/azure-sdk-for-net...`);
const client = new DefaultHttpClientFetch();

const prBody = `
This PR updates the dependency on Microsoft.TypeSpec.Generator.ClientModel to version ${packageVersion}.

## Details

- Original TypeSpec PR: ${pullRequestUrl}
- Package URL: ${packageUrl}

This is an automated PR created by the TypeSpec publish pipeline.
`.trim();

const prTitle = `Update Microsoft.TypeSpec.Generator.ClientModel to ${packageVersion}`;

const response = await client.fetch(`https://api.github.com/repos/Azure/azure-sdk-for-net/pulls`, {
method: "POST",
headers: {
"Accept": "application/vnd.github.v3+json",
"Authorization": `token ${githubToken}`,
"User-Agent": "Microsoft-TypeSpec",
},
body: JSON.stringify({
title: prTitle,
body: prBody,
head: branchName,
base: "master", // Assuming the main branch is called 'master'
}),
});

if (response.status >= 400) {
const responseBody = await response.text();
console.error(`Failed to create PR: ${responseBody}`);
throw new Error(`Failed to create PR: ${response.status}`);
}

const responseJson = await response.json();
console.log(`Successfully created PR: ${responseJson.html_url}`);
} catch (error) {
console.error(`Error creating PR:`, error);
throw error;
}
}
16 changes: 16 additions & 0 deletions packages/internal-build-utils/src/http-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Simple HTTP client for making requests to external APIs
*/

export interface HttpClient {
fetch(url: string, init?: RequestInit): Promise<Response>;
}

/**
* Default implementation of HttpClient using fetch
*/
export class DefaultHttpClientFetch implements HttpClient {
async fetch(url: string, init?: RequestInit): Promise<Response> {
return fetch(url, init);
}
}
2 changes: 2 additions & 0 deletions packages/internal-build-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export * from "./common.js";
export * from "./dotnet.js";
export * from "./visualstudio.js";
export * from "./watch.js";
export * from "./create-azure-sdk-for-net-pr.js";
export * from "./http-client.js";
Loading