Files
The-transit-Project/.github/agents/azure-deploy/scaffold/scripts/scaffold-conformance.sh
T
Sven laptop 635a130f0b
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m1s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m13s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
──────
### Summary of Changes
  1. Documentation Consolidation:
      • Relocated the docs/ folder and all repository .md files (README.md, DEPLOYMENT.md, DOCKER_README.md,
      DOMAINS_README.md, GETTING_STARTED.md, CI_CD_PORTAINER_GUIDE.md, ADMIN_DEPLOYMENT_SUMMARY.md,
      SECURITY_PRIVATE_FILES.md, handoff.md, azure-project-plan.md, plus service-specific READMEs) into .
      • Updated document links in README.md.
  2. AI Agent Project Map:
      • Created PROJECT_MAP.md in the project root containing complete architecture diagrams, service specs, ports, DB
      schemas, environment variables, and developer workflows.
      • Added PROJECT_MAP.md to .gitignore.
  3. Monitoring UI & Infrastructure Integration:
      • Added monitoring_ui service to docker-compose.yml (port 5175).
      • Configured Gitea workflow docker-build.yml for monitoring_ui builds.
      • Added CORS middleware headers in index.js.
      • Added nginx.conf copy step in Dockerfile.

  ──────
  ### Recommended Commit Message

    docs: consolidate project documentation into monitoring_ui and add agent project map

    - Move all markdown documentation files and docs/ folder into monitoring_ui/docs/
    - Update documentation hub index (monitoring_ui/docs/README.md) with relative paths
    - Generate comprehensive AI agent reference map (PROJECT_MAP.md) in repository root
    - Add PROJECT_MAP.md and monitoring_ui build output/env overrides to .gitignore
    - Integrate monitoring_ui service into docker-compose.yml and Gitea CI workflow
    - Configure CORS handling in employee_api for monitoring dashboard access
    ──────
  ### Alternative Short Commit Message

    refactor(docs): move docs to monitoring_ui and create AI agent PROJECT_MAP.md
2026-09-20 23:33:55 +02:00

271 lines
17 KiB
Bash

#!/usr/bin/env bash
# ---------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.md in the project root for license information.
# ---------------------------------------------------------------------------------------------
# Scaffold -> Deploy conformance gate (bash twin of scaffold-conformance.ps1).
# Checks generated IaC against the plan for semantic defects `az bicep build` cannot catch.
#
# Usage: scaffold-conformance.sh <sessionPath> [infraPath]
# Output: JSON { passed, failures:[{id,detail,file}] } to stdout.
# Exit: 0 = pass, 1 = one or more BLOCK failures.
#
# Pure-text checks (tags, TLS, purge, deployer role, plaintext secret) always run.
# Plan-dependent checks (version, services-complete, db-name, warn-fixed) run only when `jq` is present.
set -u
SESSION_PATH="${1:?usage: scaffold-conformance.sh <sessionPath> [infraPath]}"
INFRA_PATH="${2:-infra}"
PLAN="$SESSION_PATH/prepare-plan.json"
MAIN="$INFRA_PATH/main.bicep"
PARAMS="$INFRA_PATH/main.parameters.json"
main_bicep="$( [ -f "$MAIN" ] && cat "$MAIN" || true )"
params_raw="$( [ -f "$PARAMS" ] && cat "$PARAMS" || true )"
iac="$( find "$INFRA_PATH" -name '*.bicep' -type f 2>/dev/null -exec cat {} + || true )"
HAVE_JQ=0; command -v jq >/dev/null 2>&1 && HAVE_JQ=1
# failures accumulator (JSON objects, newline-separated)
fails=""
add_fail() { # id detail file
local obj; obj="$(printf '{"id":"%s","detail":"%s","file":"%s"}' "$1" "$2" "$3")"
fails="${fails:+$fails,}$obj"
}
iac_has() { printf '%s' "$iac" | grep -qE "$1"; }
# 1. TAGS-NO-CAMEL
if printf '%s' "$main_bicep" | grep -qE 'appOnboard(Skill|SessionId)|createdAt:|deployedBy:'; then
add_fail "TAGS-NO-CAMEL" "camelCase tag keys found — use hyphenated ('app-onboard-skill' ...)" "infra/main.bicep"
fi
# 12. DIAG-GATED — we don't scaffold diagnostic-settings; if present it must be gated behind enableDiagnostics (default false) or removed. Unconditional wiring blocks the first deploy.
# Match an actual diagnostic module/resource declaration, not a prose comment mentioning 'diagnostic'.
if printf '%s' "$main_bicep" | grep -qE 'module[[:space:]]+[A-Za-z]*[Dd]iagnostic|Microsoft\.Insights/diagnosticSettings' && ! printf '%s' "$main_bicep" | grep -q 'enableDiagnostics'; then
add_fail "DIAG-GATED" "diagnostic-settings module wired without an enableDiagnostics gate — gate it (module ... = if (enableDiagnostics), default false) or remove it; unconditional wiring blocks the first deploy" "infra/main.bicep"
fi
# 2. NO-PLAINTEXT-SECRET (jq-free: awk over the params file so it runs on hosts without jq)
if [ -n "$params_raw" ]; then
bad_keys="$(printf '%s' "$params_raw" | awk '
{ line=tolower($0) }
line ~ /"[a-z0-9_]*(password|secret|connstring|connection)[a-z0-9_]*"[ \t]*:/ {
n=split($0, parts, "\"")
for (i=1;i<=n;i++) { if (tolower(parts[i]) ~ /(password|secret|connstring|connection)/) { pend=parts[i]; break } }
}
pend != "" && line ~ /"value"[ \t]*:[ \t]*"[^"]+"/ { print pend; pend="" }
/}/ { pend="" }
')"
if [ -n "$bad_keys" ]; then
while IFS= read -r k; do
[ -z "$k" ] && continue
add_fail "NO-PLAINTEXT-SECRET" "parameter '$k' has a literal value — must be @secure(), passed at deploy" "infra/main.parameters.json"
done <<< "$bad_keys"
fi
fi
# 2b. NO-BICEP-LITERAL-SECRET (pure-text) — secret values must be @secure() params, never quoted literals.
# Property name must end in 'password'/'connectionString' right before ':' (so 'secretName' etc. never match).
if [ -n "$iac" ]; then
if printf '%s' "$iac" | grep -qiE "[A-Za-z]*(password|connectionstring)[[:space:]]*:[[:space:]]*'[^']+'"; then
k="$(printf '%s' "$iac" | grep -oiE "[A-Za-z]*(password|connectionstring)[[:space:]]*:[[:space:]]*'[^']+'" | head -n1 | sed -E "s/[[:space:]]*:.*//")"
add_fail "NO-BICEP-LITERAL-SECRET" "property '$k' assigned a literal secret in Bicep — use an @secure() param passed at deploy" "infra/"
fi
if printf '%s' "$iac" | grep -qE ":[[:space:]]*'[^']*(Password=|AccountKey=|SharedAccessKey=|://[^:@/ ]+:[^:@/ ]+@)[^']*'"; then
add_fail "NO-BICEP-LITERAL-SECRET" "quoted literal contains an embedded credential (Password=/AccountKey=/user:pass@) in Bicep — use an @secure() param" "infra/"
fi
fi
# service-name -> resource-type map (longest keys first so 'container apps environment' wins)
declare -a MAP_KEYS=( "container apps environment" "container app" "container registry" "mysql" "postgres" \
"log analytics" "application insights" "static web app" "app service" "functions" \
"sql" "cosmos" "redis" "storage" "service bus" "event hub" )
map_type() {
case "$1" in
*"container apps environment"*) echo 'Microsoft\.App/managedEnvironments';;
*"container app"*) echo 'Microsoft\.App/containerApps';;
*"container registry"*) echo 'Microsoft\.ContainerRegistry/registries';;
*mysql*) echo 'Microsoft\.DBforMySQL/flexibleServers';;
*postgres*) echo 'Microsoft\.DBforPostgreSQL/flexibleServers';;
*"log analytics"*) echo 'Microsoft\.OperationalInsights/workspaces';;
*"application insights"*) echo 'Microsoft\.Insights/components';;
*"static web app"*) echo 'Microsoft\.Web/staticSites';;
*"app service"*|*functions*) echo 'Microsoft\.Web/sites';;
*sql*) echo 'Microsoft\.Sql/servers';;
*cosmos*) echo 'Microsoft\.DocumentDB/databaseAccounts';;
*redis*) echo 'Microsoft\.Cache/redis';;
*storage*) echo 'Microsoft\.Storage/storageAccounts';;
*"service bus"*) echo 'Microsoft\.ServiceBus/namespaces';;
*"event hub"*) echo 'Microsoft\.EventHub/namespaces';;
*) echo '';;
esac
}
svc_names=""; db_version=""; app_db_name=""; has_mysql=0; has_pg=0
if [ "$HAVE_JQ" = 1 ] && [ -f "$PLAN" ]; then
svc_names="$(jq -r '.services[].name' "$PLAN" 2>/dev/null)"
app_db_name="$(jq -r '.appDbName // empty' "$PLAN" 2>/dev/null)"
printf '%s' "$svc_names" | grep -qiE 'mysql' && has_mysql=1
printf '%s' "$svc_names" | grep -qiE 'postgres' && has_pg=1
# 3. SERVICES-COMPLETE
while IFS= read -r name; do
[ -z "$name" ] && continue
lc="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')"
expected="$(map_type "$lc")"
if [ -n "$expected" ] && ! iac_has "$expected"; then
add_fail "SERVICES-COMPLETE" "planned service '$name' has no matching resource ($expected) in IaC" "infra/"
fi
done < <(printf '%s\n' "$svc_names")
else
# jq absent: fall back to grep for the DB-type presence (drives TLS/name checks below)
printf '%s' "$iac" | grep -qE 'Microsoft\.DBforMySQL/flexibleServers' && has_mysql=1
printf '%s' "$iac" | grep -qE 'Microsoft\.DBforPostgreSQL/flexibleServers' && has_pg=1
fi
# --- Managed database checks ---
if [ "$has_mysql" = 1 ] || [ "$has_pg" = 1 ]; then
# 4. DB-VERSION-MATCH — each server's version must equal its capabilities-verified plan value.
# Scope the match to the DB resource block so an unrelated 'version:' elsewhere isn't picked up.
if [ "$HAVE_JQ" = 1 ] && [ -f "$PLAN" ]; then
while IFS=$'\t' read -r dbkind dbver; do
[ -z "$dbver" ] && continue
case "$dbkind" in
*MySQL*) marker='Microsoft[.]DBforMySQL/flexibleServers';;
*) marker='Microsoft[.]DBforPostgreSQL/flexibleServers';;
esac
iac_ver="$(printf '%s\n' "$iac" | awk -v marker="$marker" '
$0 ~ marker { armed=1 }
armed==1 && match($0, /version:[ \t]*'"'"'[^'"'"']+'"'"'/) {
v=substr($0,RSTART,RLENGTH); sub(/version:[ \t]*'"'"'/,"",v); sub(/'"'"'.*/,"",v); print v; exit
}')"
if [ -n "$iac_ver" ] && [ "$iac_ver" != "$dbver" ]; then
add_fail "DB-VERSION-MATCH" "$dbkind IaC version '$iac_ver' != plan '$dbver'" "infra/modules"
fi
done < <(jq -r '.services[]? | select(.name|test("MySQL|PostgreSQL")) | [.name, (.version // "")] | @tsv' "$PLAN" 2>/dev/null)
fi
# 5. DB-TLS-ON (MySQL, pure-text)
if [ "$has_mysql" = 1 ] && ! iac_has 'require_secure_transport'; then
add_fail "DB-TLS-ON" "MySQL module missing require_secure_transport config" "infra/modules"
fi
# 6. DB-NAME-PRESENT (needs plan.appDbName from jq)
if [ -n "$app_db_name" ] && ! iac_has 'flexibleServers/databases'; then
add_fail "DB-NAME-PRESENT" "app DB '$app_db_name' declared but no flexibleServers/databases resource" "infra/modules"
fi
# 10. MYSQL-NO-NETWORK-BLOCK (MySQL, pure-text) — public flow must omit the network block;
# an empty delegatedSubnetResourceId is rejected by ARM (LinkedInvalidPropertyId).
if [ "$has_mysql" = 1 ] && iac_has 'delegatedSubnetResourceId|privateDnsZoneResourceId'; then
add_fail "MYSQL-NO-NETWORK-BLOCK" "MySQL module includes a network block (delegatedSubnetResourceId) — omit it for public access; an empty value is rejected by ARM" "infra/modules"
fi
# 11. DB-NO-LOCAL-AUTH (pure-text) — databases MUST be Entra / managed-identity only.
if iac_has "administratorLogin[[:space:]]*:"; then
add_fail "DB-NO-LOCAL-AUTH" "administratorLogin/administratorLoginPassword present — databases must be Entra-only (authConfig passwordAuth Disabled + Entra admin). Remove ALL admin login/password params." "infra/modules"
fi
if iac_has "passwordAuth[[:space:]]*:[[:space:]]*'Enabled'"; then
add_fail "DB-NO-LOCAL-AUTH" "PostgreSQL authConfig sets passwordAuth: 'Enabled' — must be 'Disabled' (Entra-only)" "infra/modules"
fi
# 11b. DB-ENTRA-ADMIN — PG/MySQL must set an Entra administrator for token-based migrations.
if ! iac_has 'flexibleServers/administrators'; then
add_fail "DB-ENTRA-ADMIN" "PostgreSQL/MySQL module missing an administrators (Entra admin) child resource — required for Entra-only auth + token-based migrations" "infra/modules"
fi
# 11c. PG-ENTRA-ONLY — PostgreSQL must explicitly disable password auth.
if [ "$has_pg" = 1 ] && ! iac_has "passwordAuth[[:space:]]*:[[:space:]]*'Disabled'"; then
add_fail "PG-ENTRA-ONLY" "PostgreSQL module missing authConfig { passwordAuth: 'Disabled', activeDirectoryAuth: 'Enabled' }" "infra/modules"
fi
# 11c2. MYSQL-ENTRA-UAMI — MySQL AAD admin requires a user-assigned MI; identityResourceId: null fails to deploy.
if [ "$has_mysql" = 1 ]; then
if iac_has "identityResourceId[[:space:]]*:[[:space:]]*null"; then
add_fail "MYSQL-ENTRA-UAMI" "MySQL administrators sets identityResourceId: null — will not deploy. Set it to a user-assigned managed identity resourceId and add that UAMI to the server's identity block." "infra/modules"
elif ! iac_has 'identityResourceId'; then
add_fail "MYSQL-ENTRA-UAMI" "MySQL administrators missing identityResourceId — MySQL Entra admin REQUIRES a user-assigned managed identity (unlike PostgreSQL)." "infra/modules"
fi
fi
fi
# --- No shared-key / access-key / local-auth on any data service (managed-identity only) ---
# 11d. STORAGE-NO-SHARED-KEY
if iac_has 'Microsoft\.Storage/storageAccounts' && ! iac_has "allowSharedKeyAccess[[:space:]]*:[[:space:]]*false"; then
add_fail "STORAGE-NO-SHARED-KEY" "Storage account must set allowSharedKeyAccess: false (managed-identity only)" "infra/modules"
fi
# 11e. REDIS-NO-KEYS
if iac_has 'Microsoft\.Cache/redis' && ! iac_has "disableAccessKeyAuthentication[[:space:]]*:[[:space:]]*true"; then
add_fail "REDIS-NO-KEYS" "Redis must set disableAccessKeyAuthentication: true + accessPolicyAssignments (Entra only)" "infra/modules"
fi
# 11f. COSMOS-NO-LOCAL-AUTH
if iac_has 'Microsoft\.DocumentDB/databaseAccounts' && ! iac_has "disableLocalAuth[[:space:]]*:[[:space:]]*true"; then
add_fail "COSMOS-NO-LOCAL-AUTH" "Cosmos DB must set disableLocalAuth: true (data-plane RBAC only)" "infra/modules"
fi
# 11g. MSG-NO-LOCAL-AUTH
if iac_has 'Microsoft\.ServiceBus/namespaces|Microsoft\.EventHub/namespaces' && ! iac_has "disableLocalAuth[[:space:]]*:[[:space:]]*true"; then
add_fail "MSG-NO-LOCAL-AUTH" "Service Bus / Event Hubs namespace must set disableLocalAuth: true (managed-identity only)" "infra/modules"
fi
# 11h. NO-FREE-SKU — free/shared compute tiers are not allowed (managed identity is mandatory).
if iac_has "name[[:space:]]*:[[:space:]]*'(F1|D1)'"; then
sku="$(printf '%s' "$iac" | grep -oE "name[[:space:]]*:[[:space:]]*'(F1|D1)'" | grep -oE '(F1|D1)' | head -n1)"
add_fail "NO-FREE-SKU" "App Service plan uses a free/shared SKU ('${sku}') — floor is B1 (managed identity is mandatory; the free-tier MI sidecar OOMs)" "infra/modules"
fi
if iac_has 'Microsoft\.Web/staticSites' && iac_has "tier[[:space:]]*:[[:space:]]*'Free'"; then
add_fail "NO-FREE-SKU" "Static Web App uses the Free tier — floor is Standard" "infra/modules"
fi
# --- No Key Vault (app-internal secrets live on the compute resource) ---
if iac_has 'Microsoft\.KeyVault/vaults' || iac_has '@Microsoft\.KeyVault' || iac_has 'keyVaultUrl'; then
add_fail "NO-KEYVAULT" "Key Vault detected (Microsoft.KeyVault/vaults, @Microsoft.KeyVault(...), or keyVaultUrl) — no Key Vault is allowed. Store app-internal secrets on the compute resource: @secure() param -> App Service appSettings / Container Apps native secrets[]." "infra/"
fi
# --- Container Apps / ACR checks (deterministic ARM-failure invariants) ---
if [ -n "$iac" ]; then
has_ca=0; iac_has 'Microsoft\.App/containerApps' && has_ca=1
has_cae=0; iac_has 'Microsoft\.App/managedEnvironments' && has_cae=1
has_acr=0; iac_has 'Microsoft\.ContainerRegistry/registries' && has_acr=1
# 13. CAE-APPLOGS — managedEnvironments log config MUST nest under appLogsConfiguration; a bare
# top-level logAnalyticsConfiguration fails deploy with ManagedEnvironmentInvalidSchema.
if [ "$has_cae" = 1 ] && iac_has 'logAnalyticsConfiguration' && ! iac_has 'appLogsConfiguration'; then
add_fail "CAE-APPLOGS" "Container Apps Environment uses a bare logAnalyticsConfiguration — nest it under appLogsConfiguration.destination='log-analytics' (else ManagedEnvironmentInvalidSchema at deploy)" "infra/modules"
fi
# 14. CA-NO-REVISION-SUFFIX — a hardcoded revisionSuffix fails Phase 2 redeploy ('revision already exists').
if [ "$has_ca" = 1 ] && iac_has 'revisionSuffix[[:space:]]*:'; then
add_fail "CA-NO-REVISION-SUFFIX" "Container App sets revisionSuffix — omit it (ARM auto-generates); a hardcoded value fails Phase 2 redeploy" "infra/modules"
fi
# 15. CA-IMAGE-PARAM — two-phase deploy needs 'param containerImage' in main.bicep, else the Phase 2 override is silently ignored and the placeholder image persists.
if [ "$has_ca" = 1 ] && [ "$has_acr" = 1 ] && ! printf '%s' "$main_bicep" | grep -qE 'param[[:space:]]+containerImage'; then
add_fail "CA-IMAGE-PARAM" "main.bicep lacks 'param containerImage' — the Phase 2 image override is silently ignored and the placeholder persists (MANIFEST_UNKNOWN)" "infra/main.bicep"
fi
# 16. ACRPULL-GUID — near-miss detector: fixed AcrPull prefix present without the canonical full GUID = hallucinated last segment → RoleDefinitionDoesNotExist. Cannot false-positive.
if iac_has '7f951dda-4ed3-4680-a7ca-' && ! iac_has '7f951dda-4ed3-4680-a7ca-43fe172d538d'; then
add_fail "ACRPULL-GUID" "AcrPull role GUID is wrong — canonical value is 7f951dda-4ed3-4680-a7ca-43fe172d538d (RoleDefinitionDoesNotExist otherwise)" "infra/modules/role-assignments.bicep"
fi
# 17. ACR-NO-PREMIUM-POLICY — retention/trust/quarantine are Premium-only; on Basic/Standard ACR they fail SkuNotSupported. FP-safe: skipped if any 'Premium' SKU appears.
if [ "$has_acr" = 1 ] && iac_has '(retentionPolicy|trustPolicy|quarantinePolicy)[[:space:]]*:' && ! iac_has "'Premium'"; then
add_fail "ACR-NO-PREMIUM-POLICY" "ACR has a Premium-only policy (retention/trust/quarantine) but is not Premium — omit these for Basic/Standard (SkuNotSupported at deploy)" "infra/modules"
fi
fi
# 9. WARN-FIXED (needs jq to read prereq-output.json warnings[]) — prereq warnings flagged
# fixPhase:"scaffold" must land in the IaC. Only warnings with a provable signal are enforced;
# unverifiable ones are skipped (no false BLOCK). Extend the case map for new provable warning IDs.
PREREQ="$SESSION_PATH/prereq-output.json"
if [ "$HAVE_JQ" = 1 ] && [ -f "$PREREQ" ]; then
while IFS=$'\t' read -r wid wfix; do
[ -z "$wid" ] && continue
present=0
case "$wid" in
W-PG-SSL) printf '%s' "$iac" | grep -qiE 'PGSSLMODE|sslmode' && present=1 ;;
W-BUILDKIT) [ -n "$(find . -name 'Dockerfile.azure' -type f 2>/dev/null | head -n1)" ] && present=1 ;;
*) continue ;; # no deterministic signal — cannot verify, do not block
esac
if [ "$present" != 1 ]; then
add_fail "WARN-FIXED" "prereq warning '$wid' (fixPhase:scaffold) fix not found in IaC — $wfix" "infra/"
fi
done < <(jq -r '.warnings[]? | select(.fixPhase=="scaffold") | [.id, .fix] | @tsv' "$PREREQ" 2>/dev/null)
fi
if [ -z "$fails" ]; then
printf '{"passed":true,"failures":[]}\n'
exit 0
else
printf '{"passed":false,"failures":[%s]}\n' "$fails"
exit 1
fi