Skip to content

Commit

Permalink
feat: build - deploy - ai comment
Browse files Browse the repository at this point in the history
  • Loading branch information
MounirAbdousNventive committed Dec 16, 2024
1 parent 591fa53 commit 2e9f429
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 10 deletions.
27 changes: 27 additions & 0 deletions template/azure-pipeline/ai-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
stages:
- ${{ if eq(variables['Build.Reason'], 'PullRequest') }}:
- stage: Ai_comment
displayName: AI comments on PR
condition: eq(variables['Build.Reason'], 'PullRequest')
jobs:
- job: Comment_trigger
displayName: Manual Trigger for AI comments
pool: server
steps:
- task: ManualValidation@1
timeoutInMinutes: 5
inputs:
instructions: "Do you want AI comment on this PR?"
onTimeout: "reject"
- job: Comment_pr
displayName: Analyze and Comment on PR
dependsOn: Comment_trigger
condition: succeeded('Comment_trigger')
steps:
- script: |
python3 -m pip install openai==0.28 requests
python3 azure-pipeline/comment-pr.py
displayName: "Run PR Analysis"
env:
OPENAI_API_KEY: $(OPEN_AI_KEY)
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
29 changes: 29 additions & 0 deletions template/azure-pipeline/azure-pipelines.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
pr:
branches:
include:
- main

trigger:
branches:
include:
- main

name: v1.0$(Rev:.r)

variables:
- group: AzureVariables
- name: DOTNET_VERSION
value: "9.x"
- name: APP_NAME
value: "placeholder"
- name: AZURE_LOCATION
value: "eastus"

pool:
vmImage: "ubuntu-latest"

stages:
- template: commit-validator.yml
- template: build.yml
- template: ai-comment.yml
- template: environments-loop.yml
35 changes: 35 additions & 0 deletions template/azure-pipeline/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
parameters:
- name: buildConfiguration
type: string
default: "Release"
- name: projectPath
type: string
default: "**/*.AppHost/*.csproj"

stages:
- stage: Build
displayName: Build [multi-env]
condition: succeeded()
jobs:
- job: Build
displayName: Build
steps:
- task: UseDotNet@2
displayName: "Install .NET $(DOTNET_VERSION) SDK"
inputs:
packageType: "sdk"
version: $(DOTNET_VERSION)
installationPath: $(Agent.ToolsDirectory)/dotnet

- task: DotNetCoreCLI@2
displayName: "Restore"
inputs:
command: restore
projects: ${{ parameters.projectPath }}

- task: DotNetCoreCLI@2
displayName: "Build"
inputs:
command: build
projects: ${{ parameters.projectPath }}
arguments: "--configuration ${{ parameters.buildConfiguration }} --verbosity detailed"
146 changes: 146 additions & 0 deletions template/azure-pipeline/comment-pr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import os
import requests
import openai
import difflib

API_VERSION = "7.1"

# Utility Functions


def get_env_variable(var_name, required=True):
value = os.getenv(var_name)
if required and not value:
raise EnvironmentError(f"Environment variable '{
var_name}' is missing.")
return value.strip("/")


def make_api_request(url, headers, method="GET", data=None):
response = requests.request(method, url, headers=headers, json=data)
if response.status_code != 200:
raise Exception(f"API call failed: {
response.status_code}, Response: {response.text}")
return response.json()


def compare_file_contents(file_path, old_content, new_content):
diff = difflib.unified_diff(
old_content.splitlines(),
new_content.splitlines(),
fromfile=f"{file_path} (old)",
tofile=f"{file_path} (new)"
)
return "\n".join(diff)


def calculate_cost(prompt_tokens, completion_tokens):
input_rate = 0.15 / 1_000_000 # $0.15 per 1M input tokens
output_rate = 0.6 / 1_000_000 # $0.60 per 1M output tokens
return (prompt_tokens * input_rate) + (completion_tokens * output_rate)


def main():
# Load Environment Variables
azure_token = get_env_variable("SYSTEM_ACCESSTOKEN")
openai_api_key = get_env_variable("OPENAI_API_KEY")
organization = get_env_variable("SYSTEM_COLLECTIONURI")
project = get_env_variable("SYSTEM_TEAMPROJECT")
repo_id = get_env_variable("BUILD_REPOSITORY_ID")
pr_id = get_env_variable("SYSTEM_PULLREQUEST_PULLREQUESTID")

# Configure API and Headers
openai.api_key = openai_api_key
headers = {"Authorization": f"Bearer {azure_token}"}
base_url = f"{organization}/{project}/_apis/git/repositories/{repo_id}"

# Fetch Pull Request Details
pr_url = f"{base_url}/pullRequests/{pr_id}?api-version=7.0"
pr_details = make_api_request(pr_url, headers)
title = pr_details["title"]
description = pr_details.get("description", "")
target_branch = pr_details["targetRefName"].split("/")[-1]

# Fetch Iterations and Changed Files
iterations_url = f"{
base_url}/pullRequests/{pr_id}/iterations?api-version={API_VERSION}"
iterations = make_api_request(iterations_url, headers)
changed_files = set()

if "value" in iterations:
for iteration in iterations["value"]:
iteration_id = iteration["id"]
commits_url = f"{base_url}/pullRequests/{pr_id}/iterations/{
iteration_id}/commits?api-version={API_VERSION}"
commits = make_api_request(commits_url, headers)["value"]

for commit in commits:
commit_id = commit["commitId"]
commit_url = f"{
base_url}/commits/{commit_id}?api-version={API_VERSION}"
commit_data = make_api_request(commit_url, headers)
changes_url = commit_data["_links"]["changes"]["href"]
changes_data = make_api_request(changes_url, headers)
if "changes" in changes_data:
for change in changes_data["changes"]:
if "item" in change and "path" in change["item"]:
path = change["item"]["path"]
if "." in path.split("/")[-1]:
changed_files.add(path)

# Analyze File Changes
messages = [
{"role": "system", "content": "You are a helpful and knowledgeable code reviewer with expertise in clean code practices."},
{"role": "user", "content": f"Here's a PR titled '{title}' with the following description: {
description}. Please review the implementation carefully."},
{"role": "user", "content": f"The following files were changed: {changed_files}."}
]

for file_path in changed_files:
print(f"Processing file: {file_path}")
try:
# Fetch New Version
new_version_url = f"{base_url}/items/{file_path}?versionType=Commit&version={
commits[-1]['commitId']}&api-version={API_VERSION}"
new_version_content = requests.get(
new_version_url, headers=headers).text

# Fetch Previous Version
prev_version_url = f"{base_url}/items?path={
file_path}&versionDescriptor[versionType]=branch&versionDescriptor[version]={target_branch}&api-version={API_VERSION}"
prev_version_content = requests.get(
prev_version_url, headers=headers).text

# Generate Diff
diff = compare_file_contents(
file_path, prev_version_content, new_version_content)
messages.append(
{"role": "user", "content": f"Changes for file: {file_path}\n{diff}"})

except Exception as e:
print(f"Failed to process file {file_path}: {e}")
continue

# OpenAI Completion
response = openai.ChatCompletion.create(
model="gpt-4o-mini",
messages=messages,
max_tokens=5000
)
comment = response["choices"][0]["message"]["content"]

# Post Comment
comment_cost = calculate_cost(
response["usage"]["prompt_tokens"],
response["usage"]["completion_tokens"]
)
comment_url = f"{base_url}/pullRequests/{pr_id}/threads?api-version=7.0"
comment_body = {
"comments": [{"content": f"{comment}\n\nTotal cost: ${comment_cost:.2f}"}],
"status": "active"
}
requests.post(comment_url, headers=headers, json=comment_body)


if __name__ == "__main__":
main()
16 changes: 16 additions & 0 deletions template/azure-pipeline/commit-validator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
stages:
- ${{ if eq(variables['Build.Reason'], 'PullRequest') }}:
- stage: Commit_validator
displayName: Conventional Commit Validator
condition: eq(variables['Build.Reason'], 'PullRequest')
jobs:
- job: Commit_validator
displayName: "Validate Conventional Commits"
steps:
- task: CommitMessageValidator@1
displayName: "Validate Conventional Commits"
inputs:
regExPattern: '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\.\-\p{Extended_Pictographic}]+\))?(!)?: ([\w \p{Extended_Pictographic}])+([\s\S]*)'
regExFlags: "um"
allCommitsMustMatch: true
prMode: true
52 changes: 52 additions & 0 deletions template/azure-pipeline/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
parameters:
- name: environment
type: string

jobs:
- deployment: "Deploy_${{ parameters.environment }}"
displayName: "Deploy [${{ parameters.environment }}]"
environment: ${{ parameters.environment }}
variables:
AZURE_ENVIRONMENT_NAME: "$(APP_NAME)-${{ parameters.environment }}"
strategy:
runOnce:
deploy:
steps:
- checkout: self

- task: UseDotNet@2
displayName: "Install .NET 9 SDK"
inputs:
packageType: "sdk"
version: $(DOTNET_VERSION)
installationPath: $(Agent.ToolsDirectory)/dotnet

- script: |
curl -fsSL https://aka.ms/install-azd.sh | bash
echo 'Azure Developer CLI installed'
displayName: "Install Azure Developer CLI"
- task: AzureCLI@2
displayName: Provision
inputs:
azureSubscription: $(AZURE_SERVICE_CONNECTION_NAME)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
azd config set auth.useAzCliAuth "true"
azd provision --no-prompt
addSpnToEnvironment: true
visibleAzLogin: true
env:
AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID)
AZURE_ENV_NAME: $(AZURE_ENVIRONMENT_NAME)
AZURE_LOCATION: $(AZURE_LOCATION)

- task: AzureCLI@2
displayName: Deploy
inputs:
azureSubscription: $(AZURE_SERVICE_CONNECTION_NAME)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
azd deploy --no-prompt
20 changes: 20 additions & 0 deletions template/azure-pipeline/environments-loop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
parameters:
- name: environments
type: object
default:
# - dev
# - qa
# - uat
# - stag
# - prod

stages:
- ${{ if not(eq(variables['Build.Reason'], 'PullRequest')) }}:
- ${{ each environment in parameters.environments }}:
- stage: "Deploy_${{ environment }}"
displayName: "Deploy [${{ environment }}]"
dependsOn: Build
jobs:
- template: deploy.yml
parameters:
environment: ${{ environment }}
8 changes: 8 additions & 0 deletions template/azure.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json

name: placeholder
services:
app:
language: dotnet
project: ./src/Placeholder.AppHost/Placeholder.AppHost.csproj
host: containerapp
6 changes: 3 additions & 3 deletions template/global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "9.0.100",
"rollForward": "disable"
"version": "9.0.101",
"rollForward": "minor"
}
}
}
15 changes: 8 additions & 7 deletions template/src/Placeholder.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
using Placeholder.AppHost;

var builder = DistributedApplication.CreateBuilder(args);

var weatherDb = builder.ConfigurePostgresDatabase("weather", builder.AddParameter("postgresUsername"), builder.AddParameter("postgresPassword", secret: true));
var postgres = builder.AddPostgres("postgres");

var weatherDb = postgres.AddDatabase("weather");

var apiService = builder.AddProject<Projects.Placeholder_ApiService>("apiservice")
var apiService = builder
.AddProject<Projects.Placeholder_ApiService>("apiservice")
.WithExternalHttpEndpoints()
.WithReference(weatherDb);

builder.AddProject<Projects.Placeholder_Migration>("migration")
.WithReference(weatherDb);
builder.AddProject<Projects.Placeholder_Migration>("migration").WithReference(weatherDb);

builder.AddProject<Projects.Placeholder_Web>("webfrontend")
builder
.AddProject<Projects.Placeholder_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(apiService);

Expand Down

0 comments on commit 2e9f429

Please sign in to comment.