Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## [Unreleased]

## [v3.3.1] - 2026-10-27

### 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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.3.0
3.3.1
33 changes: 23 additions & 10 deletions scripts/deploy.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand All @@ -221,15 +234,15 @@ 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'
}

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'
Expand Down Expand Up @@ -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')
Expand All @@ -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.'
Expand All @@ -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 += '.'
Expand All @@ -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'
Expand All @@ -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.'
22 changes: 21 additions & 1 deletion scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)"
Expand Down
1 change: 1 addition & 0 deletions tests/deployment/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Host-only deployment script regression tests."""
74 changes: 74 additions & 0 deletions tests/deployment/test_resource_group.py
Original file line number Diff line number Diff line change
@@ -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()
5 changes: 3 additions & 2 deletions tests/test_installed_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
)

Expand Down
Loading