Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
67895e5
fix(ps): add SubscriptionId alias to Verify-Agent.ps1 and Diff-Agent.ps1
dm-chelupati May 15, 2026
37cf8f6
fix(ps): add SubscriptionId alias to Export-Agent.ps1 and Clone-Agent…
dm-chelupati May 15, 2026
2508125
fix: PS 7.3+ az/curl string array splitting → jq parse errors
dm-chelupati May 15, 2026
2e1c85c
feat: add color output to verify-agent.sh
dm-chelupati May 15, 2026
89d831d
fix(ps): fix jq join filter quoting in Get-ExpList (join(`,`) → join(…
dm-chelupati May 15, 2026
042ea5d
fix(ps): wrap iterator jq paths in [] to prevent sort-on-string error
dm-chelupati May 15, 2026
e12c012
fix(ps): fix Invoke-Jq no-input hang, curl max-time, raw jq in Verify…
dm-chelupati May 15, 2026
76b1700
fix(ps): fix ConvertTo-Yaml pipeline accumulation (begin/process/end)
dm-chelupati May 15, 2026
f4a24fa
fix(ps): fix agent.json/expected-config.json not written (remove -Inp…
dm-chelupati May 15, 2026
7c68207
fix(ps): replace Python -c multiline with temp file in Assemble-Agent…
dm-chelupati May 15, 2026
60f34df
fix: ConvertTo-Yaml return string[] joined with newlines
dm-chelupati May 15, 2026
861a9d2
fix: add -TimeoutSec 30 to all Invoke-RestMethod/WebRequest calls
dm-chelupati May 15, 2026
7721b1d
fix: pre-declare $errs for PS 7.6 compat; OAuth detect via JSON match…
dm-chelupati May 15, 2026
e054af1
fix: poll /api/v1/Github/config instead of non-existent /auth/status …
dm-chelupati May 16, 2026
92b9fc1
fix: detect GitHub OAuth by attempting connector PUT instead of polli…
dm-chelupati May 16, 2026
c1b01a4
fix: replace bare jq calls with Invoke-Jq in Verify-Agent.ps1
dm-chelupati May 16, 2026
72546ea
fix: Export empty-string args, GitHub OAuth stale-auth fallback, veri…
dm-chelupati May 18, 2026
644334d
chore: remove e2e test scripts with personal values
dm-chelupati May 19, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/validate-templates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
echo "Checking PowerShell scripts..."
errors=0
for f in bin/ps/*.ps1; do
result=$(pwsh -NoProfile -Command "[System.Management.Automation.Language.Parser]::ParseFile('$f', [ref]\$null, [ref]\$errs); if(\$errs.Count -gt 0){\$errs | ForEach-Object { \$_.Message }}" 2>&1)
result=$(pwsh -NoProfile -Command "\$errs=\$null; [System.Management.Automation.Language.Parser]::ParseFile('$f', [ref]\$null, [ref]\$errs); if(\$errs -and \$errs.Count -gt 0){\$errs | ForEach-Object { \$_.Message }}" 2>&1)
if [[ -n "$result" && "$result" != *"InvalidOperation"* ]]; then
echo "❌ $f: $result"
errors=$((errors + 1))
Expand Down
241 changes: 123 additions & 118 deletions sreagent-templates/bicep/Apply-Extras.ps1

Large diffs are not rendered by default.

23 changes: 18 additions & 5 deletions sreagent-templates/bicep/Assemble-Agent.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ print(json.dumps(resolve(data)))
'@

try {
$result = $Json | & $Python -c $pyScript $BaseDir 2>$null
$pyTmp = [System.IO.Path]::GetTempFileName() + '.py'
Set-Content -Path $pyTmp -Value $pyScript -Encoding UTF8
$result = $Json | & $Python $pyTmp $BaseDir 2>$null
Remove-Item $pyTmp -Force -ErrorAction SilentlyContinue
if ($LASTEXITCODE -eq 0 -and $result) { return $result }
} catch {}
return $Json
Expand Down Expand Up @@ -168,7 +171,10 @@ with open(sys.argv[1]) as fh:
print(json.dumps(data))
'@
try {
$item = & $Python -c $pyYaml $f.FullName 2>$null
$pyTmp = [System.IO.Path]::GetTempFileName() + '.py'
Set-Content -Path $pyTmp -Value $pyYaml -Encoding UTF8
$item = & $Python $pyTmp $f.FullName 2>$null
Remove-Item $pyTmp -Force -ErrorAction SilentlyContinue
if ($LASTEXITCODE -ne 0 -or -not $item) { continue }
# Use --slurpfile to safely pass JSON without --argjson quoting issues
$tmpItem = [System.IO.Path]::GetTempFileName()
Expand Down Expand Up @@ -215,7 +221,10 @@ print(json.dumps(sub(data)))
'@

try {
$result = $Json | & $Python -c $pyScript 2>$null
$pyTmp = [System.IO.Path]::GetTempFileName() + '.py'
Set-Content -Path $pyTmp -Value $pyScript -Encoding UTF8
$result = $Json | & $Python $pyTmp 2>$null
Remove-Item $pyTmp -Force -ErrorAction SilentlyContinue
if ($LASTEXITCODE -eq 0 -and $result) { return $result }
} catch {}
return $Json
Expand Down Expand Up @@ -413,12 +422,16 @@ if ($mdFiles.Count -gt 0) {
# Build JSON item via Python to avoid jq --arg quoting issues with large content
$tmpContent = [System.IO.Path]::GetTempFileName()
Set-Content -Path $tmpContent -Value $content -NoNewline -Encoding UTF8
$item = & $Python -c @"
$pyKnowledge = @'
import json, sys
with open(sys.argv[1]) as f:
content = f.read()
print(json.dumps({"name": sys.argv[2], "type": "KnowledgeText", "content": content}))
"@ $tmpContent $fname 2>$null
'@
$pyTmp = [System.IO.Path]::GetTempFileName() + '.py'
Set-Content -Path $pyTmp -Value $pyKnowledge -Encoding UTF8
$item = & $Python $pyTmp $tmpContent $fname 2>$null
Remove-Item $pyTmp -Force -ErrorAction SilentlyContinue
Remove-Item $tmpContent -ErrorAction SilentlyContinue
if ($LASTEXITCODE -eq 0 -and $item) {
$tmpItem = [System.IO.Path]::GetTempFileName()
Expand Down
35 changes: 25 additions & 10 deletions sreagent-templates/bicep/apply-extras.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
# ADO — set $ADO_PAT, $ADO_USE_AAD=1, or $ADO_USE_MI=1 (with $ADO_ORG).
#
# Usage:
# ./apply-extras.sh <subscription-id> <resource-group> <agent-name> [extras-file]
# ./apply-extras.sh <subscription-id> <resource-group> <agent-name> [extras-file-or-config-dir]
#
# If the 4th argument is a directory (agent config dir), extras are auto-assembled from it.

set -euo pipefail

Expand All @@ -41,7 +43,16 @@ FILE="${4:-extras.parameters.json}"
FORCE=""
for arg in "$@"; do [[ "$arg" == "--force" ]] && FORCE="true"; done

[[ -f "$FILE" ]] || { echo "extras file not found: $FILE" >&2; exit 1; }
# Auto-assemble if a config directory was passed instead of a file
if [[ -d "$FILE" ]]; then
CONFIG_DIR="$FILE"
ASSEMBLE_TMP="$(mktemp -d)/assembled"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
bash "${SCRIPT_DIR}/assemble-agent.sh" "$CONFIG_DIR" --output "$ASSEMBLE_TMP"
FILE="${ASSEMBLE_TMP}.extras.json"
fi

[[ -f "$FILE" ]] || { echo "extras file not found: $FILE (pass a config dir or pre-assembled extras.json)" >&2; exit 1; }
command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; }
command -v tar >/dev/null || { echo "tar is required" >&2; exit 1; }
command -v curl >/dev/null || { echo "curl is required" >&2; exit 1; }
Expand Down Expand Up @@ -900,14 +911,23 @@ if [[ ${#oauth_repos[@]} -gt 0 ]]; then
echo " 2. Sign in to GitHub and approve the SRE Agent app."
echo
echo " Waiting for GitHub authorization (Ctrl-C to skip)..."
if [[ -z "$AGENT_UAMI" ]]; then IDENT="SystemAssigned"; else IDENT="$AGENT_UAMI"; fi
conn_body=$(jq -nc --arg id "$IDENT" '{name:"github",type:"AgentConnector",properties:{dataConnectorType:"GitHubOAuth",dataSource:"github-oauth",identity:$id}}')
auth_ok=false
for attempt in $(seq 1 24); do
sleep 10
TOKEN=$(_dp_token 2>/dev/null || true)
# Check auth/status — only trust isConfigured, not connector PUT success
GH_CHECK=$(curl -sS -H "Authorization: Bearer ${TOKEN}" \
"${AGENT_ENDPOINT}/api/v1/Github/auth/status" 2>/dev/null || echo '{}')
if echo "$GH_CHECK" | jq -e '.isConfigured // .hosts[0].isConfigured' 2>/dev/null | grep -q 'true'; then
IS_AUTH=$(echo "$GH_CHECK" | jq -r '.isConfigured // .hosts[0].isConfigured // false')
if [[ "$IS_AUTH" == "true" ]]; then
# Auth confirmed — now create the connector
curl -sS -f -X PUT "${AGENT_ENDPOINT}/api/v2/extendedAgent/connectors/github" \
-H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \
--data "$conn_body" >/dev/null 2>&1 || true
echo " GitHub authorized!"
echo " ok connector/github"
auth_ok=true
break
fi
Expand All @@ -916,14 +936,9 @@ if [[ ${#oauth_repos[@]} -gt 0 ]]; then
echo

if [[ "$auth_ok" == "true" ]]; then
# Re-enter the OAuth-done path: create connector + repos
echo "── Wiring GitHub connector + repos ──"
if [[ -z "$AGENT_UAMI" ]]; then IDENT="SystemAssigned"; else IDENT="$AGENT_UAMI"; fi
# Connector already created in polling loop — wire repos only
echo "── Wiring GitHub repos ──"
TOKEN=$(_dp_token)
body=$(jq -nc --arg id "$IDENT" '{name:"github",type:"AgentConnector",properties:{dataConnectorType:"GitHubOAuth",dataSource:"github-oauth",identity:$id}}')
curl -sS -f -X PUT "${AGENT_ENDPOINT}/api/v2/extendedAgent/connectors/github" \
-H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" --data "$body" >/dev/null && \
echo " ok connector/github" || echo " FAILED connector/github"
count=$(jq '.repos // [] | length' "$FILE")
for i in $(seq 0 $((count - 1))); do
rname=$(jq -r --argjson i "$i" '.repos[$i].name' "$FILE")
Expand Down
2 changes: 1 addition & 1 deletion sreagent-templates/bin/export-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ dp_get() {
local path="$1"
local token
token=$(_dp_token)
curl -sS -f -H "Authorization: Bearer ${token}" "${AGENT_ENDPOINT}${path}" 2>/dev/null || echo "null"
curl -sS -f --max-time 10 -H "Authorization: Bearer ${token}" "${AGENT_ENDPOINT}${path}" 2>/dev/null || echo "null"
}

# Download a file from data-plane to local path
Expand Down
2 changes: 2 additions & 0 deletions sreagent-templates/bin/ps/Clone-Agent.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ param(
[string]$Source,
[string]$FromAgent,
[string]$FromResourceGroup,
[Alias('FromSubscriptionId')]
[string]$FromSubscription,
[Parameter(Mandatory)][string]$AgentName,
[Parameter(Mandatory)][string]$ResourceGroup,
[string]$Location,
[string]$TargetResourceGroups,
[Alias('SubscriptionId')]
[string]$Subscription,
[switch]$ValidateOnly,
[switch]$SkipExtras,
Expand Down
6 changes: 6 additions & 0 deletions sreagent-templates/bin/ps/Deploy-Agent.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ if ($IsDirectory) {
$ExtrasFile = "${AssembleOut}.extras.json"
$CleanupFiles += $ParametersFile, $ExtrasFile, (Split-Path $AssembleTmp -Parent)

# Copy extras.json into InputPath so it survives cleanup and can be re-used
# (e.g. re-running Apply-Extras standalone without a full re-deploy)
$AgentNameForExtras = (Get-Item $InputPath).Name
$PersistedExtras = Join-Path $InputPath "${AgentNameForExtras}.extras.json"
Copy-Item $ExtrasFile $PersistedExtras -Force

Write-Host ''
} elseif (Test-Path $InputPath -PathType Leaf) {
$ParametersFile = $InputPath
Expand Down
6 changes: 3 additions & 3 deletions sreagent-templates/bin/ps/Diff-Agent.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[Alias('s')]
[Alias('s', 'SubscriptionId')]
[string]$Subscription,

[Parameter(Mandatory)]
Expand Down Expand Up @@ -77,7 +77,7 @@ $ARM_BASE = "https://management.azure.com/subscriptions/${Subscription}/resou

# ─────────────────────────── Check if agent exists ───────────────────────────

$AgentJson = az rest -m GET --url "${ARM_BASE}?api-version=${API_VERSION}" -o json 2>$null
$AgentJson = (az rest -m GET --url "${ARM_BASE}?api-version=${API_VERSION}" -o json 2>$null) -join "`n"
if (-not $AgentJson) { $AgentJson = '{}' }

$Endpoint = $AgentJson | Invoke-Jq -Raw -Filter '.properties.agentEndpoint // empty'
Expand Down Expand Up @@ -154,7 +154,7 @@ if (-not $Token) {

function Invoke-Dp {
param([string]$Path)
curl -sS "${Endpoint}${Path}" -H "Authorization: Bearer $Token" 2>$null
(curl -sS "${Endpoint}${Path}" -H "Authorization: Bearer $Token" 2>$null) -join "`n"
}

# ─────────────────────────── Results tracking ───────────────────────────
Expand Down
85 changes: 54 additions & 31 deletions sreagent-templates/bin/ps/Export-Agent.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[Alias('s')]
[Alias('s', 'SubscriptionId')]
[string]$Subscription,

[Parameter(Mandatory)]
Expand Down Expand Up @@ -159,7 +159,11 @@ function _fail { param([string]$Msg) Write-Host " ERROR: $Msg" -ForegroundColor
# ── JSON → YAML via python3 ──
function ConvertTo-Yaml {
param([Parameter(ValueFromPipeline)][string]$Json)
$result = $Json | python3 -c @"
begin { $inputLines = [System.Collections.Generic.List[string]]::new() }
process { if ($Json) { $inputLines.Add($Json) } }
end {
$fullJson = $inputLines -join "`n"
$result = $fullJson | python3 -c @"
import sys, json, yaml

def strip_nulls(obj):
Expand All @@ -173,10 +177,13 @@ data = json.load(sys.stdin)
data = strip_nulls(data)
yaml.dump(data, sys.stdout, default_flow_style=False, sort_keys=False, allow_unicode=True)
"@
if ($LASTEXITCODE -ne 0 -or -not $result) {
throw "python3 YAML conversion failed (exit code: $LASTEXITCODE)"
if ($LASTEXITCODE -ne 0 -or -not $result) {
throw "python3 YAML conversion failed (exit code: $LASTEXITCODE)"
}
# PS captures native-command stdout as string[] (one per line).
# Join with newlines so Set-Content -NoNewline writes proper YAML.
return ($result -join "`n")
}
return $result
}

function Write-Yaml {
Expand All @@ -188,7 +195,7 @@ function Write-Yaml {
function Invoke-ArmGet {
param([string]$Url)
try {
$result = az rest -m GET --url "${Url}?api-version=${API_VERSION}" -o json 2>$null
$result = (az rest -m GET --url "${Url}?api-version=${API_VERSION}" -o json 2>$null) -join "`n"
if ($LASTEXITCODE -ne 0 -or -not $result) { return 'null' }
return $result
} catch {
Expand All @@ -201,7 +208,7 @@ function Invoke-ArmList {
param([string]$ChildType)
$url = "${ARM_BASE}/${ChildType}?api-version=${API_VERSION}"
try {
$result = az rest -m GET --url $url -o json 2>$null
$result = (az rest -m GET --url $url -o json 2>$null) -join "`n"
if ($LASTEXITCODE -ne 0 -or -not $result) { return '[]' }
return ($result | Invoke-Jq -Compact -Filter '.value // []')
} catch {
Expand Down Expand Up @@ -230,7 +237,7 @@ function Invoke-DpGet {
param([string]$Path)
$token = Get-DpToken
try {
$result = curl -sS -f -H "Authorization: Bearer $token" "${AGENT_ENDPOINT}${Path}" 2>$null
$result = (curl -sS -f --max-time 10 -H "Authorization: Bearer $token" "${AGENT_ENDPOINT}${Path}" 2>$null) -join "`n"
if ($LASTEXITCODE -ne 0 -or -not $result) { return 'null' }
return $result
} catch {
Expand All @@ -250,25 +257,34 @@ function Invoke-DpDownload {

# ── Data-plane download tarball ──
function Invoke-DpDownloadTarball {
param([string]$Path, [string]$DestDir, [string]$Label)
$token = Get-DpToken
$tmpfile = [System.IO.Path]::GetTempFileName() + '.tar.gz'
param([string]$Path, [string]$DestDir, [string]$Label, [int]$Retries = 3)
$ok = $false
try {
curl -sS -f -H "Authorization: Bearer $token" -o $tmpfile "${AGENT_ENDPOINT}${Path}" 2>$null
if ($LASTEXITCODE -eq 0) {
if (-not (Test-Path $DestDir)) { New-Item -ItemType Directory -Path $DestDir -Force | Out-Null }
tar -xzf $tmpfile -C $DestDir 2>$null
$count = (Get-ChildItem -Path $DestDir -File -Recurse -ErrorAction SilentlyContinue | Measure-Object).Count
_log " Downloaded ${Label}: ${count} file(s) → ${DestDir}"
$ok = $true
} else {
_log " WARN: Could not download ${Label}"
for ($attempt = 1; $attempt -le $Retries; $attempt++) {
$token = Get-DpToken
$tmpfile = [System.IO.Path]::GetTempFileName() + '.tar.gz'
try {
curl -sS -f -H "Authorization: Bearer $token" -o $tmpfile "${AGENT_ENDPOINT}${Path}" 2>$null
if ($LASTEXITCODE -eq 0) {
if (-not (Test-Path $DestDir)) { New-Item -ItemType Directory -Path $DestDir -Force | Out-Null }
tar -xzf $tmpfile -C $DestDir 2>$null
$count = (Get-ChildItem -Path $DestDir -File -Recurse -ErrorAction SilentlyContinue | Measure-Object).Count
_log " Downloaded ${Label}: ${count} file(s) → ${DestDir}"
$ok = $true
break
}
} catch { }
finally {
Remove-Item $tmpfile -Force -ErrorAction SilentlyContinue
}
if ($attempt -lt $Retries) {
_log " Retry ${attempt}/${Retries} for ${Label} download..."
Start-Sleep -Seconds (5 * $attempt)
# Refresh token on retry
$script:DpTokenCache = ''
}
} catch {
_log " WARN: Could not download ${Label}"
} finally {
Remove-Item $tmpfile -Force -ErrorAction SilentlyContinue
}
if (-not $ok) {
_log " WARN: Could not download ${Label} after ${Retries} attempts"
}
return $ok
}
Expand Down Expand Up @@ -720,7 +736,7 @@ _log 'Checking for webhook bridge (Logic App)...'
$BRIDGE_EXISTS = $false
$bridgeId = "/subscriptions/${Subscription}/resourceGroups/${ResourceGroup}/providers/Microsoft.Logic/workflows/${AgentName}-webhook-bridge"
try {
$BRIDGE_JSON = az resource show --ids $bridgeId -o json 2>$null
$BRIDGE_JSON = (az resource show --ids $bridgeId -o json 2>$null) -join "`n"
if ($LASTEXITCODE -eq 0 -and $BRIDGE_JSON -and $BRIDGE_JSON -ne 'null') {
$BRIDGE_EXISTS = $true
_log " Found webhook bridge: ${AgentName}-webhook-bridge"
Expand Down Expand Up @@ -1049,7 +1065,7 @@ Invoke-Jq -Filter '{
'--arg', 'action', $ACTION_MODE,
'--slurpfile', 'targetRgs', $trgTmpFile,
'--arg', 'enableBridge', $bridgeBool
) -InputFile '/dev/null' | Set-Content -Path (Join-Path $EXPORT_DIR 'agent.json') -Encoding utf8
) | Set-Content -Path (Join-Path $EXPORT_DIR 'agent.json') -Encoding utf8
Remove-Item $trgTmpFile -Force -ErrorAction SilentlyContinue

# Apply --set overrides
Expand Down Expand Up @@ -1158,13 +1174,20 @@ $enableAIStr = if ($ENABLE_AI) { 'true' } else { 'false' }
$enableLAWStr = if ($ENABLE_LAW) { 'true' } else { 'false' }
$enableAzMonStr = if ($ENABLE_AZMON) { 'true' } else { 'false' }

# PowerShell drops empty-string arguments when calling native commands, which
# misaligns jq --arg pairs. Default to a single space so the arg is preserved;
# the values are only used when their toggle is true.
if ([string]::IsNullOrEmpty($AI_RESOURCE_ID)) { $AI_RESOURCE_ID = ' ' }
if ([string]::IsNullOrEmpty($AI_APP_ID)) { $AI_APP_ID = ' ' }
if ([string]::IsNullOrEmpty($LAW_RESOURCE_ID)) { $LAW_RESOURCE_ID = ' ' }

$CONNECTORS_ARRAY | Invoke-Jq -Filter '{
"toggles": {
"enableAppInsightsConnector": ($enableAI | test("true")),
"appInsightsResourceId": $aiResId,
"appInsightsAppId": $aiAppId,
"appInsightsResourceId": ($aiResId | ltrimstr(" ")),
"appInsightsAppId": ($aiAppId | ltrimstr(" ")),
"enableLogAnalyticsConnector": ($enableLAW | test("true")),
"lawResourceId": $lawResId,
"lawResourceId": ($lawResId | ltrimstr(" ")),
"enableAzureMonitorConnector": ($enableAzMon | test("true")),
"azureMonitorLookbackDays": ($azMonLookback | tonumber)
},
Expand Down Expand Up @@ -1283,7 +1306,7 @@ Invoke-Jq -Filter '{
'--slurpfile', 'tasks', (Join-Path $ecTmpDir 'tasks.json'),
'--slurpfile', 'plans', (Join-Path $ecTmpDir 'plans.json'),
'--slurpfile', 'repos', (Join-Path $ecTmpDir 'repos.json')
) -InputFile '/dev/null' | Set-Content -Path (Join-Path $EXPORT_DIR 'expected-config.json') -Encoding utf8
) | Set-Content -Path (Join-Path $EXPORT_DIR 'expected-config.json') -Encoding utf8
Remove-Item $ecTmpDir -Recurse -Force -ErrorAction SilentlyContinue

_log 'Wrote expected-config.json'
Expand Down
Loading
Loading