Skip to content

Commit

Permalink
feat: support Azure DevOps pipeline PR (#324)
Browse files Browse the repository at this point in the history
Co-authored-by: Tao Lin <[email protected]>
  • Loading branch information
lintaonz and Tao Lin authored Mar 20, 2024
1 parent 87f656d commit 6448b2a
Show file tree
Hide file tree
Showing 13 changed files with 1,179 additions and 662 deletions.
29 changes: 29 additions & 0 deletions packages/code-review-gpt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,35 @@ npm install code-review-gpt
npx code-review-gpt configure --setupTarget=gitlab
```

See templates for example yaml files. Copy and paste them to perform a manual setup.

### Azure DevOps

If you are running this tool in Azure DevOps, you will need to do some additional setup.

The code-reivew-gpt needs additional Git history available for affected to function correctly. Make sure Shallow fetching is disabled in your pipeline settings UI. For more info, check out this article from Microsoft [doc](https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/steps-checkout?view=azure-pipelines#shallow-fetch).

You will need to create a **personal access token** in Gitlab and store it in your CI/CD variables to allow the bot access to your Azure DevOps account. Follow the steps below.

#### Set Personal Access Token as a CI/CD Variable

1. **Sign in to Azure DevOps:** Go to the Azure DevOps portal and sign in to your account.
2. **Navigate to User Settings:** Click on your profile picture in the top right corner and select "Security" from the dropdown menu.
3. **Generate Personal Access Token (PAT):** In the Security page, select "Personal access tokens" and click on the "+ New Token" button.
4. **Configure Token Details:** Provide a name for your token, choose the organization, and set the expiration date.
5. **Define Token Permissions:** Specify the necessary permissions for the token based on the tasks you want to perform. For pipeline access, you might need to select "Read & manage" under "Build" and "Release."
6. **Create Token:** Click on the "Create" button to generate the token.
7. **Copy Token:** Copy the generated token immediately, as it will not be visible again.
8. **Add Token as YAML Pipeline Variable:** Go to your Azure DevOps project, open the pipeline for which you want to use the PAT, and select "Edit."
9. **Navigate to Variables:** In the pipeline editor, go to the "Variables" tab.
10. **Add New Variable:** Add a new variable with a relevant name (e.g., `API_TOKEN`) and paste the copied PAT as the value.
11. **Save Changes:** Save the pipeline changes, ensuring that the PAT is securely stored as a variable.
12. **Use Variable in Pipeline:** Modify your YAML pipeline code to reference the variable where needed, replacing hard-coded values with the variable (e.g., `$(API_TOKEN)`).

```shell
npm install code-review-gpt
npx code-review-gpt configure --setupTarget=azdev
```

See templates for example yaml files. Copy and paste them to perform a manual setup.

Expand Down
1,635 changes: 980 additions & 655 deletions packages/code-review-gpt/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/code-review-gpt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@gitbeaker/rest": "^39.10.2",
"@inquirer/prompts": "^3.0.4",
"@inquirer/rawlist": "^1.2.9",
"azure-devops-node-api": "^12.3.0",
"chalk": "^4.1.2",
"dotenv": "^16.3.1",
"glob": "^10.3.10",
Expand Down
10 changes: 5 additions & 5 deletions packages/code-review-gpt/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@ export const getYargs = async (): Promise<ReviewArgs> => {
const argv = yargs
.option("ci", {
description:
"Indicates that the script is running on a CI environment. Specifies which platform the script is running on, 'github' or 'gitlab'. Defaults to 'github'.",
choices: ["github", "gitlab"],
"Indicates that the script is running on a CI environment. Specifies which platform the script is running on, 'github', 'azdev' or 'gitlab'. Defaults to 'github'.",
choices: ["github", "gitlab", "azdev"],
type: "string",
coerce: (arg: string | undefined) => {
return arg || "github";
},
})
.option("setupTarget", {
description:
"Specifies for which platform ('github' or 'gitlab') the project should be configured for. Defaults to 'github'.",
choices: ["github", "gitlab"],
"Specifies for which platform ('github', 'gitlab' or 'azdev') the project should be configured for. Defaults to 'github'.",
choices: ["github", "gitlab", "azdev"],
type: "string",
default: "github",
})
Expand Down Expand Up @@ -97,7 +97,7 @@ export const getYargs = async (): Promise<ReviewArgs> => {

if (argv.isCi === PlatformOptions.GITLAB && argv.shouldCommentPerFile) {
logger.warn(
"The 'commentPerFile' flag only works for GitHub, not for GitLab."
"The 'commentPerFile' flag only works for GitHub, not for GitLab and AzureDevOps."
);
}

Expand Down
67 changes: 67 additions & 0 deletions packages/code-review-gpt/src/common/ci/azdev/commentOnPR.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as azdev from "azure-devops-node-api";
import * as gitApiObject from "azure-devops-node-api/GitApi";
import * as GitInterfaces from "azure-devops-node-api/interfaces/GitInterfaces";
import { logger } from "../../utils/logger";

const gitAzdevEnvVariables = (): Record<string, string> => {
const envVars = [
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI",
"API_TOKEN",
"SYSTEM_PULLREQUEST_PULLREQUESTID",
"BUILD_REPOSITORY_ID",
"SYSTEM_TEAMPROJECTID",
];
const missingVars: string[] = [];
envVars.forEach((envVar) => process.env[envVar] ?? missingVars.push(envVar));

if (missingVars.length > 0) {
logger.error(`Missing environment variables: ${missingVars.join(", ")}`);
throw new Error(
"One or more Azure DevOps environment variables are not set"
);
}

return {
serverUrl: process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI ?? "",
azdevToken: process.env.API_TOKEN ?? "",
pullRequestId: process.env.SYSTEM_PULLREQUEST_PULLREQUESTID ?? "",
project: process.env.SYSTEM_TEAMPROJECTID ?? "",
repositoryId: process.env.BUILD_REPOSITORY_ID ?? "",
};
};

/**
* Publish a comment on the pull request. It always create a new one.
* The comment will be signed off with the provided sign off.
* @param comment The body of the comment to publish.
* @param signOff The sign off to use.
* @returns
*/
export const commentOnPR = async (
comment: string,
signOff: string
): Promise<void> => {
try {
const { serverUrl, azdevToken, pullRequestId, repositoryId, project } =
gitAzdevEnvVariables();

const pullRequestIdNumber = Number(pullRequestId);

const authHandler = azdev.getPersonalAccessTokenHandler(azdevToken);
const connection: azdev.WebApi = new azdev.WebApi(serverUrl, authHandler);

const git: gitApiObject.IGitApi = await connection.getGitApi();

const commentThread = <GitInterfaces.GitPullRequestCommentThread>{
comments: [<GitInterfaces.Comment>{
content: `${comment}\n\n---\n\n${signOff}`
}]
};

await git.createThread(commentThread, repositoryId, pullRequestIdNumber, project);

} catch (error) {
logger.error(`Failed to comment on PR: ${JSON.stringify(error)}`);
throw error;
}
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { exec } from "child_process";

import { getGitHubEnvVariables, getGitLabEnvVariables } from "../../config";
import { getGitHubEnvVariables, getGitLabEnvVariables, gitAzdevEnvVariables } from "../../config";
import { PlatformOptions } from "../types";
export const getChangesFileLinesCommand = (
isCi: string | undefined,
Expand All @@ -14,6 +14,10 @@ export const getChangesFileLinesCommand = (
const { gitlabSha, mergeRequestBaseSha } = getGitLabEnvVariables();

return `git diff -U0 --diff-filter=AMRT ${mergeRequestBaseSha} ${gitlabSha} ${fileName}`;
} else if (isCi === PlatformOptions.AZDEV) {
const { azdevSha, baseSha } = gitAzdevEnvVariables();

return `git diff -U0 --diff-filter=AMRT ${baseSha} ${azdevSha} ${fileName}`;
}

return `git diff -U0 --diff-filter=AMRT --cached ${fileName}`;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { exec } from "child_process";
import { join } from "path";

import { getGitHubEnvVariables, getGitLabEnvVariables } from "../../config";
import { getGitHubEnvVariables, getGitLabEnvVariables, gitAzdevEnvVariables } from "../../config";
import { PlatformOptions } from "../types";

export const getChangedFilesNamesCommand = (
Expand All @@ -15,6 +15,10 @@ export const getChangedFilesNamesCommand = (
const { gitlabSha, mergeRequestBaseSha } = getGitLabEnvVariables();

return `git diff --name-only --diff-filter=AMRT ${mergeRequestBaseSha} ${gitlabSha}`;
} else if (isCi === PlatformOptions.AZDEV) {
const { azdevSha, baseSha } = gitAzdevEnvVariables();

return `git diff --name-only --diff-filter=AMRT ${baseSha} ${azdevSha}`;
}

return "git diff --name-only --diff-filter=AMRT --cached";
Expand Down
1 change: 1 addition & 0 deletions packages/code-review-gpt/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type IFeedback = {
export enum PlatformOptions {
GITHUB = "github",
GITLAB = "gitlab",
AZDEV = "azdev",
}

export type ReviewArgs = {
Expand Down
17 changes: 17 additions & 0 deletions packages/code-review-gpt/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,20 @@ export const getGitLabEnvVariables = (): Record<string, string> => {
mergeRequestIIdString: process.env.CI_MERGE_REQUEST_IID ?? "",
};
};

export const gitAzdevEnvVariables = (): Record<string, string> => {
const envVars = ["SYSTEM_PULLREQUEST_SOURCECOMMITID", "BASE_SHA", "API_TOKEN"];
const missingVars: string[] = [];
envVars.forEach((envVar) => process.env[envVar] ?? missingVars.push(envVar));

if (missingVars.length > 0) {
logger.error(`Missing environment variables: ${missingVars.join(", ")}`);
throw new Error("One or more Azure DevOps environment variables are not set");
}

return {
azdevSha: process.env.SYSTEM_PULLREQUEST_SOURCECOMMITID ?? "",
baseSha: process.env.BASE_SHA ?? "",
azdevToken: process.env.API_TOKEN ?? "",
};
};
24 changes: 24 additions & 0 deletions packages/code-review-gpt/src/configure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const configure = async (yargs: ReviewArgs): Promise<void> => {
if (yargs.setupTarget === PlatformOptions.GITLAB) {
await configureGitLab();
}
if (yargs.setupTarget === PlatformOptions.AZDEV) {
await configureAzureDevOps();
}
};

const captureApiKey = async (): Promise<string | undefined> => {
Expand Down Expand Up @@ -102,3 +105,24 @@ const configureGitLab = async () => {
);
}
};

const configureAzureDevOps = async () => {
const azdevPipelineTemplate = await findTemplateFile(
"**/templates/azdev-pr.yml"
);

const pipelineDir = process.cwd();
const pipelineFile = path.join(pipelineDir, "code-review-gpt.yaml");

fs.writeFileSync(
pipelineFile,
fs.readFileSync(azdevPipelineTemplate, "utf8"),
"utf8"
);

logger.info(`Created Azure DevOps Pipeline at: ${pipelineFile}`);

logger.info(
"Please manually add the OPENAI_API_KEY and API_TOKEN secrets as encrypted variables in the UI."
);
};
5 changes: 5 additions & 0 deletions packages/code-review-gpt/src/review/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { commentOnPR as commentOnPRGithub } from "../common/ci/github/commentOnPR";
import { commentPerFile } from "../common/ci/github/commentPerFile";
import { commentOnPR as commentOnPRGitlab } from "../common/ci/gitlab/commentOnPR";
import { commentOnPR as commentOnPRAzdev } from "../common/ci/azdev/commentOnPR";
import { getMaxPromptLength } from "../common/model/getMaxPromptLength";
import { PlatformOptions, ReviewArgs, ReviewFile } from "../common/types";
import { logger } from "../common/utils/logger";
Expand Down Expand Up @@ -80,5 +81,9 @@ export const review = async (
await commentOnPRGitlab(response, signOff);
}

if (isCi === PlatformOptions.AZDEV) {
await commentOnPRAzdev(response, signOff);
}

return response;
};
36 changes: 36 additions & 0 deletions packages/code-review-gpt/templates/azdev-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Code Review GPT
trigger:
- main
pr:
- main

# Important: code-reivew-gpt needs additional Git history available for affected to function correctly.
# Make sure Shallow fetching is disabled in your pipeline settings UI.
# For more info, check out this article from Microsoft https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/steps-checkout?view=azure-pipelines#shallow-fetch.
variables:
TARGET_BRANCH: $[replace(variables['System.PullRequest.TargetBranch'],'refs/heads/','origin/')]
BASE_SHA: $(git merge-base $(TARGET_BRANCH) HEAD)

pool:
vmImage: ubuntu-latest

stages:
- stage: GTP_Review
jobs:
- job: gpt_review
displayName: Code Review GPT
workspace:
clean: all
steps:
- script: |
npm install code-review-gpt
displayName: "Install code-review-gpt"
- script: |
npx code-review-gpt review --ci=azdev --model=gpt-3.5-turbo
env:
API_TOKEN: $(API_TOKEN)
OPENAI_API_KEY: $(OPENAI_API_KEY)
BASE_SHA: $(BASE_SHA)
workingDirectory: $(Build.SourcesDirectory)
displayName: "Run code review script"
4 changes: 4 additions & 0 deletions packages/code-review-gpt/utils/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ build(sharedConfig).then(() => {
path.join(__dirname, "../templates", "gitlab-pr.yml"),
path.join(__dirname, "../dist", "gitlab-pr.yml")
);
fs.copyFileSync(
path.join(__dirname, "../templates", "azdev-pr.yml"),
path.join(__dirname, "../dist", "azdev-pr.yml")
);
});

0 comments on commit 6448b2a

Please sign in to comment.