From 72e0a9401a92f32473730176ec0f8d90755d352f Mon Sep 17 00:00:00 2001 From: Harsh-Microsoft Date: Mon, 1 Dec 2025 17:00:55 +0530 Subject: [PATCH 01/13] Update deployment guide and quota check documentation for VS Code Web usage --- docs/DeploymentGuide.md | 13 +++++++++++-- docs/images/vscodeweb_intialize.png | Bin 35612 -> 0 bytes docs/quota_check.md | 10 +++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) delete mode 100644 docs/images/vscodeweb_intialize.png diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 8abf4f58..d67bf4da 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -100,9 +100,18 @@ You can run this solution in VS Code Dev Containers, which will open the project 2. Sign in with your Azure account when prompted 3. Select the subscription where you want to deploy the solution 4. Wait for the environment to initialize (includes all deployment tools) -5. When prompted in the VS Code Web terminal, choose one of the available options shown below: +5. Once the solution opens, the **AI Foundry terminal** will automatically start running the following command to install the required dependencies: - ![VS Code Initial Prompt](./images/vscodeweb_intialize.png) + ```shell + sh install.sh + ``` + During this process, you’ll be prompted with the message: + ``` + What would you like to do with these files? + - Overwrite with versions from template + - Keep my existing files unchanged + ``` + Choose β€œ**Overwrite with versions from template**” and provide a unique environment name when prompted. 6. Continue with the [deploying steps](#deploying-with-azd) diff --git a/docs/images/vscodeweb_intialize.png b/docs/images/vscodeweb_intialize.png deleted file mode 100644 index 1ef8ce8174399d32bdaf028eb9a2c84a06b30f3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35612 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
  • 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 u%K5_p`C{4E@BbG%UGs1N diff --git a/docs/quota_check.md b/docs/quota_check.md index 3f922743..37da7db5 100644 --- a/docs/quota_check.md +++ b/docs/quota_check.md @@ -5,9 +5,12 @@ Before deploying the accelerator, **ensure sufficient quota availability** for t ### Login if you have not done so already ``` -azd auth login +az login +``` +If using VS Code Web: +``` +az login --use-device-code ``` - ### πŸ“Œ Default Models & Capacities: ``` @@ -75,7 +78,7 @@ The final table lists regions with available quota. You can select any of these ### **If using VS Code or Codespaces** 1. Open the terminal in VS Code or Codespaces. -2. If you're using VS Code, click the dropdown on the right side of the terminal window, and select `Git Bash`. +2. If you're using VS Code, click the dropdown on the right side of the terminal window, and select `Git Bash` / `bash`. ![git_bash](images/read_me/git_bash.png) 3. Navigate to the `scripts` folder where the script files are located and make the script as executable: ```sh @@ -97,4 +100,5 @@ The final table lists regions with available quota. You can select any of these curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash az login ``` + > Note: Use `az login --use-device-code` in VS Code Web. 6. Rerun the script after installing Azure CLI. From 26aa44bf6c1512319c8c2a3fb7f9e45047ae41da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:14:58 +0000 Subject: [PATCH 02/13] Initial plan From d2a1f23445e0b1eeb2178ccf59b65d77305a1eba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:17:07 +0000 Subject: [PATCH 03/13] Add dgp10801 to CODEOWNERS file Co-authored-by: Prekshith-Microsoft <216912978+Prekshith-Microsoft@users.noreply.github.com> --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 52471219..c850ddd2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,4 +2,4 @@ # Each line is a file pattern followed by one or more owners. # These owners will be the default owners for everything in the repo. -* @Avijit-Microsoft @Roopan-Microsoft @Prajwal-Microsoft @aniaroramsft @marktayl1 @Vinay-Microsoft @toherman-msft @nchandhi +* @Avijit-Microsoft @Roopan-Microsoft @Prajwal-Microsoft @aniaroramsft @marktayl1 @Vinay-Microsoft @toherman-msft @nchandhi @dgp10801 From 961ac74543e606747a63e5b09b7938de1c04aa17 Mon Sep 17 00:00:00 2001 From: Shreyas-Microsoft Date: Fri, 12 Dec 2025 12:07:44 +0530 Subject: [PATCH 04/13] add known issues --- docs/DeploymentGuide.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index d67bf4da..8e40ee77 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -257,3 +257,44 @@ To help you get started, here's the [Sample Workflow](./SampleWorkflow.md) you c provisioned using `azd provision` or `azd up`, a `.env` file is automatically generated in the `.azure//.env` file. To get your `` run `azd env list` to see which env is default. 5. Ensure that `APP_ENV` is set to "**dev**" in your `.env` file. + +### Known Issues + +**Unable to update/add environment variables in Azure Container App** + +You may encounter issues when attempting to modify environment variables or container configuration in Azure Container Apps: + +**Affected Scenarios:** +- **App Authentication Setup:** When adding authentication-related environment variables (CRUD operations on env variables) +- **Container Configuration:** When trying to edit ACR name, image, or tag information for Container Apps + +**Root Cause:** +This is an ongoing issue in Azure that affects the Azure Portal's ability to update Container Apps configurations. + +**Workaround - Use Azure CLI:** + +Until this issue is resolved, use Azure CLI commands to add or update environment variables and container configurations: + +**For Environment Variables:** +```bash +# Update environment variables +az containerapp update \ + --name \ + --resource-group \ + --set-env-vars "KEY1=value1" "KEY2=value2" +``` + +**For Container Image Updates:** +```bash +# Update container image +az containerapp update \ + --name \ + --resource-group \ + --image /: +``` + +πŸ“– **Detailed CLI Documentation:** +- [Manage environment variables](https://learn.microsoft.com/en-us/azure/container-apps/environment-variables?tabs=cli) +- [Manage revisions](https://learn.microsoft.com/en-us/azure/container-apps/revisions-manage?tabs=bash) + +> **Note:** This is a temporary workaround. The documentation will be updated once the Azure Portal issue is resolved. \ No newline at end of file From d7fc9c76d448533079874fcfff9f87d0b3ecb4dd Mon Sep 17 00:00:00 2001 From: Shreyas-Microsoft Date: Fri, 12 Dec 2025 12:09:27 +0530 Subject: [PATCH 05/13] remove # --- docs/DeploymentGuide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 8e40ee77..e5c3745f 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -258,7 +258,7 @@ To help you get started, here's the [Sample Workflow](./SampleWorkflow.md) you c file. To get your `` run `azd env list` to see which env is default. 5. Ensure that `APP_ENV` is set to "**dev**" in your `.env` file. -### Known Issues +## Known Issues **Unable to update/add environment variables in Azure Container App** From 65a605746693c42f14400c0492c002c08e5d33ea Mon Sep 17 00:00:00 2001 From: Roopan-Microsoft <168007406+Roopan-Microsoft@users.noreply.github.com> Date: Tue, 16 Dec 2025 00:24:00 +0530 Subject: [PATCH 06/13] Delete src/frontend/src/components/errorWarningSection.tsx --- .../src/components/errorWarningSection.tsx | 58 ------------------- 1 file changed, 58 deletions(-) delete mode 100644 src/frontend/src/components/errorWarningSection.tsx diff --git a/src/frontend/src/components/errorWarningSection.tsx b/src/frontend/src/components/errorWarningSection.tsx deleted file mode 100644 index 86eb5b89..00000000 --- a/src/frontend/src/components/errorWarningSection.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from "react"; -import { ErrorWarningProps } from "../types/types"; - -export const ErrorWarningSection: React.FC = ({ title, count, type, items }) => { - return ( -
    -
    -
    -
    - {title} ({count}) -
    -
    -
    - -
    -
    -
    -
    -
    - {items.map((item, index) => ( -
    -
    -
    -
    - -
    -
    -
    -
    - {item.fileName} ({item.count}) -
    -
    -
    source
    -
    -
    -
    - {item.messages.map((message, msgIndex) => ( -
    -
    - -
    -
    -
    -
    - {message.message} -
    -
    -
    {message.location}
    -
    -
    -
    - ))} -
    - ))} -
    -
    - ); -}; \ No newline at end of file From a04b7b884722ff50f92f41cac19b96465b887093 Mon Sep 17 00:00:00 2001 From: Shreyas-Microsoft Date: Tue, 16 Dec 2025 08:57:40 +0530 Subject: [PATCH 07/13] Remove known issues about Azure Container Apps Removed known issues section regarding environment variables and container configuration in Azure Container Apps, including affected scenarios, root cause, workarounds, and related CLI documentation. --- docs/DeploymentGuide.md | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index e5c3745f..d67bf4da 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -257,44 +257,3 @@ To help you get started, here's the [Sample Workflow](./SampleWorkflow.md) you c provisioned using `azd provision` or `azd up`, a `.env` file is automatically generated in the `.azure//.env` file. To get your `` run `azd env list` to see which env is default. 5. Ensure that `APP_ENV` is set to "**dev**" in your `.env` file. - -## Known Issues - -**Unable to update/add environment variables in Azure Container App** - -You may encounter issues when attempting to modify environment variables or container configuration in Azure Container Apps: - -**Affected Scenarios:** -- **App Authentication Setup:** When adding authentication-related environment variables (CRUD operations on env variables) -- **Container Configuration:** When trying to edit ACR name, image, or tag information for Container Apps - -**Root Cause:** -This is an ongoing issue in Azure that affects the Azure Portal's ability to update Container Apps configurations. - -**Workaround - Use Azure CLI:** - -Until this issue is resolved, use Azure CLI commands to add or update environment variables and container configurations: - -**For Environment Variables:** -```bash -# Update environment variables -az containerapp update \ - --name \ - --resource-group \ - --set-env-vars "KEY1=value1" "KEY2=value2" -``` - -**For Container Image Updates:** -```bash -# Update container image -az containerapp update \ - --name \ - --resource-group \ - --image /: -``` - -πŸ“– **Detailed CLI Documentation:** -- [Manage environment variables](https://learn.microsoft.com/en-us/azure/container-apps/environment-variables?tabs=cli) -- [Manage revisions](https://learn.microsoft.com/en-us/azure/container-apps/revisions-manage?tabs=bash) - -> **Note:** This is a temporary workaround. The documentation will be updated once the Azure Portal issue is resolved. \ No newline at end of file From 12a7906a17d404be0e33d4e0510678864e469847 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Thu, 18 Dec 2025 10:27:36 +0530 Subject: [PATCH 08/13] added Deployment v2 workflow --- .github/workflows/deploy-linux.yml | 88 +++++ .github/workflows/deploy-orchestrator.yml | 138 +++++++ .github/workflows/deploy-windows.yml | 92 +++++ .github/workflows/job-cleanup-deployment.yml | 114 ++++++ .github/workflows/job-deploy-linux.yml | 180 ++++++++++ .github/workflows/job-deploy-windows.yml | 181 ++++++++++ .github/workflows/job-deploy.yml | 355 +++++++++++++++++++ .github/workflows/job-docker-build.yml | 113 ++++++ .github/workflows/job-send-notification.yml | 224 ++++++++++++ .github/workflows/test-automation-v2.yml | 185 ++++++++++ infra/main.bicep | 7 +- infra/main.parameters.json | 6 + infra/main.waf.parameters.json | 6 + 13 files changed, 1687 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/deploy-linux.yml create mode 100644 .github/workflows/deploy-orchestrator.yml create mode 100644 .github/workflows/deploy-windows.yml create mode 100644 .github/workflows/job-cleanup-deployment.yml create mode 100644 .github/workflows/job-deploy-linux.yml create mode 100644 .github/workflows/job-deploy-windows.yml create mode 100644 .github/workflows/job-deploy.yml create mode 100644 .github/workflows/job-docker-build.yml create mode 100644 .github/workflows/job-send-notification.yml create mode 100644 .github/workflows/test-automation-v2.yml diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-linux.yml new file mode 100644 index 00000000..aecf5071 --- /dev/null +++ b/.github/workflows/deploy-linux.yml @@ -0,0 +1,88 @@ +name: Deploy-Test-Cleanup (v2) Linux +on: + workflow_dispatch: + inputs: + azure_location: + description: 'Azure Location For Deployment' + required: false + default: 'australiaeast' + type: choice + options: + - 'australiaeast' + - 'centralus' + - 'eastasia' + - 'eastus2' + - 'japaneast' + - 'northeurope' + - 'southeastasia' + - 'uksouth' + resource_group_name: + description: 'Resource Group Name (Optional)' + required: false + default: '' + type: string + waf_enabled: + description: 'Enable WAF' + required: false + default: false + type: boolean + EXP: + description: 'Enable EXP' + required: false + default: false + type: boolean + build_docker_image: + description: 'Build & Push Docker Image (Optional)' + required: false + default: false + type: boolean + cleanup_resources: + description: 'Cleanup Deployed Resources' + required: false + default: false + type: boolean + run_e2e_tests: + description: 'Run End-to-End Tests' + required: false + default: 'GoldenPath-Testing' + type: choice + options: + - 'GoldenPath-Testing' + - 'Smoke-Testing' + - 'None' + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + description: 'Log Analytics Workspace ID (Optional)' + required: false + default: '' + type: string + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + description: 'AI Project Resource ID (Optional)' + required: false + default: '' + type: string + existing_webapp_url: + description: 'Existing Container WebApp URL (Skips Deployment)' + required: false + default: '' + type: string + + schedule: + - cron: '0 5,17 * * *' # Runs at 5:00 AM and 5:00 PM GMT + +jobs: + Run: + uses: ./.github/workflows/deploy-orchestrator.yml + with: + runner_os: ubuntu-latest + azure_location: ${{ github.event.inputs.azure_location || 'australiaeast' }} + resource_group_name: ${{ github.event.inputs.resource_group_name || '' }} + waf_enabled: ${{ github.event.inputs.waf_enabled == 'true' }} + EXP: ${{ github.event.inputs.EXP == 'true' }} + build_docker_image: ${{ github.event.inputs.build_docker_image == 'true' }} + cleanup_resources: ${{ github.event.inputs.cleanup_resources == 'true' }} + run_e2e_tests: ${{ github.event.inputs.run_e2e_tests || 'GoldenPath-Testing' }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID || '' }} + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID || '' }} + existing_webapp_url: ${{ github.event.inputs.existing_webapp_url || '' }} + trigger_type: ${{ github.event_name }} + secrets: inherit diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml new file mode 100644 index 00000000..23a59b4d --- /dev/null +++ b/.github/workflows/deploy-orchestrator.yml @@ -0,0 +1,138 @@ +name: Deployment orchestrator + +on: + workflow_call: + inputs: + runner_os: + description: 'Runner OS (ubuntu-latest or windows-latest)' + required: true + type: string + azure_location: + description: 'Azure Location For Deployment' + required: false + default: 'australiaeast' + type: string + resource_group_name: + description: 'Resource Group Name (Optional)' + required: false + default: '' + type: string + waf_enabled: + description: 'Enable WAF' + required: false + default: false + type: boolean + EXP: + description: 'Enable EXP' + required: false + default: false + type: boolean + build_docker_image: + description: 'Build And Push Docker Image (Optional)' + required: false + default: false + type: boolean + cleanup_resources: + description: 'Cleanup Deployed Resources' + required: false + default: false + type: boolean + run_e2e_tests: + description: 'Run End-to-End Tests' + required: false + default: 'GoldenPath-Testing' + type: string + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + description: 'Log Analytics Workspace ID (Optional)' + required: false + default: '' + type: string + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + description: 'AI Project Resource ID (Optional)' + required: false + default: '' + type: string + existing_webapp_url: + description: 'Existing Container WebApp URL (Skips Deployment)' + required: false + default: '' + type: string + trigger_type: + description: 'Trigger type (workflow_dispatch, pull_request, schedule)' + required: true + type: string + +env: + AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + +jobs: + docker-build: + uses: ./.github/workflows/job-docker-build.yml + with: + trigger_type: ${{ inputs.trigger_type }} + build_docker_image: ${{ inputs.build_docker_image }} + secrets: inherit + + deploy: + if: "!cancelled() && (inputs.trigger_type != 'workflow_dispatch' || inputs.existing_webapp_url == '' || inputs.existing_webapp_url == null)" + needs: docker-build + uses: ./.github/workflows/job-deploy.yml + with: + trigger_type: ${{ inputs.trigger_type }} + runner_os: ${{ inputs.runner_os }} + azure_location: ${{ inputs.azure_location }} + resource_group_name: ${{ inputs.resource_group_name }} + waf_enabled: ${{ inputs.waf_enabled }} + EXP: ${{ inputs.EXP }} + build_docker_image: ${{ inputs.build_docker_image }} + existing_webapp_url: ${{ inputs.existing_webapp_url }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + docker_image_tag: ${{ needs.docker-build.outputs.IMAGE_TAG }} + run_e2e_tests: ${{ inputs.run_e2e_tests }} + cleanup_resources: ${{ inputs.cleanup_resources }} + secrets: inherit + + e2e-test: + if: "!cancelled() && ((needs.deploy.result == 'success' && needs.deploy.outputs.CONTAINER_WEB_APPURL != '') || (inputs.existing_webapp_url != '' && inputs.existing_webapp_url != null)) && (inputs.trigger_type != 'workflow_dispatch' || (inputs.run_e2e_tests != 'None' && inputs.run_e2e_tests != '' && inputs.run_e2e_tests != null))" + needs: [docker-build, deploy] + uses: ./.github/workflows/test-automation-v2.yml + with: + TEST_URL: ${{ needs.deploy.outputs.CONTAINER_WEB_APPURL || inputs.existing_webapp_url }} + TEST_SUITE: ${{ inputs.trigger_type == 'workflow_dispatch' && inputs.run_e2e_tests || 'GoldenPath-Testing' }} + secrets: inherit + + send-notification: + if: "!cancelled()" + needs: [docker-build, deploy, e2e-test] + uses: ./.github/workflows/job-send-notification.yml + with: + trigger_type: ${{ inputs.trigger_type }} + waf_enabled: ${{ inputs.waf_enabled }} + EXP: ${{ inputs.EXP }} + run_e2e_tests: ${{ inputs.run_e2e_tests }} + existing_webapp_url: ${{ inputs.existing_webapp_url }} + deploy_result: ${{ needs.deploy.result }} + e2e_test_result: ${{ needs.e2e-test.result }} + CONTAINER_WEB_APPURL: ${{ needs.deploy.outputs.CONTAINER_WEB_APPURL }} + RESOURCE_GROUP_NAME: ${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }} + QUOTA_FAILED: ${{ needs.deploy.outputs.QUOTA_FAILED }} + TEST_SUCCESS: ${{ needs.e2e-test.outputs.TEST_SUCCESS }} + TEST_REPORT_URL: ${{ needs.e2e-test.outputs.TEST_REPORT_URL }} + secrets: inherit + + cleanup-deployment: + if: "!cancelled() && needs.deploy.result == 'success' && needs.deploy.outputs.RESOURCE_GROUP_NAME != '' && inputs.existing_webapp_url == '' && (inputs.trigger_type != 'workflow_dispatch' || inputs.cleanup_resources)" + needs: [docker-build, deploy, e2e-test] + uses: ./.github/workflows/job-cleanup-deployment.yml + with: + runner_os: ${{ inputs.runner_os }} + trigger_type: ${{ inputs.trigger_type }} + cleanup_resources: ${{ inputs.cleanup_resources }} + existing_webapp_url: ${{ inputs.existing_webapp_url }} + RESOURCE_GROUP_NAME: ${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }} + AZURE_LOCATION: ${{ needs.deploy.outputs.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ needs.deploy.outputs.AZURE_ENV_OPENAI_LOCATION }} + ENV_NAME: ${{ needs.deploy.outputs.ENV_NAME }} + IMAGE_TAG: ${{ needs.deploy.outputs.IMAGE_TAG }} + secrets: inherit diff --git a/.github/workflows/deploy-windows.yml b/.github/workflows/deploy-windows.yml new file mode 100644 index 00000000..5ac5f7c2 --- /dev/null +++ b/.github/workflows/deploy-windows.yml @@ -0,0 +1,92 @@ +name: Deploy-Test-Cleanup (v2) Windows +on: + workflow_dispatch: + inputs: + azure_location: + description: 'Azure Location For Deployment' + required: false + default: 'australiaeast' + type: choice + options: + - 'australiaeast' + - 'centralus' + - 'eastasia' + - 'eastus2' + - 'japaneast' + - 'northeurope' + - 'southeastasia' + - 'uksouth' + resource_group_name: + description: 'Resource Group Name (Optional)' + required: false + default: '' + type: string + + waf_enabled: + description: 'Enable WAF' + required: false + default: false + type: boolean + EXP: + description: 'Enable EXP' + required: false + default: false + type: boolean + build_docker_image: + description: 'Build & Push Docker Image (Optional)' + required: false + default: false + type: boolean + + cleanup_resources: + description: 'Cleanup Deployed Resources' + required: false + default: false + type: boolean + + run_e2e_tests: + description: 'Run End-to-End Tests' + required: false + default: 'GoldenPath-Testing' + type: choice + options: + - 'GoldenPath-Testing' + - 'Smoke-Testing' + - 'None' + + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + description: 'Log Analytics Workspace ID (Optional)' + required: false + default: '' + type: string + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + description: 'AI Project Resource ID (Optional)' + required: false + default: '' + type: string + existing_webapp_url: + description: 'Existing Container WebApp URL (Skips Deployment)' + required: false + default: '' + type: string + + # schedule: + # - cron: '0 5,17 * * *' # Runs at 5:00 AM and 5:00 PM GMT + +jobs: + Run: + uses: ./.github/workflows/deploy-orchestrator.yml + with: + runner_os: windows-latest + azure_location: ${{ github.event.inputs.azure_location || 'australiaeast' }} + resource_group_name: ${{ github.event.inputs.resource_group_name || '' }} + waf_enabled: ${{ github.event.inputs.waf_enabled == 'true' }} + EXP: ${{ github.event.inputs.EXP == 'true' }} + build_docker_image: ${{ github.event.inputs.build_docker_image == 'true' }} + cleanup_resources: ${{ github.event.inputs.cleanup_resources == 'true' }} + run_e2e_tests: ${{ github.event.inputs.run_e2e_tests || 'GoldenPath-Testing' }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID || '' }} + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID || '' }} + existing_webapp_url: ${{ github.event.inputs.existing_webapp_url || '' }} + trigger_type: ${{ github.event_name }} + secrets: inherit diff --git a/.github/workflows/job-cleanup-deployment.yml b/.github/workflows/job-cleanup-deployment.yml new file mode 100644 index 00000000..6b920a4e --- /dev/null +++ b/.github/workflows/job-cleanup-deployment.yml @@ -0,0 +1,114 @@ +name: Cleanup Deployment Job +on: + workflow_call: + inputs: + runner_os: + description: 'Runner OS (ubuntu-latest or windows-latest)' + required: true + type: string + trigger_type: + description: 'Trigger type (workflow_dispatch, pull_request, schedule)' + required: true + type: string + cleanup_resources: + description: 'Cleanup Deployed Resources' + required: false + default: false + type: boolean + existing_webapp_url: + description: 'Existing Container WebApp URL (Skips Deployment)' + required: false + default: '' + type: string + RESOURCE_GROUP_NAME: + description: 'Resource Group Name to cleanup' + required: true + type: string + AZURE_LOCATION: + description: 'Azure Location' + required: true + type: string + AZURE_ENV_OPENAI_LOCATION: + description: 'Azure OpenAI Location' + required: true + type: string + ENV_NAME: + description: 'Environment Name' + required: true + type: string + IMAGE_TAG: + description: 'Docker Image Tag' + required: true + type: string + +jobs: + cleanup-deployment: + runs-on: ${{ inputs.runner_os }} + continue-on-error: true + env: + RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} + AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + ENV_NAME: ${{ inputs.ENV_NAME }} + IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + steps: + - name: Setup Azure CLI + shell: bash + run: | + if [[ "${{ runner.os }}" == "Linux" ]]; then + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + fi + az --version + + - name: Login to Azure + shell: bash + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Delete Resource Group (Optimized Cleanup) + id: delete_rg + shell: bash + run: | + set -e + echo "πŸ—‘οΈ Starting optimized resource cleanup..." + echo "Deleting resource group: ${{ env.RESOURCE_GROUP_NAME }}" + + az group delete \ + --name "${{ env.RESOURCE_GROUP_NAME }}" \ + --yes \ + --no-wait + + echo "βœ… Resource group deletion initiated (running asynchronously)" + echo "Note: Resources will be cleaned up in the background" + + - name: Logout from Azure + if: always() + shell: bash + run: | + azd auth logout || true + az logout || echo "Warning: Failed to logout from Azure CLI" + echo "Logged out from Azure." + + - name: Generate Cleanup Job Summary + if: always() + shell: bash + run: | + echo "## 🧹 Cleanup Job Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| **Resource Group deletion Status** | ${{ steps.delete_rg.outcome == 'success' && 'βœ… Initiated' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Resource Group** | \`${{ env.RESOURCE_GROUP_NAME }}\` |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "${{ steps.delete_rg.outcome }}" == "success" ]]; then + echo "### βœ… Cleanup Details" >> $GITHUB_STEP_SUMMARY + echo "- Successfully initiated deletion for Resource Group \`${{ env.RESOURCE_GROUP_NAME }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Cleanup Failed" >> $GITHUB_STEP_SUMMARY + echo "- Cleanup process encountered an error" >> $GITHUB_STEP_SUMMARY + echo "- Manual cleanup may be required for:" >> $GITHUB_STEP_SUMMARY + echo " - Resource Group: \`${{ env.RESOURCE_GROUP_NAME }}\`" >> $GITHUB_STEP_SUMMARY + echo "- Check the cleanup-deployment job logs for detailed error information" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml new file mode 100644 index 00000000..4bd437b1 --- /dev/null +++ b/.github/workflows/job-deploy-linux.yml @@ -0,0 +1,180 @@ +name: Deploy Steps - Linux + +on: + workflow_call: + inputs: + ENV_NAME: + required: true + type: string + AZURE_ENV_OPENAI_LOCATION: + required: true + type: string + AZURE_LOCATION: + required: true + type: string + RESOURCE_GROUP_NAME: + required: true + type: string + IMAGE_TAG: + required: true + type: string + BUILD_DOCKER_IMAGE: + required: true + type: string + EXP: + required: true + type: string + WAF_ENABLED: + required: false + type: string + default: 'false' + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + required: false + type: string + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + required: false + type: string + outputs: + CONTAINER_WEB_APPURL: + description: "Container Web App URL" + value: ${{ jobs.deploy-linux.outputs.WEBAPP_URL }} + +jobs: + deploy-linux: + runs-on: ubuntu-latest + env: + AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + outputs: + WEBAPP_URL: ${{ steps.get_output_linux.outputs.WEBAPP_URL }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Configure Parameters Based on WAF Setting + shell: bash + run: | + if [[ "${{ inputs.WAF_ENABLED }}" == "true" ]]; then + cp infra/main.waf.parameters.json infra/main.parameters.json + echo "βœ… Successfully copied WAF parameters to main parameters file" + else + echo "πŸ”§ Configuring Non-WAF deployment - using default main.parameters.json..." + fi + + - name: Setup Azure CLI + shell: bash + run: | + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + + - name: Setup Azure Developer CLI (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + curl -fsSL https://aka.ms/install-azd.sh | sudo bash + azd version + + - name: Login to AZD + id: login-azure + shell: bash + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + azd auth login --client-id ${{ secrets.AZURE_CLIENT_ID }} --client-secret ${{ secrets.AZURE_CLIENT_SECRET }} --tenant-id ${{ secrets.AZURE_TENANT_ID }} + + - name: Deploy using azd up and extract values (Linux) + id: get_output_linux + shell: bash + run: | + set -e + + echo "Creating environment..." + azd env new ${{ inputs.ENV_NAME }} --no-prompt + echo "Environment created: ${{ inputs.ENV_NAME }}" + + echo "Setting default subscription..." + azd config set defaults.subscription ${{ vars.AZURE_SUBSCRIPTION_ID }} + + # Set additional parameters + azd env set AZURE_SUBSCRIPTION_ID="${{ vars.AZURE_SUBSCRIPTION_ID }}" + azd env set AZURE_ENV_AI_SERVICE_LOCATION="${{ inputs.AZURE_ENV_OPENAI_LOCATION }}" + azd env set AZURE_LOCATION="${{ inputs.AZURE_LOCATION }}" + azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" + azd env set AZURE_ENV_IMAGETAG="${{ inputs.IMAGE_TAG }}" + + if [[ "${{ inputs.BUILD_DOCKER_IMAGE }}" == "true" ]]; then + ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") + azd env set AZURE_ENV_ACR_NAME="$ACR_NAME" + echo "Set ACR name to: $ACR_NAME" + else + echo "Skipping ACR name configuration (using existing image)" + fi + + if [[ "${{ inputs.EXP }}" == "true" ]]; then + echo "βœ… EXP ENABLED - Setting EXP parameters..." + + if [[ -n "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]]; then + EXP_LOG_ANALYTICS_ID="${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + else + EXP_LOG_ANALYTICS_ID="${{ vars.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + fi + + if [[ -n "${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" ]]; then + EXP_AI_PROJECT_ID="${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" + else + EXP_AI_PROJECT_ID="${{ vars.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" + fi + + echo "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: $EXP_LOG_ANALYTICS_ID" + echo "AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: $EXP_AI_PROJECT_ID" + azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID="$EXP_LOG_ANALYTICS_ID" + azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID="$EXP_AI_PROJECT_ID" + else + echo "❌ EXP DISABLED - Skipping EXP parameters" + fi + + azd up --no-prompt + echo "βœ… Deployment succeeded" + + WEBAPP_URL=$(azd env get-value WEB_APP_URL) + echo "WEBAPP_URL=${WEBAPP_URL}" >> $GITHUB_ENV + echo "WEBAPP_URL=${WEBAPP_URL}" >> $GITHUB_OUTPUT + + - name: Assign Contributor role to Service Principal + if: always() + run: | + echo "Assigning Contributor role to SPN for RG: ${{ inputs.RESOURCE_GROUP_NAME }}" + az role assignment create \ + --assignee ${{ secrets.AZURE_CLIENT_ID }} \ + --role "Contributor" \ + --scope /subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/resourceGroups/${{ inputs.RESOURCE_GROUP_NAME }} + + - name: Generate Deployment Summary + if: always() + shell: bash + run: | + echo "## πŸš€ Deploy Job Summary (Linux)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| **Job Status** | ${{ job.status == 'success' && 'βœ… Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Resource Group** | \`${{ inputs.RESOURCE_GROUP_NAME }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Configuration Type** | \`${{ inputs.WAF_ENABLED == 'true' && inputs.EXP == 'true' && 'WAF + EXP' || inputs.WAF_ENABLED == 'true' && inputs.EXP != 'true' && 'WAF + Non-EXP' || inputs.WAF_ENABLED != 'true' && inputs.EXP == 'true' && 'Non-WAF + EXP' || 'Non-WAF + Non-EXP' }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure Region (Infrastructure)** | \`${{ inputs.AZURE_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure OpenAI Region** | \`${{ inputs.AZURE_ENV_OPENAI_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Docker Image Tag** | \`${{ inputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "${{ job.status }}" == "success" ]]; then + echo "### βœ… Deployment Details" >> $GITHUB_STEP_SUMMARY + echo "- **Container Web App URL**: [${{ steps.get_output_linux.outputs.WEBAPP_URL }}](${{ steps.get_output_linux.outputs.WEBAPP_URL }})" >> $GITHUB_STEP_SUMMARY + echo "- Successfully deployed to Azure with all resources configured" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Deployment Failed" >> $GITHUB_STEP_SUMMARY + echo "- Deployment process encountered an error" >> $GITHUB_STEP_SUMMARY + echo "- Check the deployment steps above for detailed error information" >> $GITHUB_STEP_SUMMARY + fi + + - name: Logout from Azure + if: always() + shell: bash + run: | + az logout || true + echo "Logged out from Azure." diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml new file mode 100644 index 00000000..fc016103 --- /dev/null +++ b/.github/workflows/job-deploy-windows.yml @@ -0,0 +1,181 @@ +name: Deploy Steps - Windows + +on: + workflow_call: + inputs: + ENV_NAME: + required: true + type: string + AZURE_ENV_OPENAI_LOCATION: + required: true + type: string + AZURE_LOCATION: + required: true + type: string + RESOURCE_GROUP_NAME: + required: true + type: string + IMAGE_TAG: + required: true + type: string + BUILD_DOCKER_IMAGE: + required: true + type: string + EXP: + required: true + type: string + WAF_ENABLED: + required: false + type: string + default: 'false' + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + required: false + type: string + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + required: false + type: string + outputs: + CONTAINER_WEB_APPURL: + description: "Container Web App URL" + value: ${{ jobs.deploy-windows.outputs.WEBAPP_URL }} + +jobs: + deploy-windows: + runs-on: windows-latest + env: + AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + outputs: + WEBAPP_URL: ${{ steps.get_output_windows.outputs.WEBAPP_URL }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Configure Parameters Based on WAF Setting + shell: bash + run: | + if [[ "${{ inputs.WAF_ENABLED }}" == "true" ]]; then + cp infra/main.waf.parameters.json infra/main.parameters.json + echo "βœ… Successfully copied WAF parameters to main parameters file" + else + echo "πŸ”§ Configuring Non-WAF deployment - using default main.parameters.json..." + fi + + - name: Setup Azure Developer CLI (Windows) + uses: Azure/setup-azd@v2 + + - name: Login to AZD + id: login-azure + shell: bash + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + azd auth login --client-id ${{ secrets.AZURE_CLIENT_ID }} --client-secret ${{ secrets.AZURE_CLIENT_SECRET }} --tenant-id ${{ secrets.AZURE_TENANT_ID }} + + + - name: Deploy using azd up and extract values (Windows) + id: get_output_windows + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + Write-Host "Starting azd deployment..." + Write-Host "EXP: ${{ inputs.EXP }}" + Write-Host "Using Docker Image Tag: ${{ inputs.IMAGE_TAG }}" + + Write-Host "Creating environment..." + azd env new ${{ inputs.ENV_NAME }} --no-prompt + Write-Host "Environment created: ${{ inputs.ENV_NAME }}" + + Write-Host "Setting default subscription..." + azd config set defaults.subscription ${{ vars.AZURE_SUBSCRIPTION_ID }} + + # Set additional parameters + azd env set AZURE_SUBSCRIPTION_ID="${{ vars.AZURE_SUBSCRIPTION_ID }}" + azd env set AZURE_ENV_AI_SERVICE_LOCATION="${{ inputs.AZURE_ENV_OPENAI_LOCATION }}" + azd env set AZURE_LOCATION="${{ inputs.AZURE_LOCATION }}" + azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" + azd env set AZURE_ENV_IMAGETAG="${{ inputs.IMAGE_TAG }}" + + # Set ACR name only when building Docker image + if ("${{ inputs.BUILD_DOCKER_IMAGE }}" -eq "true") { + $ACR_NAME = "${{ secrets.ACR_TEST_LOGIN_SERVER }}" + azd env set AZURE_ENV_ACR_NAME="$ACR_NAME" + Write-Host "Set ACR name to: $ACR_NAME" + } else { + Write-Host "Skipping ACR name configuration (using existing image)" + } + + if ("${{ inputs.EXP }}" -eq "true") { + Write-Host "EXP ENABLED βœ… - Setting EXP parameters..." + + # Set EXP variables dynamically + if ("${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" -ne "") { + $EXP_LOG_ANALYTICS_ID = "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + } else { + $EXP_LOG_ANALYTICS_ID = "${{ vars.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + } + + if ("${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" -ne "") { + $EXP_AI_PROJECT_ID = "${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" + } else { + $EXP_AI_PROJECT_ID = "${{ vars.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" + } + + Write-Host "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: $EXP_LOG_ANALYTICS_ID" + Write-Host "AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: $EXP_AI_PROJECT_ID" + azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID="$EXP_LOG_ANALYTICS_ID" + azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID="$EXP_AI_PROJECT_ID" + } else { + Write-Host "EXP DISABLED - Skipping EXP parameters" + } + + # Deploy using azd up + azd up --no-prompt + Write-Host "βœ… Deployment succeeded." + + $WEBAPP_URL = azd env get-value WEB_APP_URL + "WEBAPP_URL=$WEBAPP_URL" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "WEBAPP_URL=$WEBAPP_URL" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + + - name: Assign Contributor role to Service Principal + if: always() + shell: bash + run: | + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + echo "Assigning Contributor role to SPN for RG: ${{ inputs.RESOURCE_GROUP_NAME }}" + az role assignment create \ + --assignee ${{ secrets.AZURE_CLIENT_ID }} \ + --role "Contributor" \ + --scope /subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/resourceGroups/${{ inputs.RESOURCE_GROUP_NAME }} + + - name: Generate Deployment Summary + if: always() + shell: bash + run: | + echo "## πŸš€ Deploy Job Summary (Windows)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| **Job Status** | ${{ job.status == 'success' && 'βœ… Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Configuration Type** | \`${{ inputs.WAF_ENABLED == 'true' && inputs.EXP == 'true' && 'WAF + EXP' || inputs.WAF_ENABLED == 'true' && inputs.EXP != 'true' && 'WAF + Non-EXP' || inputs.WAF_ENABLED != 'true' && inputs.EXP == 'true' && 'Non-WAF + EXP' || 'Non-WAF + Non-EXP' }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Resource Group** | \`${{ inputs.RESOURCE_GROUP_NAME }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure Region (Infrastructure)** | \`${{ inputs.AZURE_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure OpenAI Region** | \`${{ inputs.AZURE_ENV_OPENAI_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Docker Image Tag** | \`${{ inputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ job.status }}" == "success" ]; then + echo "### βœ… Deployment Details" >> $GITHUB_STEP_SUMMARY + echo "- **Container Web App URL**: [${{ steps.get_output_windows.outputs.WEBAPP_URL }}](${{ steps.get_output_windows.outputs.WEBAPP_URL }})" >> $GITHUB_STEP_SUMMARY + echo "- Successfully deployed to Azure with all resources configured" >> $GITHUB_STEP_SUMMARY + echo "- Schemas registered and sample data uploaded successfully" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Deployment Failed" >> $GITHUB_STEP_SUMMARY + echo "- Deployment process encountered an error" >> $GITHUB_STEP_SUMMARY + echo "- Check the deployment steps above for detailed error information" >> $GITHUB_STEP_SUMMARY + fi + + - name: Logout from Azure + if: always() + shell: bash + run: | + az logout || true + echo "Logged out from Azure." diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml new file mode 100644 index 00000000..6ac84e16 --- /dev/null +++ b/.github/workflows/job-deploy.yml @@ -0,0 +1,355 @@ +name: Deploy Job + +on: + workflow_call: + inputs: + trigger_type: + description: 'Trigger type (workflow_dispatch, pull_request, schedule)' + required: true + type: string + runner_os: + description: 'Runner OS (ubuntu-latest or windows-latest)' + required: true + type: string + azure_location: + description: 'Azure Location For Deployment' + required: false + default: 'australiaeast' + type: string + resource_group_name: + description: 'Resource Group Name (Optional)' + required: false + default: '' + type: string + waf_enabled: + description: 'Enable WAF' + required: false + default: false + type: boolean + EXP: + description: 'Enable EXP' + required: false + default: false + type: boolean + build_docker_image: + description: 'Build And Push Docker Image (Optional)' + required: false + default: false + type: boolean + cleanup_resources: + description: 'Cleanup Deployed Resources' + required: false + default: false + type: boolean + run_e2e_tests: + description: 'Run End-to-End Tests' + required: false + default: 'GoldenPath-Testing' + type: string + existing_webapp_url: + description: 'Existing Container WebApp URL (Skips Deployment)' + required: false + default: '' + type: string + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + description: 'Log Analytics Workspace ID (Optional)' + required: false + default: '' + type: string + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + description: 'AI Project Resource ID (Optional)' + required: false + default: '' + type: string + docker_image_tag: + description: 'Docker Image Tag from build job' + required: false + default: '' + type: string + outputs: + RESOURCE_GROUP_NAME: + description: "Resource Group Name" + value: ${{ jobs.azure-setup.outputs.RESOURCE_GROUP_NAME }} + CONTAINER_WEB_APPURL: + description: "Container Web App URL" + value: ${{ jobs.deploy-linux.outputs.CONTAINER_WEB_APPURL || jobs.deploy-windows.outputs.CONTAINER_WEB_APPURL }} + ENV_NAME: + description: "Environment Name" + value: ${{ jobs.azure-setup.outputs.ENV_NAME }} + AZURE_LOCATION: + description: "Azure Location" + value: ${{ jobs.azure-setup.outputs.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: + description: "Azure OpenAI Location" + value: ${{ jobs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} + IMAGE_TAG: + description: "Docker Image Tag Used" + value: ${{ jobs.azure-setup.outputs.IMAGE_TAG }} + QUOTA_FAILED: + description: "Quota Check Failed Flag" + value: ${{ jobs.azure-setup.outputs.QUOTA_FAILED }} + +env: + GPT_MIN_CAPACITY: 150 + BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} + WAF_ENABLED: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.waf_enabled || false) || false }} + EXP: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.EXP || false) || false }} + CLEANUP_RESOURCES: ${{ inputs.trigger_type != 'workflow_dispatch' || inputs.cleanup_resources }} + RUN_E2E_TESTS: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.run_e2e_tests || 'GoldenPath-Testing') || 'GoldenPath-Testing' }} + BUILD_DOCKER_IMAGE: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.build_docker_image || false) || false }} + +jobs: + azure-setup: + name: Azure Setup + if: inputs.trigger_type != 'workflow_dispatch' || inputs.existing_webapp_url == '' || inputs.existing_webapp_url == null + runs-on: ubuntu-latest + outputs: + RESOURCE_GROUP_NAME: ${{ steps.check_create_rg.outputs.RESOURCE_GROUP_NAME }} + ENV_NAME: ${{ steps.generate_env_name.outputs.ENV_NAME }} + AZURE_LOCATION: ${{ steps.set_region.outputs.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ steps.set_region.outputs.AZURE_ENV_OPENAI_LOCATION }} + IMAGE_TAG: ${{ steps.determine_image_tag.outputs.IMAGE_TAG }} + QUOTA_FAILED: ${{ steps.quota_failure_output.outputs.QUOTA_FAILED }} + + steps: + - name: Validate and Auto-Configure EXP + shell: bash + run: | + echo "πŸ” Validating EXP configuration..." + + if [[ "${{ inputs.EXP }}" != "true" ]]; then + if [[ -n "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]] || [[ -n "${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" ]]; then + echo "πŸ”§ AUTO-ENABLING EXP: EXP parameter values were provided but EXP was not explicitly enabled." + echo "" + echo "You provided values for:" + [[ -n "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]] && echo " - Azure Log Analytics Workspace ID: '${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}'" + [[ -n "${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" ]] && echo " - Azure AI Project Resource ID: '${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}'" + echo "" + echo "βœ… Automatically enabling EXP to use these values." + echo "EXP=true" >> $GITHUB_ENV + echo "πŸ“Œ EXP has been automatically enabled for this deployment." + fi + fi + + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Login to Azure + shell: bash + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Run Quota Check + id: quota-check + run: | + export AZURE_CLIENT_ID=${{ secrets.AZURE_CLIENT_ID }} + export AZURE_TENANT_ID=${{ secrets.AZURE_TENANT_ID }} + export AZURE_CLIENT_SECRET=${{ secrets.AZURE_CLIENT_SECRET }} + export AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" + export AZURE_REGIONS="${{ vars.AZURE_REGIONS }}" + export GPT_MIN_CAPACITY=${{ env.GPT_MIN_CAPACITY }} + + chmod +x scripts/checkquota.sh + if ! scripts/checkquota.sh; then + # If quota check fails due to insufficient quota, set the flag + if grep -q "No region with sufficient quota found" scripts/checkquota.sh; then + echo "QUOTA_FAILED=true" >> $GITHUB_ENV + fi + exit 1 # Fail the pipeline if any other failure occurs + fi + + - name: Set Quota Failure Output + id: quota_failure_output + if: env.QUOTA_FAILED == 'true' + shell: bash + run: | + echo "QUOTA_FAILED=true" >> $GITHUB_OUTPUT + echo "Quota check failed - will notify via separate notification job" + + - name: Fail Pipeline if Quota Check Fails + if: env.QUOTA_FAILED == 'true' + shell: bash + run: exit 1 + + - name: Set Deployment Region + id: set_region + shell: bash + run: | + echo "Selected Region from Quota Check: $VALID_REGION" + echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_ENV + echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT + + if [[ "${{ inputs.trigger_type }}" == "workflow_dispatch" && -n "${{ inputs.azure_location }}" ]]; then + USER_SELECTED_LOCATION="${{ inputs.azure_location }}" + echo "Using user-selected Azure location: $USER_SELECTED_LOCATION" + echo "AZURE_LOCATION=$USER_SELECTED_LOCATION" >> $GITHUB_ENV + echo "AZURE_LOCATION=$USER_SELECTED_LOCATION" >> $GITHUB_OUTPUT + else + echo "Using location from quota check for automatic triggers: $VALID_REGION" + echo "AZURE_LOCATION=$VALID_REGION" >> $GITHUB_ENV + echo "AZURE_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT + fi + + - name: Generate Resource Group Name + id: generate_rg_name + shell: bash + run: | + # Check if a resource group name was provided as input + if [[ -n "${{ inputs.resource_group_name }}" ]]; then + echo "Using provided Resource Group name: ${{ inputs.resource_group_name }}" + echo "RESOURCE_GROUP_NAME=${{ inputs.resource_group_name }}" >> $GITHUB_ENV + else + echo "Generating a unique resource group name..." + ACCL_NAME="codmodv2" # Account name as specified + SHORT_UUID=$(uuidgen | cut -d'-' -f1) + UNIQUE_RG_NAME="arg-${ACCL_NAME}-${SHORT_UUID}" + echo "RESOURCE_GROUP_NAME=${UNIQUE_RG_NAME}" >> $GITHUB_ENV + echo "Generated RESOURCE_GROUP_NAME: ${UNIQUE_RG_NAME}" + fi + + - name: Install Bicep CLI + shell: bash + run: az bicep install + + - name: Check and Create Resource Group + id: check_create_rg + shell: bash + run: | + set -e + echo "πŸ” Checking if resource group '$RESOURCE_GROUP_NAME' exists..." + rg_exists=$(az group exists --name $RESOURCE_GROUP_NAME) + if [ "$rg_exists" = "false" ]; then + echo "πŸ“¦ Resource group does not exist. Creating new resource group '$RESOURCE_GROUP_NAME' in location '$AZURE_LOCATION'..." + az group create --name $RESOURCE_GROUP_NAME --location $AZURE_LOCATION || { echo "❌ Error creating resource group"; exit 1; } + echo "βœ… Resource group '$RESOURCE_GROUP_NAME' created successfully." + else + echo "βœ… Resource group '$RESOURCE_GROUP_NAME' already exists. Deploying to existing resource group." + fi + echo "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" >> $GITHUB_OUTPUT + echo "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" >> $GITHUB_ENV + + - name: Generate Unique Solution Prefix + id: generate_solution_prefix + shell: bash + run: | + set -e + COMMON_PART="psldg" + TIMESTAMP=$(date +%s) + UPDATED_TIMESTAMP=$(echo $TIMESTAMP | tail -c 6) + UNIQUE_SOLUTION_PREFIX="${COMMON_PART}${UPDATED_TIMESTAMP}" + echo "SOLUTION_PREFIX=${UNIQUE_SOLUTION_PREFIX}" >> $GITHUB_ENV + echo "Generated SOLUTION_PREFIX: ${UNIQUE_SOLUTION_PREFIX}" + + - name: Determine Docker Image Tag + id: determine_image_tag + shell: bash + run: | + if [[ "${{ env.BUILD_DOCKER_IMAGE }}" == "true" ]]; then + if [[ -n "${{ inputs.docker_image_tag }}" ]]; then + IMAGE_TAG="${{ inputs.docker_image_tag }}" + echo "πŸ”— Using Docker image tag from build job: $IMAGE_TAG" + else + echo "❌ Docker build job failed or was skipped, but BUILD_DOCKER_IMAGE is true" + exit 1 + fi + else + echo "🏷️ Using existing Docker image based on branch..." + BRANCH_NAME="${{ env.BRANCH_NAME }}" + echo "Current branch: $BRANCH_NAME" + + if [[ "$BRANCH_NAME" == "main" ]]; then + IMAGE_TAG="latest" + elif [[ "$BRANCH_NAME" == "dev" ]]; then + IMAGE_TAG="dev" + elif [[ "$BRANCH_NAME" == "demo" ]]; then + IMAGE_TAG="demo" + else + IMAGE_TAG="latest" + echo "Using default for branch '$BRANCH_NAME' - image tag: latest" + fi + echo "Using existing Docker image tag: $IMAGE_TAG" + fi + + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_OUTPUT + + - name: Generate Unique Environment Name + id: generate_env_name + shell: bash + run: | + COMMON_PART="pslc" + TIMESTAMP=$(date +%s) + UPDATED_TIMESTAMP=$(echo $TIMESTAMP | tail -c 6) + UNIQUE_ENV_NAME="${COMMON_PART}${UPDATED_TIMESTAMP}" + echo "ENV_NAME=${UNIQUE_ENV_NAME}" >> $GITHUB_ENV + echo "Generated Environment Name: ${UNIQUE_ENV_NAME}" + echo "ENV_NAME=${UNIQUE_ENV_NAME}" >> $GITHUB_OUTPUT + + - name: Display Workflow Configuration to GitHub Summary + shell: bash + run: | + echo "## πŸ“‹ Workflow Configuration Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Configuration | Value |" >> $GITHUB_STEP_SUMMARY + echo "|---------------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| **Branch** | \`${{ env.BRANCH_NAME }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **WAF Enabled** | ${{ env.WAF_ENABLED == 'true' && 'βœ… Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **EXP Enabled** | ${{ env.EXP == 'true' && 'βœ… Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Run E2E Tests** | \`${{ env.RUN_E2E_TESTS }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Cleanup Resources** | ${{ env.CLEANUP_RESOURCES == 'true' && 'βœ… Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Build Docker Image** | ${{ env.BUILD_DOCKER_IMAGE == 'true' && 'βœ… Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ inputs.trigger_type }}" == "workflow_dispatch" && -n "${{ inputs.azure_location }}" ]]; then + echo "| **Azure Location** | \`${{ inputs.azure_location }}\` (User Selected) |" >> $GITHUB_STEP_SUMMARY + fi + + if [[ -n "${{ inputs.resource_group_name }}" ]]; then + echo "| **Resource Group** | \`${{ inputs.resource_group_name }}\` (Pre-specified) |" >> $GITHUB_STEP_SUMMARY + else + echo "| **Resource Group** | \`${{ env.RESOURCE_GROUP_NAME }}\` (Auto-generated) |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ inputs.trigger_type }}" != "workflow_dispatch" ]]; then + echo "ℹ️ **Note:** Automatic Trigger - Using Non-WAF + Non-EXP configuration" >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ **Note:** Manual Trigger - Using user-specified configuration" >> $GITHUB_STEP_SUMMARY + fi + + deploy-linux: + name: Deploy on Linux + needs: azure-setup + if: inputs.runner_os == 'ubuntu-latest' && !cancelled() && needs.azure-setup.result == 'success' + uses: ./.github/workflows/job-deploy-linux.yml + with: + ENV_NAME: ${{ needs.azure-setup.outputs.ENV_NAME }} + AZURE_ENV_OPENAI_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} + RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} + IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} + BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} + EXP: ${{ inputs.EXP || 'false' }} + WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + secrets: inherit + + deploy-windows: + name: Deploy on Windows + needs: azure-setup + if: inputs.runner_os == 'windows-latest' && !cancelled() && needs.azure-setup.result == 'success' + uses: ./.github/workflows/job-deploy-windows.yml + with: + ENV_NAME: ${{ needs.azure-setup.outputs.ENV_NAME }} + AZURE_ENV_OPENAI_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} + RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} + IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} + BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} + EXP: ${{ inputs.EXP || 'false' }} + WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + secrets: inherit diff --git a/.github/workflows/job-docker-build.yml b/.github/workflows/job-docker-build.yml new file mode 100644 index 00000000..818e8457 --- /dev/null +++ b/.github/workflows/job-docker-build.yml @@ -0,0 +1,113 @@ +name: Docker Build Job + +on: + workflow_call: + inputs: + trigger_type: + description: 'Trigger type (workflow_dispatch, pull_request, schedule)' + required: true + type: string + build_docker_image: + description: 'Build And Push Docker Image (Optional)' + required: false + default: false + type: boolean + outputs: + IMAGE_TAG: + description: "Generated Docker Image Tag" + value: ${{ jobs.docker-build.outputs.IMAGE_TAG }} + +env: + BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} + +jobs: + docker-build: + if: inputs.trigger_type == 'workflow_dispatch' && inputs.build_docker_image == true + runs-on: ubuntu-latest + outputs: + IMAGE_TAG: ${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Generate Unique Docker Image Tag + id: generate_docker_tag + shell: bash + run: | + echo "πŸ”¨ Building new Docker image - generating unique tag..." + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + RUN_ID="${{ github.run_id }}" + BRANCH_NAME="${{ github.head_ref || github.ref_name }}" + CLEAN_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') + UNIQUE_TAG="${CLEAN_BRANCH_NAME}-${TIMESTAMP}-${RUN_ID}" + echo "IMAGE_TAG=$UNIQUE_TAG" >> $GITHUB_ENV + echo "IMAGE_TAG=$UNIQUE_TAG" >> $GITHUB_OUTPUT + echo "Generated unique Docker tag: $UNIQUE_TAG" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Azure Container Registry + uses: azure/docker-login@v2 + with: + login-server: ${{ secrets.ACR_TEST_LOGIN_SERVER }} + username: ${{ secrets.ACR_TEST_USERNAME }} + password: ${{ secrets.ACR_TEST_PASSWORD }} + + - name: Build and Push Cod Mod backend Docker image + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_SUMMARY: false + with: + context: . + file: docker/Backend.Dockerfile + push: true + tags: | + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/cmsabackend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/cmsabackend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} + + - name: Build and Push Cod Mod frontend Docker image + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_SUMMARY: false + with: + context: . + file: docker/Frontend.Dockerfile + push: true + tags: | + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/cmsafrontend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/cmsafrontend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} + + - name: Verify Docker Image Build + shell: bash + run: | + echo "βœ… Docker image successfully built and pushed" + echo "Image tag: ${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}" + + - name: Generate Docker Build Summary + if: always() + shell: bash + run: | + ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") + echo "## 🐳 Docker Build Job Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| **Job Status** | ${{ job.status == 'success' && 'βœ… Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Image Tag** | \`${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Branch** | \`${{ env.BRANCH_NAME }}\` |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "${{ job.status }}" == "success" ]]; then + echo "### βœ… Build Details" >> $GITHUB_STEP_SUMMARY + echo "Successfully built and pushed Docker images to Azure Container Registry" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Built Images:**" >> $GITHUB_STEP_SUMMARY + echo "- **Backend**: \`${ACR_NAME}/cmsabackend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Frontend**: \`${ACR_NAME}/cmsafrontend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Build Failed" >> $GITHUB_STEP_SUMMARY + echo "- Docker build process encountered an error" >> $GITHUB_STEP_SUMMARY + echo "- Check the docker-build job steps above for detailed error information" >> $GITHUB_STEP_SUMMARY + echo "- Verify ACR credentials and Docker build context" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/job-send-notification.yml b/.github/workflows/job-send-notification.yml new file mode 100644 index 00000000..e4a12491 --- /dev/null +++ b/.github/workflows/job-send-notification.yml @@ -0,0 +1,224 @@ +name: Send Notification Job + +on: + workflow_call: + inputs: + trigger_type: + description: 'Trigger type (workflow_dispatch, pull_request, schedule)' + required: true + type: string + waf_enabled: + description: 'Enable WAF' + required: false + default: false + type: boolean + EXP: + description: 'Enable EXP' + required: false + default: false + type: boolean + run_e2e_tests: + description: 'Run End-to-End Tests' + required: false + default: 'GoldenPath-Testing' + type: string + existing_webapp_url: + description: 'Existing Container WebApp URL (Skips Deployment)' + required: false + default: '' + type: string + deploy_result: + description: 'Deploy job result (success, failure, skipped)' + required: true + type: string + e2e_test_result: + description: 'E2E test job result (success, failure, skipped)' + required: true + type: string + CONTAINER_WEB_APPURL: + description: 'Container Web App URL' + required: false + default: '' + type: string + RESOURCE_GROUP_NAME: + description: 'Resource Group Name' + required: false + default: '' + type: string + QUOTA_FAILED: + description: 'Quota Check Failed Flag' + required: false + default: 'false' + type: string + TEST_SUCCESS: + description: 'Test Success Flag' + required: false + default: '' + type: string + TEST_REPORT_URL: + description: 'Test Report URL' + required: false + default: '' + type: string + +env: + GPT_MIN_CAPACITY: 100 + BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} + WAF_ENABLED: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.waf_enabled || false) || false }} + EXP: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.EXP || false) || false }} + RUN_E2E_TESTS: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.run_e2e_tests || 'GoldenPath-Testing') || 'GoldenPath-Testing' }} + +jobs: + send-notification: + runs-on: ubuntu-latest + continue-on-error: true + env: + accelerator_name: "Code Modernization" + steps: + - name: Determine Test Suite Display Name + id: test_suite + shell: bash + run: | + if [ "${{ env.RUN_E2E_TESTS }}" = "GoldenPath-Testing" ]; then + TEST_SUITE_NAME="Golden Path Testing" + elif [ "${{ env.RUN_E2E_TESTS }}" = "Smoke-Testing" ]; then + TEST_SUITE_NAME="Smoke Testing" + elif [ "${{ env.RUN_E2E_TESTS }}" = "None" ]; then + TEST_SUITE_NAME="None" + else + TEST_SUITE_NAME="${{ env.RUN_E2E_TESTS }}" + fi + echo "TEST_SUITE_NAME=$TEST_SUITE_NAME" >> $GITHUB_OUTPUT + echo "Test Suite: $TEST_SUITE_NAME" + + - name: Send Quota Failure Notification + if: inputs.deploy_result == 'failure' && inputs.QUOTA_FAILED == 'true' + shell: bash + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + EMAIL_BODY=$(cat <Dear Team,

    We would like to inform you that the ${{ env.accelerator_name }} deployment has failed due to insufficient quota in the requested regions.

    Issue Details:
    β€’ Quota check failed for GPT model
    β€’ Required GPT Capacity: ${{ env.GPT_MIN_CAPACITY }}
    β€’ Checked Regions: ${{ vars.AZURE_REGIONS }}

    Run URL: ${RUN_URL}

    Please resolve the quota issue and retry the deployment.

    Best regards,
    Your Automation Team

    ", + "subject": "${{ env.accelerator_name }} Pipeline - Failed (Insufficient Quota)" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send quota failure notification" + + - name: Send Deployment Failure Notification + if: inputs.deploy_result == 'failure' && inputs.QUOTA_FAILED != 'true' + shell: bash + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" + + EMAIL_BODY=$(cat <Dear Team,

    We would like to inform you that the ${{ env.accelerator_name }} deployment process has encountered an issue and has failed to complete successfully.

    Deployment Details:
    β€’ Resource Group: ${RESOURCE_GROUP}
    β€’ WAF Enabled: ${{ env.WAF_ENABLED }}
    β€’ EXP Enabled: ${{ env.EXP }}

    Run URL: ${RUN_URL}

    Please investigate the deployment failure at your earliest convenience.

    Best regards,
    Your Automation Team

    ", + "subject": "${{ env.accelerator_name }} Pipeline - Failed" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send deployment failure notification" + + - name: Send Success Notification + if: inputs.deploy_result == 'success' && (inputs.e2e_test_result == 'skipped' || inputs.TEST_SUCCESS == 'true') + shell: bash + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + WEBAPP_URL="${{ inputs.CONTAINER_WEB_APPURL || inputs.existing_webapp_url }}" + RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" + TEST_REPORT_URL="${{ inputs.TEST_REPORT_URL }}" + TEST_SUITE_NAME="${{ steps.test_suite.outputs.TEST_SUITE_NAME }}" + + if [ "${{ inputs.e2e_test_result }}" = "skipped" ]; then + EMAIL_BODY=$(cat <Dear Team,

    We would like to inform you that the ${{ env.accelerator_name }} deployment has completed successfully.

    Deployment Details:
    β€’ Resource Group: ${RESOURCE_GROUP}
    β€’ Web App URL: ${WEBAPP_URL}
    β€’ E2E Tests: Skipped (as configured)

    Configuration:
    β€’ WAF Enabled: ${{ env.WAF_ENABLED }}
    β€’ EXP Enabled: ${{ env.EXP }}

    Run URL: ${RUN_URL}

    Best regards,
    Your Automation Team

    ", + "subject": "${{ env.accelerator_name }} Pipeline - Deployment Success" + } + EOF + ) + else + EMAIL_BODY=$(cat <Dear Team,

    We would like to inform you that the ${{ env.accelerator_name }} deployment and testing process has completed successfully.

    Deployment Details:
    β€’ Resource Group: ${RESOURCE_GROUP}
    β€’ Web App URL: ${WEBAPP_URL}
    β€’ E2E Tests: Passed βœ…
    β€’ Test Suite: ${TEST_SUITE_NAME}
    β€’ Test Report: View Report

    Configuration:
    β€’ WAF Enabled: ${{ env.WAF_ENABLED }}
    β€’ EXP Enabled: ${{ env.EXP }}

    Run URL: ${RUN_URL}

    Best regards,
    Your Automation Team

    ", + "subject": "${{ env.accelerator_name }} Pipeline - Test Automation - Success" + } + EOF + ) + fi + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send success notification" + + - name: Send Test Failure Notification + if: inputs.deploy_result == 'success' && inputs.e2e_test_result != 'skipped' && inputs.TEST_SUCCESS != 'true' + shell: bash + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + TEST_REPORT_URL="${{ inputs.TEST_REPORT_URL }}" + WEBAPP_URL="${{ inputs.CONTAINER_WEB_APPURL || inputs.existing_webapp_url }}" + RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" + TEST_SUITE_NAME="${{ steps.test_suite.outputs.TEST_SUITE_NAME }}" + + EMAIL_BODY=$(cat <Dear Team,

    We would like to inform you that ${{ env.accelerator_name }} accelerator test automation process has encountered issues and failed to complete successfully.

    Deployment Details:
    β€’ Resource Group: ${RESOURCE_GROUP}
    β€’ Web App URL: ${WEBAPP_URL}
    β€’ Deployment Status: βœ… Success
    β€’ E2E Tests: ❌ Failed
    β€’ Test Suite: ${TEST_SUITE_NAME}

    Test Details:
    β€’ Test Report: View Report

    Run URL: ${RUN_URL}

    Please investigate the matter at your earliest convenience.

    Best regards,
    Your Automation Team

    ", + "subject": "${{ env.accelerator_name }} Pipeline - Test Automation - Failed" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send test failure notification" + + - name: Send Existing URL Success Notification + if: inputs.deploy_result == 'skipped' && inputs.existing_webapp_url != '' && inputs.e2e_test_result == 'success' && (inputs.TEST_SUCCESS == 'true' || inputs.TEST_SUCCESS == '') + shell: bash + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + EXISTING_URL="${{ inputs.existing_webapp_url }}" + TEST_REPORT_URL="${{ inputs.TEST_REPORT_URL }}" + TEST_SUITE_NAME="${{ steps.test_suite.outputs.TEST_SUITE_NAME }}" + + EMAIL_BODY=$(cat <Dear Team,

    The ${{ env.accelerator_name }} pipeline executed against the specified Target URL and testing process has completed successfully.

    Test Results:
    β€’ Status: βœ… Passed
    β€’ Test Suite: ${TEST_SUITE_NAME}
    ${TEST_REPORT_URL:+β€’ Test Report: View Report}
    β€’ Target URL: ${EXISTING_URL}

    Deployment: Skipped

    Run URL: ${RUN_URL}

    Best regards,
    Your Automation Team

    ", + "subject": "${{ env.accelerator_name }} Pipeline - Test Automation Passed " + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send existing URL success notification" + + - name: Send Existing URL Test Failure Notification + if: inputs.deploy_result == 'skipped' && inputs.existing_webapp_url != '' && inputs.e2e_test_result == 'failure' + shell: bash + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + EXISTING_URL="${{ inputs.existing_webapp_url }}" + TEST_REPORT_URL="${{ inputs.TEST_REPORT_URL }}" + TEST_SUITE_NAME="${{ steps.test_suite.outputs.TEST_SUITE_NAME }}" + + EMAIL_BODY=$(cat <Dear Team,

    The ${{ env.accelerator_name }} pipeline executed against the specified Target URL and the test automation has encountered issues and failed to complete successfully.

    Failure Details:
    β€’ Target URL: ${EXISTING_URL}
    ${TEST_REPORT_URL:+β€’ Test Report: View Report}
    β€’ Test Suite: ${TEST_SUITE_NAME}
    β€’ Deployment: Skipped

    Run URL: ${RUN_URL}

    Best regards,
    Your Automation Team

    ", + "subject": "${{ env.accelerator_name }} Pipeline - Test Automation Failed " + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send existing URL test failure notification" diff --git a/.github/workflows/test-automation-v2.yml b/.github/workflows/test-automation-v2.yml new file mode 100644 index 00000000..1c710b61 --- /dev/null +++ b/.github/workflows/test-automation-v2.yml @@ -0,0 +1,185 @@ +name: Test Automation Code Modernization - v2 + +on: + workflow_call: + inputs: + TEST_URL: + required: true + type: string + description: "Web URL for code modernization" + TEST_SUITE: + required: false + type: string + default: "GoldenPath-Testing" + description: "Test suite to run: 'Smoke-Testing', 'GoldenPath-Testing' " + outputs: + TEST_SUCCESS: + description: "Whether tests passed" + value: ${{ jobs.test.outputs.TEST_SUCCESS }} + TEST_REPORT_URL: + description: "URL to test report artifact" + value: ${{ jobs.test.outputs.TEST_REPORT_URL }} + +env: + url: ${{ inputs.TEST_URL }} + accelerator_name: "Code Modernization" + test_suite: ${{ inputs.TEST_SUITE }} + +jobs: + test: + runs-on: ubuntu-latest + outputs: + TEST_SUCCESS: ${{ steps.test1.outcome == 'success' || steps.test2.outcome == 'success' || steps.test3.outcome == 'success' }} + TEST_REPORT_URL: ${{ steps.upload_report.outputs.artifact-url }} + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - name: Login to Azure + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tests/e2e-test/requirements.txt + + - name: Ensure browsers are installed + run: python -m playwright install --with-deps chromium + + - name: Validate URL + run: | + if [ -z "${{ env.url }}" ]; then + echo "ERROR: No URL provided for testing" + exit 1 + fi + echo "Testing URL: ${{ env.url }}" + echo "Test Suite: ${{ env.test_suite }}" + + + - name: Wait for Application to be Ready + run: | + echo "Waiting for application to be ready at ${{ env.url }} " + max_attempts=10 + attempt=1 + + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt: Checking if application is ready..." + if curl -f -s "${{ env.url }}" > /dev/null; then + echo "Application is ready!" + break + + fi + + if [ $attempt -eq $max_attempts ]; then + echo "Application is not ready after $max_attempts attempts" + exit 1 + fi + + echo "Application not ready, waiting 30 seconds..." + sleep 30 + attempt=$((attempt + 1)) + done + + - name: Run tests(1) + id: test1 + run: | + if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + xvfb-run pytest --html=report/report.html --self-contained-html + fi + working-directory: tests/e2e-test + continue-on-error: true + + - name: Sleep for 30 seconds + if: ${{ steps.test1.outcome == 'failure' }} + run: sleep 30s + shell: bash + + - name: Run tests(2) + id: test2 + if: ${{ steps.test1.outcome == 'failure' }} + run: | + if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + xvfb-run pytest --html=report/report.html --self-contained-html + fi + working-directory: tests/e2e-test + continue-on-error: true + + - name: Sleep for 60 seconds + if: ${{ steps.test2.outcome == 'failure' }} + run: sleep 60s + shell: bash + + - name: Run tests(3) + id: test3 + if: ${{ steps.test2.outcome == 'failure' }} + run: | + if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + xvfb-run pytest --html=report/report.html --self-contained-html + fi + working-directory: tests/e2e-test + + - name: Upload test report + id: upload_report + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: test-report + path: tests/e2e-test/report/* + + - name: Generate E2E Test Summary + if: always() + run: | + # Determine test suite type for title + if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + echo "## πŸ§ͺ E2E Test Job Summary : Golden Path Testing" >> $GITHUB_STEP_SUMMARY + else + echo "## πŸ§ͺ E2E Test Job Summary : Smoke Testing" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + + # Determine overall test result + OVERALL_SUCCESS="${{ steps.test1.outcome == 'success' || steps.test2.outcome == 'success' || steps.test3.outcome == 'success' }}" + if [[ "$OVERALL_SUCCESS" == "true" ]]; then + echo "| **Job Status** | βœ… Success |" >> $GITHUB_STEP_SUMMARY + else + echo "| **Job Status** | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + fi + + echo "| **Target URL** | [${{ env.url }}](${{ env.url }}) |" >> $GITHUB_STEP_SUMMARY + echo "| **Test Suite** | \`${{ env.test_suite }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Test Report** | [Download Artifact](${{ steps.upload_report.outputs.artifact-url }}) |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "### πŸ“‹ Test Execution Details" >> $GITHUB_STEP_SUMMARY + echo "| Attempt | Status | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|---------|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| **Test Run 1** | ${{ steps.test1.outcome == 'success' && 'βœ… Passed' || '❌ Failed' }} | Initial test execution |" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ steps.test1.outcome }}" == "failure" ]]; then + echo "| **Test Run 2** | ${{ steps.test2.outcome == 'success' && 'βœ… Passed' || steps.test2.outcome == 'failure' && '❌ Failed' || '⏸️ Skipped' }} | Retry after 30s delay |" >> $GITHUB_STEP_SUMMARY + fi + + if [[ "${{ steps.test2.outcome }}" == "failure" ]]; then + echo "| **Test Run 3** | ${{ steps.test3.outcome == 'success' && 'βœ… Passed' || steps.test3.outcome == 'failure' && '❌ Failed' || '⏸️ Skipped' }} | Final retry after 60s delay |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "$OVERALL_SUCCESS" == "true" ]]; then + echo "### βœ… Test Results" >> $GITHUB_STEP_SUMMARY + echo "- End-to-end tests completed successfully" >> $GITHUB_STEP_SUMMARY + echo "- Application is functioning as expected" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Test Results" >> $GITHUB_STEP_SUMMARY + echo "- All test attempts failed" >> $GITHUB_STEP_SUMMARY + echo "- Check the e2e-test/test job for detailed error information" >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/infra/main.bicep b/infra/main.bicep index a6472f5c..34d5b1fc 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -105,6 +105,9 @@ param gptModelName string = 'gpt-4o' @description('Optional. Set the Image tag. Defaults to latest_2025-11-10_599.') param imageVersion string = 'latest_2025-11-10_599' +@description('Optional. Azure Container Registry name. Defaults to cmsacontainerreg.azurecr.io') +param acrName string = 'cmsacontainerreg.azurecr.io' + @minLength(1) @description('Optional. Version of the GPT model to deploy. Defaults to 2024-08-06.') param gptModelVersion string = '2024-08-06' @@ -856,7 +859,7 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.19.0' = { containers: [ { name: 'cmsabackend' - image: 'cmsacontainerreg.azurecr.io/cmsabackend:${imageVersion}' + image: '${acrName}/cmsabackend:${imageVersion}' env: concat( [ { @@ -1044,7 +1047,7 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.19.0' = { value: 'prod' } ] - image: 'cmsacontainerreg.azurecr.io/cmsafrontend:${imageVersion}' + image: '${acrName}/cmsafrontend:${imageVersion}' name: 'cmsafrontend' resources: { cpu: 1 diff --git a/infra/main.parameters.json b/infra/main.parameters.json index b5371502..da8b3cc7 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -23,6 +23,9 @@ "imageVersion": { "value": "${AZURE_ENV_IMAGETAG=latest}" }, + "acrName": { + "value": "${AZURE_ENV_ACR_NAME=cmsacontainerreg.azurecr.io}" + }, "existingLogAnalyticsWorkspaceId": { "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}" }, @@ -32,6 +35,9 @@ "secondaryLocation": { "value": "${AZURE_ENV_COSMOS_SECONDARY_LOCATION}" }, + "azureAiServiceLocation": { + "value": "${AZURE_ENV_AI_SERVICE_LOCATION}" + }, "vmSize": { "value": "${AZURE_ENV_JUMPBOX_SIZE}" }, diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index 7bd336b2..98a1ead9 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -23,6 +23,9 @@ "imageVersion": { "value": "${AZURE_ENV_IMAGETAG=latest}" }, + "acrName": { + "value": "${AZURE_ENV_ACR_NAME=cmsacontainerreg.azurecr.io}" + }, "existingLogAnalyticsWorkspaceId": { "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}" }, @@ -32,6 +35,9 @@ "secondaryLocation": { "value": "${AZURE_ENV_COSMOS_SECONDARY_LOCATION}" }, + "azureAiServiceLocation": { + "value": "${AZURE_ENV_AI_SERVICE_LOCATION}" + }, "vmSize": { "value": "${AZURE_ENV_JUMPBOX_SIZE}" }, From 7726d6bacba9cd733a0fabf57015bdfbb17d9bd1 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Thu, 18 Dec 2025 10:36:15 +0530 Subject: [PATCH 09/13] Added workflow trigger --- .github/workflows/deploy-linux.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-linux.yml index aecf5071..d5e7d029 100644 --- a/.github/workflows/deploy-linux.yml +++ b/.github/workflows/deploy-linux.yml @@ -1,5 +1,13 @@ name: Deploy-Test-Cleanup (v2) Linux on: + workflow_run: + workflows: ["Build Docker and Optional Push"] + types: + - completed + branches: + - main + - dev + - demo workflow_dispatch: inputs: azure_location: From 03c07466bc83dba1d038647651a50de469ad13b0 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Thu, 18 Dec 2025 10:49:51 +0530 Subject: [PATCH 10/13] removed SecurityControl tag --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dd0e0d83..8ba97981 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -142,7 +142,7 @@ jobs: azureAiServiceLocation='${{ env.AZURE_LOCATION }}' \ imageVersion="${IMAGE_TAG}" \ createdBy="Pipeline" \ - tags="{'SecurityControl':'Ignore','Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" + tags="{'Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" - name: Assign Contributor role to Service Principal if: always() From 5449c84137832bc3bbc4f3be793354e8fc956115 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Thu, 18 Dec 2025 16:18:23 +0530 Subject: [PATCH 11/13] Updated readme --- docs/CustomizingAzdParameters.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index bfcffe6d..b71a16a3 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -10,6 +10,7 @@ By default this template will use the environment name as the prefix to prevent | -------------------------------------- | ------- | ---------------- | ---------------------------------------------------------------------------------------------------- | | `AZURE_ENV_NAME` | string | `azdtemp` | Used as a prefix for all resource names to ensure uniqueness across environments. | | `AZURE_LOCATION` | string | `` | Location of the Azure resources. Controls where the infrastructure will be deployed. | +| `AZURE_ENV_AI_SERVICE_LOCATION` | string | `` | Location of the Azure resources. Controls where the Azure AI Services will be deployed. | | `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Change the Model Deployment Type (allowed values: Standard, GlobalStandard). | | `AZURE_ENV_MODEL_NAME` | string | `gpt-4o` | Set the Model Name (allowed values: gpt-4o). | | `AZURE_ENV_MODEL_VERSION` | string | `2024-08-06` | Set the Azure model version (allowed values: 2024-08-06) | @@ -21,6 +22,8 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_ENV_JUMPBOX_ADMIN_PASSWORD` | string | `JumpboxAdminP@ssw0rd1234!` | Specifies the administrator password for the Jumpbox Virtual Machine. | | `AZURE_ENV_COSMOS_SECONDARY_LOCATION` | string | *(not set by default)* | Specifies the secondary region for Cosmos DB. Required if `enableRedundancy` is `true`. | | `AZURE_EXISTING_AI_PROJECT_RESOURCE_ID` | string | *(not set by default)* | Specifies the existing AI Foundry Project Resource ID if it needs to be reused. | +| `AZURE_ENV_ACR_NAME` | string | `cmsacontainerreg.azurecr.io` | Specifies the Azure Container Registry name to use for container images. | + --- ## How to Set a Parameter From ecca2f5e08b5a6aa834c3ff45814ccdbd897eb93 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Thu, 18 Dec 2025 16:43:21 +0530 Subject: [PATCH 12/13] Change account name in resource group generation --- .github/workflows/job-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 6ac84e16..0f0812b4 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -201,7 +201,7 @@ jobs: echo "RESOURCE_GROUP_NAME=${{ inputs.resource_group_name }}" >> $GITHUB_ENV else echo "Generating a unique resource group name..." - ACCL_NAME="codmodv2" # Account name as specified + ACCL_NAME="codmod" # Account name as specified SHORT_UUID=$(uuidgen | cut -d'-' -f1) UNIQUE_RG_NAME="arg-${ACCL_NAME}-${SHORT_UUID}" echo "RESOURCE_GROUP_NAME=${UNIQUE_RG_NAME}" >> $GITHUB_ENV From 3cd8b4c5fc1d75317e9fcca99ac8788c3eaa0d19 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Tue, 23 Dec 2025 10:05:04 +0530 Subject: [PATCH 13/13] Update subscription ID references to use secrets in deployment workflows --- .github/workflows/job-deploy-linux.yml | 4 ++-- .github/workflows/job-deploy-windows.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index 4bd437b1..2b69d56e 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -91,10 +91,10 @@ jobs: echo "Environment created: ${{ inputs.ENV_NAME }}" echo "Setting default subscription..." - azd config set defaults.subscription ${{ vars.AZURE_SUBSCRIPTION_ID }} + azd config set defaults.subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} # Set additional parameters - azd env set AZURE_SUBSCRIPTION_ID="${{ vars.AZURE_SUBSCRIPTION_ID }}" + azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" azd env set AZURE_ENV_AI_SERVICE_LOCATION="${{ inputs.AZURE_ENV_OPENAI_LOCATION }}" azd env set AZURE_LOCATION="${{ inputs.AZURE_LOCATION }}" azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index fc016103..a67bf146 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -86,10 +86,10 @@ jobs: Write-Host "Environment created: ${{ inputs.ENV_NAME }}" Write-Host "Setting default subscription..." - azd config set defaults.subscription ${{ vars.AZURE_SUBSCRIPTION_ID }} + azd config set defaults.subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} # Set additional parameters - azd env set AZURE_SUBSCRIPTION_ID="${{ vars.AZURE_SUBSCRIPTION_ID }}" + azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" azd env set AZURE_ENV_AI_SERVICE_LOCATION="${{ inputs.AZURE_ENV_OPENAI_LOCATION }}" azd env set AZURE_LOCATION="${{ inputs.AZURE_LOCATION }}" azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}"