From beb27b3c174948d3871062ecb765ac6cc9888df5 Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Tue, 27 Oct 2026 00:36:38 -0300 Subject: [PATCH 1/2] fix: honor the application resource group (#155) Use the application resource group for deployment operations while retaining the legacy configuration fallback. Keep host deployment tests separate from installed runtime acceptance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 7 +++ scripts/deploy.ps1 | 33 +++++++---- scripts/deploy.sh | 22 +++++++- tests/deployment/__init__.py | 1 + tests/deployment/test_resource_group.py | 74 +++++++++++++++++++++++++ tests/test_installed_package.py | 5 +- 6 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 tests/deployment/__init__.py create mode 100644 tests/deployment/test_resource_group.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cc68e7c..3eca77e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## [Unreleased] +### Fixed + +- **Application resource-group targeting.** Resolve Container Apps, registry + operations and deployment identities from `AZURE_APP_RESOURCE_GROUP` under + `agent-lz`. Confirmed missing or empty values retain the legacy + `AZURE_RESOURCE_GROUP` fallback; configuration access failures stop deployment. + ## [v3.3.0] - 2026-10-14 ### Added diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index b10ec52..5b48a64 100755 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -147,6 +147,19 @@ function Get-RequiredConfigValue { return $value } +function Get-ApplicationResourceGroup { + $output = @(& az appconfig kv list --endpoint $APP_CONFIG_ENDPOINT ` + --key AZURE_APP_RESOURCE_GROUP --label agent-lz --auth-mode login ` + --only-show-errors --query '[].value' --output tsv) + if ($LASTEXITCODE -ne 0) { + throw 'Cannot read AZURE_APP_RESOURCE_GROUP; check App Configuration access. No legacy fallback was attempted.' + } + if ($output.Count -gt 1) { throw 'Application resource group must have one configuration value.' } + $group = if ($output.Count) { "$($output[0])".Trim() } else { '' } + if ($group) { return $group } + return Get-RequiredConfigValue -Key 'AZURE_RESOURCE_GROUP' +} + function Test-DockerReady { if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { return $false } $output = & docker info 2>&1 @@ -210,7 +223,7 @@ function Set-ContainerAppRegistry { $identityOutput = Invoke-ExternalCommand -FilePath 'az' -Arguments @( 'containerapp','identity','show', '--name',$AppName, - '--resource-group',$Values.AZURE_RESOURCE_GROUP, + '--resource-group',$Values.AZURE_APP_RESOURCE_GROUP, '--only-show-errors', '--query','type','-o','tsv' ) -What 'fetch container app identity' @@ -221,7 +234,7 @@ function Set-ContainerAppRegistry { Write-ErrorColored 'SUBSCRIPTION_ID and RESOURCE_TOKEN are required for user-assigned registry identity.' exit 1 } - $registryIdentity = "/subscriptions/$($Values.SUBSCRIPTION_ID)/resourceGroups/$($Values.AZURE_RESOURCE_GROUP)/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai-ca-$($Values.RESOURCE_TOKEN)-$identitySuffix" + $registryIdentity = "/subscriptions/$($Values.SUBSCRIPTION_ID)/resourceGroups/$($Values.AZURE_APP_RESOURCE_GROUP)/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai-ca-$($Values.RESOURCE_TOKEN)-$identitySuffix" } else { $registryIdentity = 'system' } @@ -229,7 +242,7 @@ function Set-ContainerAppRegistry { Invoke-ExternalCommand -FilePath 'az' -Arguments @( 'containerapp','registry','set', '--name',$AppName, - '--resource-group',$Values.AZURE_RESOURCE_GROUP, + '--resource-group',$Values.AZURE_APP_RESOURCE_GROUP, '--server',$Values.CONTAINER_REGISTRY_LOGIN_SERVER, '--identity',$registryIdentity, '--only-show-errors' @@ -310,7 +323,7 @@ Write-Blue "Loading App Configuration settings (labels=$($labels -join ', '))... $values = @{ CONTAINER_REGISTRY_NAME = (Get-RequiredConfigValue -Key 'CONTAINER_REGISTRY_NAME') CONTAINER_REGISTRY_LOGIN_SERVER = (Get-RequiredConfigValue -Key 'CONTAINER_REGISTRY_LOGIN_SERVER') - AZURE_RESOURCE_GROUP = (Get-RequiredConfigValue -Key 'AZURE_RESOURCE_GROUP') + AZURE_APP_RESOURCE_GROUP = (Get-ApplicationResourceGroup) APP_NAME = (Get-RequiredConfigValue -Key $appConfigKey) SUBSCRIPTION_ID = (Get-ConfigValue -Key 'SUBSCRIPTION_ID') RESOURCE_TOKEN = (Get-ConfigValue -Key 'RESOURCE_TOKEN') @@ -323,12 +336,12 @@ if ([string]::IsNullOrWhiteSpace($values.SUBSCRIPTION_ID)) { Write-Green 'All App Configuration values retrieved:' Write-Host (" CONTAINER_REGISTRY_NAME = {0}" -f $values.CONTAINER_REGISTRY_NAME) Write-Host (" CONTAINER_REGISTRY_LOGIN_SERVER = {0}" -f $values.CONTAINER_REGISTRY_LOGIN_SERVER) -Write-Host (" AZURE_RESOURCE_GROUP = {0}" -f $values.AZURE_RESOURCE_GROUP) +Write-Host (" AZURE_APP_RESOURCE_GROUP = {0}" -f $values.AZURE_APP_RESOURCE_GROUP) Write-Host (" APP_NAME = {0}" -f $values.APP_NAME) if ($buildMode -eq 'local') { - Write-Blue ("Logging into ACR ({0} in {1})..." -f $values.CONTAINER_REGISTRY_NAME, $values.AZURE_RESOURCE_GROUP) - Invoke-ExternalCommand -FilePath 'az' -Arguments @('acr','login','--name',$values.CONTAINER_REGISTRY_NAME,'--resource-group',$values.AZURE_RESOURCE_GROUP) -What 'ACR login' | Out-Null + Write-Blue ("Logging into ACR ({0} in {1})..." -f $values.CONTAINER_REGISTRY_NAME, $values.AZURE_APP_RESOURCE_GROUP) + Invoke-ExternalCommand -FilePath 'az' -Arguments @('acr','login','--name',$values.CONTAINER_REGISTRY_NAME,'--resource-group',$values.AZURE_APP_RESOURCE_GROUP) -What 'ACR login' | Out-Null Write-Green 'Logged into ACR.' } else { Write-Green 'Using remote ACR build; local Docker login is not required.' @@ -348,7 +361,7 @@ if ($buildMode -eq 'local') { Write-Blue 'Building image remotely via az acr build...' $acrBuildArgs = @('acr','build','--registry',$values.CONTAINER_REGISTRY_NAME,'--image',"azure-gpt-rag/${imageRepository}:$tag",'--file','Dockerfile','--no-logs') if ($env:ACR_TASK_AGENT_POOL) { - Invoke-ExternalCommand -FilePath 'az' -Arguments @('acr','agentpool','show','--registry',$values.CONTAINER_REGISTRY_NAME,'--name',$env:ACR_TASK_AGENT_POOL,'--resource-group',$values.AZURE_RESOURCE_GROUP,'--only-show-errors') -What 'validate ACR task agent pool' | Out-Null + Invoke-ExternalCommand -FilePath 'az' -Arguments @('acr','agentpool','show','--registry',$values.CONTAINER_REGISTRY_NAME,'--name',$env:ACR_TASK_AGENT_POOL,'--resource-group',$values.AZURE_APP_RESOURCE_GROUP,'--only-show-errors') -What 'validate ACR task agent pool' | Out-Null $acrBuildArgs += @('--agent-pool',$env:ACR_TASK_AGENT_POOL) } $acrBuildArgs += '.' @@ -364,7 +377,7 @@ Write-Blue 'Updating container app image...' $latestRevisionOutput = Invoke-ExternalCommand -FilePath 'az' -Arguments @( 'containerapp','update', '--name',$values.APP_NAME, - '--resource-group',$values.AZURE_RESOURCE_GROUP, + '--resource-group',$values.AZURE_APP_RESOURCE_GROUP, '--image',$fullImageName, '--only-show-errors', '--query','properties.latestRevisionName','-o','tsv' @@ -377,5 +390,5 @@ if (-not [string]::IsNullOrWhiteSpace($latestRevision) -and $latestRevision -ne } Write-Blue 'Verifying container app image...' -Confirm-ContainerAppImage -AppName $values.APP_NAME -ResourceGroupName $values.AZURE_RESOURCE_GROUP -ExpectedImage $fullImageName +Confirm-ContainerAppImage -AppName $values.APP_NAME -ResourceGroupName $values.AZURE_APP_RESOURCE_GROUP -ExpectedImage $fullImageName Write-Green 'Container app image verified.' diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 1fd04d7..72b8d81 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -114,6 +114,26 @@ require_config_value() { printf "%s" "$value" } +get_application_resource_group() { + local group + group="$(az appconfig kv list --endpoint "$APP_CONFIG_ENDPOINT" \ + --key AZURE_APP_RESOURCE_GROUP --label agent-lz --auth-mode login \ + --only-show-errors --query '[].value' --output tsv)" || { + error "Cannot read AZURE_APP_RESOURCE_GROUP; check App Configuration access. No legacy fallback was attempted." + return 1 + } + group="$(printf "%s" "$group" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + if [[ "$group" == *$'\n'* ]]; then + error "Application resource group must have one configuration value." + return 1 + fi + if [[ -n "$group" ]]; then + printf "%s" "$group" + else + require_config_value "AZURE_RESOURCE_GROUP" + fi +} + docker_ready() { command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1 } @@ -274,7 +294,7 @@ success "Build mode: ${buildMode}" info "Loading App Configuration settings (labels=${config_labels[*]})..." containerRegistryName="$(require_config_value "CONTAINER_REGISTRY_NAME")" containerRegistryLoginServer="$(require_config_value "CONTAINER_REGISTRY_LOGIN_SERVER")" -resourceGroupName="$(require_config_value "AZURE_RESOURCE_GROUP")" +resourceGroupName="$(get_application_resource_group)" appName="$(require_config_value "$appConfigKey")" subscriptionId="$(get_config_value "SUBSCRIPTION_ID" || az account show --only-show-errors --query id -o tsv 2>&1 | select_cli_value)" resourceToken="$(get_config_value "RESOURCE_TOKEN" || true)" diff --git a/tests/deployment/__init__.py b/tests/deployment/__init__.py new file mode 100644 index 0000000..a0b132c --- /dev/null +++ b/tests/deployment/__init__.py @@ -0,0 +1 @@ +"""Host-only deployment script regression tests.""" diff --git a/tests/deployment/test_resource_group.py b/tests/deployment/test_resource_group.py new file mode 100644 index 0000000..0c0284b --- /dev/null +++ b/tests/deployment/test_resource_group.py @@ -0,0 +1,74 @@ +"""Offline destination selection for both deployment scripts.""" + +import os +from pathlib import Path +import re +import shutil +import subprocess +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +CASES = ( + ("rg-app", 0, "rg-app"), + ("", 0, "rg-legacy"), + (" ", 0, "rg-legacy"), + ("", 1, None), + ("rg-app\nrg-other", 0, None), +) + + +class DeploymentResourceGroupTests(unittest.TestCase): + def run_cases(self, command, source): + for value, status, expected in CASES: + with self.subTest(value=value, status=status): + result = subprocess.run( + [*command, source], capture_output=True, text=True, timeout=15, + env={**os.environ, "RG_TEST_VALUE": value, "RG_TEST_EXIT": str(status)}, + ) + if expected is None: + self.assertNotEqual(result.returncode, 0) + self.assertNotIn("rg-legacy", result.stdout) + else: + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), expected) + + @unittest.skipUnless(shutil.which("pwsh"), "PowerShell is unavailable") + def test_powershell(self): + source = (ROOT / "scripts" / "deploy.ps1").read_text(encoding="utf-8-sig") + resolver = re.search( + r"^function Get-ApplicationResourceGroup \{.*?^\}", source, re.M | re.S, + ).group() + self.assertIn("AZURE_APP_RESOURCE_GROUP = (Get-ApplicationResourceGroup)", source) + self.assertNotIn("$values.AZURE_RESOURCE_GROUP", source) + driver = """ +$ErrorActionPreference = 'Stop' +function az { + $global:LASTEXITCODE = [int]$env:RG_TEST_EXIT + if ($env:RG_TEST_VALUE) { $env:RG_TEST_VALUE -split "`n" } +} +function Get-RequiredConfigValue { param($Key); 'rg-legacy' } +""" + self.run_cases( + [shutil.which("pwsh"), "-NoProfile", "-NonInteractive", "-Command"], + driver + resolver + "\nGet-ApplicationResourceGroup", + ) + + @unittest.skipUnless(shutil.which("bash"), "Bash is unavailable") + def test_shell(self): + source = (ROOT / "scripts" / "deploy.sh").read_text(encoding="utf-8-sig") + resolver = re.search( + r"^get_application_resource_group\(\) \{.*?^\}", source, re.M | re.S, + ).group() + self.assertIn('resourceGroupName="$(get_application_resource_group)"', source) + driver = """ +az() { printf '%s' "$RG_TEST_VALUE"; return "$RG_TEST_EXIT"; } +require_config_value() { printf 'rg-legacy'; } +error() { printf '%s\\n' "$*" >&2; } +APP_CONFIG_ENDPOINT=https://example.azconfig.io +""" + self.run_cases([shutil.which("bash"), "-c"], driver + resolver + "\nget_application_resource_group") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_installed_package.py b/tests/test_installed_package.py index 5723520..2bcb8aa 100644 --- a/tests/test_installed_package.py +++ b/tests/test_installed_package.py @@ -43,14 +43,15 @@ def setUpClass(cls): cls.cwd.mkdir() cls.inventory = json.loads((ROOT / ".quality/migration.json").read_text(encoding="utf-8")) cls.behavioral_tests = cls.root / "behavioral-tests" - # The other three modules inspect/build the source tree and already run - # separately. Reuse the behavioral suite without copying application code. + # Source/build and deployment-tooling contracts run in the checkout. + # Reuse runtime behavior here without copying application source. shutil.copytree( ROOT / "tests", cls.behavioral_tests, ignore=shutil.ignore_patterns( "__pycache__", "test_installed_package.py", "test_module_compatibility.py", "test_quality_policy.py", + "deployment", ), ) From cb9cae82fcd9aebac6dc3603ca79abb5bedb1e97 Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Tue, 27 Oct 2026 00:41:46 -0300 Subject: [PATCH 2/2] chore: prepare release 3.3.1 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 ++ VERSION | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eca77e..6ae798c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [v3.3.1] - 2026-10-27 + ### Fixed - **Application resource-group targeting.** Resolve Container Apps, registry diff --git a/VERSION b/VERSION index 0fa4ae4..712bd5a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.0 \ No newline at end of file +3.3.1 \ No newline at end of file