@polymorphism-tech/morph-spec 1.0.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1381 -0
- package/LICENSE +72 -0
- package/README.md +89 -6
- package/bin/detect-agents.js +225 -0
- package/bin/morph-spec.js +120 -0
- package/bin/render-template.js +302 -0
- package/bin/semantic-detect-agents.js +246 -0
- package/bin/validate-agents-skills.js +239 -0
- package/bin/validate-agents.js +69 -0
- package/bin/validate-phase.js +263 -0
- package/content/.azure/README.md +293 -0
- package/content/.azure/docs/azure-devops-setup.md +454 -0
- package/content/.azure/docs/branch-strategy.md +398 -0
- package/content/.azure/docs/local-development.md +515 -0
- package/content/.azure/pipelines/pipeline-variables.yml +34 -0
- package/content/.azure/pipelines/prod-pipeline.yml +319 -0
- package/content/.azure/pipelines/staging-pipeline.yml +234 -0
- package/content/.azure/pipelines/templates/build-dotnet.yml +75 -0
- package/content/.azure/pipelines/templates/deploy-app-service.yml +94 -0
- package/content/.azure/pipelines/templates/deploy-container-app.yml +120 -0
- package/content/.azure/pipelines/templates/infra-deploy.yml +90 -0
- package/content/.claude/commands/morph-apply.md +118 -26
- package/content/.claude/commands/morph-archive.md +9 -9
- package/content/.claude/commands/morph-clarify.md +184 -0
- package/content/.claude/commands/morph-design.md +275 -0
- package/content/.claude/commands/morph-proposal.md +56 -15
- package/content/.claude/commands/morph-setup.md +100 -0
- package/content/.claude/commands/morph-status.md +47 -32
- package/content/.claude/commands/morph-tasks.md +319 -0
- package/content/.claude/commands/morph-uiux.md +211 -0
- package/content/.claude/skills/specialists/ai-system-architect.md +604 -0
- package/content/.claude/skills/specialists/ms-agent-expert.md +143 -89
- package/content/.claude/skills/specialists/ui-ux-designer.md +744 -9
- package/content/.claude/skills/stacks/dotnet-blazor.md +244 -8
- package/content/.claude/skills/stacks/dotnet-nextjs.md +2 -2
- package/content/.morph/.morphversion +5 -0
- package/content/.morph/config/agents.json +101 -8
- package/content/.morph/config/azure-pricing.json +70 -0
- package/content/.morph/config/azure-pricing.schema.json +50 -0
- package/content/.morph/config/config.template.json +15 -3
- package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -0
- package/content/.morph/hooks/README.md +239 -0
- package/content/.morph/hooks/pre-commit-agents.sh +24 -0
- package/content/.morph/hooks/pre-commit-all.sh +48 -0
- package/content/.morph/hooks/pre-commit-costs.sh +91 -0
- package/content/.morph/hooks/pre-commit-specs.sh +49 -0
- package/content/.morph/hooks/pre-commit-tests.sh +60 -0
- package/content/.morph/project.md +5 -4
- package/content/.morph/schemas/agent.schema.json +296 -0
- package/content/.morph/standards/agent-framework-setup.md +453 -0
- package/content/.morph/standards/architecture.md +142 -7
- package/content/.morph/standards/azure.md +218 -23
- package/content/.morph/standards/coding.md +47 -12
- package/content/.morph/standards/dotnet10-migration.md +494 -0
- package/content/.morph/standards/fluent-ui-setup.md +590 -0
- package/content/.morph/standards/migration-guide.md +514 -0
- package/content/.morph/standards/passkeys-auth.md +423 -0
- package/content/.morph/standards/vector-search-rag.md +536 -0
- package/content/.morph/state.json +18 -0
- package/content/.morph/templates/FluentDesignTheme.cs +149 -0
- package/content/.morph/templates/MudTheme.cs +281 -0
- package/content/.morph/templates/contracts.cs +55 -55
- package/content/.morph/templates/decisions.md +4 -4
- package/content/.morph/templates/design-system.css +226 -0
- package/content/.morph/templates/infra/.dockerignore.example +89 -0
- package/content/.morph/templates/infra/Dockerfile.example +82 -0
- package/content/.morph/templates/infra/README.md +286 -0
- package/content/.morph/templates/infra/app-service.bicep +164 -0
- package/content/.morph/templates/infra/deploy.ps1 +229 -0
- package/content/.morph/templates/infra/deploy.sh +208 -0
- package/content/.morph/templates/infra/main.bicep +41 -7
- package/content/.morph/templates/infra/parameters.dev.json +6 -0
- package/content/.morph/templates/infra/parameters.prod.json +6 -0
- package/content/.morph/templates/infra/parameters.staging.json +29 -0
- package/content/.morph/templates/proposal.md +3 -3
- package/content/.morph/templates/recap.md +3 -3
- package/content/.morph/templates/spec.md +9 -8
- package/content/.morph/templates/sprint-status.yaml +68 -0
- package/content/.morph/templates/state.template.json +222 -0
- package/content/.morph/templates/story.md +143 -0
- package/content/.morph/templates/tasks.md +1 -1
- package/content/.morph/templates/ui-components.md +276 -0
- package/content/.morph/templates/ui-design-system.md +286 -0
- package/content/.morph/templates/ui-flows.md +336 -0
- package/content/.morph/templates/ui-mockups.md +133 -0
- package/content/.morph/test-infra/example.bicep +59 -0
- package/content/CLAUDE.md +124 -0
- package/content/README.md +79 -0
- package/detectors/config-detector.js +223 -0
- package/detectors/conversation-analyzer.js +163 -0
- package/detectors/index.js +84 -0
- package/detectors/standards-generator.js +275 -0
- package/detectors/structure-detector.js +221 -0
- package/docs/README.md +149 -0
- package/docs/api/cost-calculator.js.html +513 -0
- package/docs/api/design-system-generator.js.html +382 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/api/global.html +5263 -0
- package/docs/api/index.html +96 -0
- package/docs/api/scripts/collapse.js +39 -0
- package/docs/api/scripts/commonNav.js +28 -0
- package/docs/api/scripts/linenumber.js +25 -0
- package/docs/api/scripts/nav.js +12 -0
- package/docs/api/scripts/polyfill.js +4 -0
- package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/api/scripts/prettify/lang-css.js +2 -0
- package/docs/api/scripts/prettify/prettify.js +28 -0
- package/docs/api/scripts/search.js +99 -0
- package/docs/api/state-manager.js.html +423 -0
- package/docs/api/styles/jsdoc.css +776 -0
- package/docs/api/styles/prettify.css +80 -0
- package/docs/examples.md +328 -0
- package/docs/getting-started.md +302 -0
- package/docs/installation.md +361 -0
- package/docs/templates.md +418 -0
- package/docs/validation-checklist.md +266 -0
- package/package.json +39 -12
- package/src/commands/cost.js +181 -0
- package/src/commands/create-story.js +283 -0
- package/src/commands/detect.js +104 -0
- package/src/commands/doctor.js +67 -0
- package/src/commands/generate.js +149 -0
- package/src/commands/init.js +69 -45
- package/src/commands/shard-spec.js +224 -0
- package/src/commands/sprint-status.js +250 -0
- package/src/commands/state.js +333 -0
- package/src/commands/sync.js +167 -0
- package/src/commands/update-pricing.js +206 -0
- package/src/commands/update.js +88 -13
- package/src/lib/complexity-analyzer.js +292 -0
- package/src/lib/cost-calculator.js +429 -0
- package/src/lib/design-system-generator.js +298 -0
- package/src/lib/state-manager.js +340 -0
- package/src/utils/file-copier.js +59 -0
- package/src/utils/version-checker.js +175 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// MORPH-SPEC - App Service
|
|
3
|
+
// Azure App Service with Free F1 tier support
|
|
4
|
+
// ==============================================================================
|
|
5
|
+
|
|
6
|
+
@description('App Service name')
|
|
7
|
+
param name string
|
|
8
|
+
|
|
9
|
+
@description('Location')
|
|
10
|
+
param location string
|
|
11
|
+
|
|
12
|
+
@description('Tags')
|
|
13
|
+
param tags object = {}
|
|
14
|
+
|
|
15
|
+
@description('App Service Plan SKU (F1, B1, S1, P1v2)')
|
|
16
|
+
@allowed(['F1', 'B1', 'S1', 'P1v2', 'P2v2'])
|
|
17
|
+
param sku string = 'F1'
|
|
18
|
+
|
|
19
|
+
@description('App Insights connection string')
|
|
20
|
+
param appInsightsConnectionString string = ''
|
|
21
|
+
|
|
22
|
+
@description('.NET version')
|
|
23
|
+
@allowed(['v6.0', 'v7.0', 'v8.0'])
|
|
24
|
+
param dotnetVersion string = 'v8.0'
|
|
25
|
+
|
|
26
|
+
@description('Always On (not available on Free tier)')
|
|
27
|
+
param alwaysOn bool = false
|
|
28
|
+
|
|
29
|
+
@description('Environment variables')
|
|
30
|
+
param envVars object = {}
|
|
31
|
+
|
|
32
|
+
// ==============================================================================
|
|
33
|
+
// VARIABLES
|
|
34
|
+
// ==============================================================================
|
|
35
|
+
|
|
36
|
+
var skuTiers = {
|
|
37
|
+
F1: 'Free'
|
|
38
|
+
B1: 'Basic'
|
|
39
|
+
S1: 'Standard'
|
|
40
|
+
P1v2: 'PremiumV2'
|
|
41
|
+
P2v2: 'PremiumV2'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var planName = 'plan-${name}'
|
|
45
|
+
|
|
46
|
+
// Merge default env vars with custom ones
|
|
47
|
+
var defaultEnvVars = {
|
|
48
|
+
ASPNETCORE_ENVIRONMENT: 'Production'
|
|
49
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var mergedEnvVars = union(defaultEnvVars, envVars)
|
|
53
|
+
|
|
54
|
+
// ==============================================================================
|
|
55
|
+
// APP SERVICE PLAN
|
|
56
|
+
// ==============================================================================
|
|
57
|
+
|
|
58
|
+
resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = {
|
|
59
|
+
name: planName
|
|
60
|
+
location: location
|
|
61
|
+
tags: tags
|
|
62
|
+
sku: {
|
|
63
|
+
name: sku
|
|
64
|
+
tier: skuTiers[sku]
|
|
65
|
+
}
|
|
66
|
+
kind: 'linux'
|
|
67
|
+
properties: {
|
|
68
|
+
reserved: true // Required for Linux
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ==============================================================================
|
|
73
|
+
// APP SERVICE
|
|
74
|
+
// ==============================================================================
|
|
75
|
+
|
|
76
|
+
resource appService 'Microsoft.Web/sites@2022-09-01' = {
|
|
77
|
+
name: name
|
|
78
|
+
location: location
|
|
79
|
+
tags: tags
|
|
80
|
+
kind: 'app,linux'
|
|
81
|
+
identity: {
|
|
82
|
+
type: 'SystemAssigned'
|
|
83
|
+
}
|
|
84
|
+
properties: {
|
|
85
|
+
serverFarmId: appServicePlan.id
|
|
86
|
+
httpsOnly: true
|
|
87
|
+
siteConfig: {
|
|
88
|
+
linuxFxVersion: 'DOTNETCORE|${dotnetVersion}'
|
|
89
|
+
alwaysOn: alwaysOn
|
|
90
|
+
minTlsVersion: '1.2'
|
|
91
|
+
ftpsState: 'Disabled'
|
|
92
|
+
http20Enabled: true
|
|
93
|
+
healthCheckPath: '/health'
|
|
94
|
+
appSettings: [for key in objectKeys(mergedEnvVars): {
|
|
95
|
+
name: key
|
|
96
|
+
value: mergedEnvVars[key]
|
|
97
|
+
}]
|
|
98
|
+
cors: {
|
|
99
|
+
allowedOrigins: [
|
|
100
|
+
'https://portal.azure.com'
|
|
101
|
+
]
|
|
102
|
+
supportCredentials: false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
clientAffinityEnabled: false // Recommended for stateless apps
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ==============================================================================
|
|
110
|
+
// DEPLOYMENT SLOT (Only for non-Free tiers)
|
|
111
|
+
// ==============================================================================
|
|
112
|
+
|
|
113
|
+
resource stagingSlot 'Microsoft.Web/sites/slots@2022-09-01' = if (sku != 'F1') {
|
|
114
|
+
name: 'staging'
|
|
115
|
+
parent: appService
|
|
116
|
+
location: location
|
|
117
|
+
tags: tags
|
|
118
|
+
kind: 'app,linux'
|
|
119
|
+
identity: {
|
|
120
|
+
type: 'SystemAssigned'
|
|
121
|
+
}
|
|
122
|
+
properties: {
|
|
123
|
+
serverFarmId: appServicePlan.id
|
|
124
|
+
httpsOnly: true
|
|
125
|
+
siteConfig: {
|
|
126
|
+
linuxFxVersion: 'DOTNETCORE|${dotnetVersion}'
|
|
127
|
+
alwaysOn: alwaysOn
|
|
128
|
+
minTlsVersion: '1.2'
|
|
129
|
+
ftpsState: 'Disabled'
|
|
130
|
+
http20Enabled: true
|
|
131
|
+
healthCheckPath: '/health'
|
|
132
|
+
appSettings: [for key in objectKeys(mergedEnvVars): {
|
|
133
|
+
name: key
|
|
134
|
+
value: mergedEnvVars[key]
|
|
135
|
+
}]
|
|
136
|
+
}
|
|
137
|
+
clientAffinityEnabled: false
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ==============================================================================
|
|
142
|
+
// OUTPUTS
|
|
143
|
+
// ==============================================================================
|
|
144
|
+
|
|
145
|
+
@description('App Service ID')
|
|
146
|
+
output id string = appService.id
|
|
147
|
+
|
|
148
|
+
@description('App Service name')
|
|
149
|
+
output name string = appService.name
|
|
150
|
+
|
|
151
|
+
@description('App Service URL')
|
|
152
|
+
output url string = 'https://${appService.properties.defaultHostName}'
|
|
153
|
+
|
|
154
|
+
@description('App Service default hostname')
|
|
155
|
+
output defaultHostName string = appService.properties.defaultHostName
|
|
156
|
+
|
|
157
|
+
@description('App Service Plan ID')
|
|
158
|
+
output planId string = appServicePlan.id
|
|
159
|
+
|
|
160
|
+
@description('App Service Managed Identity Principal ID')
|
|
161
|
+
output principalId string = appService.identity.principalId
|
|
162
|
+
|
|
163
|
+
@description('Staging slot URL (if available)')
|
|
164
|
+
output stagingUrl string = sku != 'F1' ? 'https://${stagingSlot.properties.defaultHostName}' : ''
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# MORPH-SPEC - Deploy Script (PowerShell)
|
|
3
|
+
# Automated deployment of Azure infrastructure
|
|
4
|
+
# ==============================================================================
|
|
5
|
+
|
|
6
|
+
[CmdletBinding()]
|
|
7
|
+
param(
|
|
8
|
+
[Parameter(Mandatory=$false)]
|
|
9
|
+
[string]$AppName = "myapp",
|
|
10
|
+
|
|
11
|
+
[Parameter(Mandatory=$false)]
|
|
12
|
+
[ValidateSet("dev", "staging", "prod")]
|
|
13
|
+
[string]$Environment = "dev",
|
|
14
|
+
|
|
15
|
+
[Parameter(Mandatory=$false)]
|
|
16
|
+
[string]$Location = "eastus",
|
|
17
|
+
|
|
18
|
+
[Parameter(Mandatory=$false)]
|
|
19
|
+
[string]$SubscriptionId,
|
|
20
|
+
|
|
21
|
+
[Parameter(Mandatory=$false)]
|
|
22
|
+
[ValidateSet("appservice", "containerapp")]
|
|
23
|
+
[string]$HostingType = "appservice",
|
|
24
|
+
|
|
25
|
+
[Parameter(Mandatory=$false)]
|
|
26
|
+
[ValidateSet("F1", "B1", "S1", "P1v2")]
|
|
27
|
+
[string]$AppServiceSku = "F1",
|
|
28
|
+
|
|
29
|
+
[Parameter(Mandatory=$false)]
|
|
30
|
+
[string]$ContainerImage = "mcr.microsoft.com/hello-world:latest"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# ==============================================================================
|
|
34
|
+
# CONFIGURATION
|
|
35
|
+
# ==============================================================================
|
|
36
|
+
|
|
37
|
+
$ErrorActionPreference = "Stop"
|
|
38
|
+
|
|
39
|
+
$ResourceGroup = "rg-$AppName-$Environment"
|
|
40
|
+
$DeploymentName = "deploy-$AppName-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
|
|
41
|
+
|
|
42
|
+
# ==============================================================================
|
|
43
|
+
# FUNCTIONS
|
|
44
|
+
# ==============================================================================
|
|
45
|
+
|
|
46
|
+
function Write-InfoLog {
|
|
47
|
+
param([string]$Message)
|
|
48
|
+
Write-Host "ℹ️ $Message" -ForegroundColor Blue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function Write-SuccessLog {
|
|
52
|
+
param([string]$Message)
|
|
53
|
+
Write-Host "✅ $Message" -ForegroundColor Green
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Write-WarningLog {
|
|
57
|
+
param([string]$Message)
|
|
58
|
+
Write-Host "⚠️ $Message" -ForegroundColor Yellow
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function Write-ErrorLog {
|
|
62
|
+
param([string]$Message)
|
|
63
|
+
Write-Host "❌ $Message" -ForegroundColor Red
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Test-Prerequisites {
|
|
67
|
+
Write-InfoLog "Checking prerequisites..."
|
|
68
|
+
|
|
69
|
+
# Check Azure CLI
|
|
70
|
+
$azCliInstalled = Get-Command az -ErrorAction SilentlyContinue
|
|
71
|
+
if (-not $azCliInstalled) {
|
|
72
|
+
Write-ErrorLog "Azure CLI not found. Install from: https://aka.ms/azure-cli"
|
|
73
|
+
exit 1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Check login
|
|
77
|
+
try {
|
|
78
|
+
$null = az account show 2>$null
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
Write-ErrorLog "Not logged in to Azure. Run: az login"
|
|
82
|
+
exit 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Set subscription if provided
|
|
86
|
+
if ($SubscriptionId) {
|
|
87
|
+
Write-InfoLog "Setting subscription to: $SubscriptionId"
|
|
88
|
+
az account set --subscription $SubscriptionId
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Write-SuccessLog "Prerequisites checked"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function New-ResourceGroupIfNotExists {
|
|
95
|
+
Write-InfoLog "Creating resource group: $ResourceGroup"
|
|
96
|
+
|
|
97
|
+
$exists = az group exists -n $ResourceGroup
|
|
98
|
+
if ($exists -eq "true") {
|
|
99
|
+
Write-WarningLog "Resource group already exists"
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
az group create `
|
|
103
|
+
--name $ResourceGroup `
|
|
104
|
+
--location $Location `
|
|
105
|
+
--tags `
|
|
106
|
+
environment=$Environment `
|
|
107
|
+
application=$AppName `
|
|
108
|
+
managedBy=bicep `
|
|
109
|
+
framework=morph-spec
|
|
110
|
+
|
|
111
|
+
Write-SuccessLog "Resource group created"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function New-SqlPassword {
|
|
116
|
+
# Generate secure random password
|
|
117
|
+
$bytes = New-Object byte[] 32
|
|
118
|
+
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($bytes)
|
|
119
|
+
$password = [Convert]::ToBase64String($bytes) -replace '[=+/]', '' | Select-Object -First 25
|
|
120
|
+
|
|
121
|
+
Write-SuccessLog "SQL password generated (stored in Key Vault after deploy)"
|
|
122
|
+
return $password
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function Deploy-Infrastructure {
|
|
126
|
+
param([string]$SqlPassword)
|
|
127
|
+
|
|
128
|
+
Write-InfoLog "Deploying infrastructure..."
|
|
129
|
+
Write-InfoLog " App Name: $AppName"
|
|
130
|
+
Write-InfoLog " Environment: $Environment"
|
|
131
|
+
Write-InfoLog " Hosting Type: $HostingType"
|
|
132
|
+
Write-InfoLog " Location: $Location"
|
|
133
|
+
|
|
134
|
+
# Prepare parameters file
|
|
135
|
+
$paramsFile = "parameters.$Environment.json"
|
|
136
|
+
|
|
137
|
+
if (-not (Test-Path $paramsFile)) {
|
|
138
|
+
Write-ErrorLog "Parameters file not found: $paramsFile"
|
|
139
|
+
exit 1
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Deploy
|
|
143
|
+
az deployment group create `
|
|
144
|
+
--resource-group $ResourceGroup `
|
|
145
|
+
--name $DeploymentName `
|
|
146
|
+
--template-file main.bicep `
|
|
147
|
+
--parameters "@$paramsFile" `
|
|
148
|
+
--parameters `
|
|
149
|
+
appName=$AppName `
|
|
150
|
+
environment=$Environment `
|
|
151
|
+
location=$Location `
|
|
152
|
+
hostingType=$HostingType `
|
|
153
|
+
appServiceSku=$AppServiceSku `
|
|
154
|
+
containerImage=$ContainerImage `
|
|
155
|
+
sqlAdminPassword=$SqlPassword `
|
|
156
|
+
--output table
|
|
157
|
+
|
|
158
|
+
Write-SuccessLog "Infrastructure deployed"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function Show-Outputs {
|
|
162
|
+
Write-InfoLog "Retrieving deployment outputs..."
|
|
163
|
+
|
|
164
|
+
$appUrl = az deployment group show `
|
|
165
|
+
-g $ResourceGroup `
|
|
166
|
+
-n $DeploymentName `
|
|
167
|
+
--query properties.outputs.appUrl.value -o tsv
|
|
168
|
+
|
|
169
|
+
$sqlConnection = az deployment group show `
|
|
170
|
+
-g $ResourceGroup `
|
|
171
|
+
-n $DeploymentName `
|
|
172
|
+
--query properties.outputs.sqlConnectionString.value -o tsv
|
|
173
|
+
|
|
174
|
+
$appInsightsConnection = az deployment group show `
|
|
175
|
+
-g $ResourceGroup `
|
|
176
|
+
-n $DeploymentName `
|
|
177
|
+
--query properties.outputs.appInsightsConnectionString.value -o tsv
|
|
178
|
+
|
|
179
|
+
Write-Host ""
|
|
180
|
+
Write-Host "╔════════════════════════════════════════════════════════════════╗" -ForegroundColor Green
|
|
181
|
+
Write-Host "║ DEPLOYMENT SUCCESSFUL ║" -ForegroundColor Green
|
|
182
|
+
Write-Host "╚════════════════════════════════════════════════════════════════╝" -ForegroundColor Green
|
|
183
|
+
Write-Host ""
|
|
184
|
+
Write-Host "🌐 Application URL:" -ForegroundColor Cyan
|
|
185
|
+
Write-Host " $appUrl"
|
|
186
|
+
Write-Host ""
|
|
187
|
+
Write-Host "🗄️ SQL Connection String:" -ForegroundColor Cyan
|
|
188
|
+
Write-Host " $sqlConnection"
|
|
189
|
+
Write-Host ""
|
|
190
|
+
Write-Host "📊 App Insights Connection String:" -ForegroundColor Cyan
|
|
191
|
+
Write-Host " $appInsightsConnection"
|
|
192
|
+
Write-Host ""
|
|
193
|
+
Write-Host "💡 Next steps:" -ForegroundColor Yellow
|
|
194
|
+
|
|
195
|
+
if ($HostingType -eq "appservice") {
|
|
196
|
+
Write-Host " 1. Deploy your code: az webapp up --name app-$AppName-$Environment"
|
|
197
|
+
Write-Host " 2. View logs: az webapp log tail --name app-$AppName-$Environment -g $ResourceGroup"
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
Write-Host " 1. Build and push container: az acr build --registry <ACR> --image ${AppName}:latest ."
|
|
201
|
+
Write-Host " 2. Update container app: az containerapp update -n ca-$AppName-$Environment -g $ResourceGroup --image <IMAGE>"
|
|
202
|
+
Write-Host " 3. View logs: az containerapp logs show -n ca-$AppName-$Environment -g $ResourceGroup --follow"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
Write-Host ""
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# ==============================================================================
|
|
209
|
+
# MAIN
|
|
210
|
+
# ==============================================================================
|
|
211
|
+
|
|
212
|
+
function Main {
|
|
213
|
+
Write-Host ""
|
|
214
|
+
Write-Host "╔════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
|
|
215
|
+
Write-Host "║ MORPH-SPEC - Azure Infrastructure Deploy ║" -ForegroundColor Cyan
|
|
216
|
+
Write-Host "╚════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
|
|
217
|
+
Write-Host ""
|
|
218
|
+
|
|
219
|
+
Test-Prerequisites
|
|
220
|
+
New-ResourceGroupIfNotExists
|
|
221
|
+
$sqlPassword = New-SqlPassword
|
|
222
|
+
Deploy-Infrastructure -SqlPassword $sqlPassword
|
|
223
|
+
Show-Outputs
|
|
224
|
+
|
|
225
|
+
Write-SuccessLog "Deployment complete! 🚀"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Run main function
|
|
229
|
+
Main
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ==============================================================================
|
|
3
|
+
# MORPH-SPEC - Deploy Script
|
|
4
|
+
# Automated deployment of Azure infrastructure
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
7
|
+
set -e # Exit on error
|
|
8
|
+
|
|
9
|
+
# ==============================================================================
|
|
10
|
+
# CONFIGURATION
|
|
11
|
+
# ==============================================================================
|
|
12
|
+
|
|
13
|
+
# Required variables (override via environment)
|
|
14
|
+
APP_NAME="${APP_NAME:-myapp}"
|
|
15
|
+
ENVIRONMENT="${ENVIRONMENT:-dev}"
|
|
16
|
+
LOCATION="${LOCATION:-eastus}"
|
|
17
|
+
SUBSCRIPTION_ID="${SUBSCRIPTION_ID:-}"
|
|
18
|
+
|
|
19
|
+
# Optional variables
|
|
20
|
+
HOSTING_TYPE="${HOSTING_TYPE:-appservice}" # appservice or containerapp
|
|
21
|
+
APP_SERVICE_SKU="${APP_SERVICE_SKU:-F1}"
|
|
22
|
+
CONTAINER_IMAGE="${CONTAINER_IMAGE:-mcr.microsoft.com/hello-world:latest}"
|
|
23
|
+
|
|
24
|
+
# Derived variables
|
|
25
|
+
RESOURCE_GROUP="rg-${APP_NAME}-${ENVIRONMENT}"
|
|
26
|
+
DEPLOYMENT_NAME="deploy-${APP_NAME}-$(date +%Y%m%d-%H%M%S)"
|
|
27
|
+
|
|
28
|
+
# ==============================================================================
|
|
29
|
+
# COLORS
|
|
30
|
+
# ==============================================================================
|
|
31
|
+
|
|
32
|
+
RED='\033[0;31m'
|
|
33
|
+
GREEN='\033[0;32m'
|
|
34
|
+
YELLOW='\033[1;33m'
|
|
35
|
+
BLUE='\033[0;34m'
|
|
36
|
+
NC='\033[0m' # No Color
|
|
37
|
+
|
|
38
|
+
# ==============================================================================
|
|
39
|
+
# FUNCTIONS
|
|
40
|
+
# ==============================================================================
|
|
41
|
+
|
|
42
|
+
log_info() {
|
|
43
|
+
echo -e "${BLUE}ℹ️ $1${NC}"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
log_success() {
|
|
47
|
+
echo -e "${GREEN}✅ $1${NC}"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
log_warning() {
|
|
51
|
+
echo -e "${YELLOW}⚠️ $1${NC}"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
log_error() {
|
|
55
|
+
echo -e "${RED}❌ $1${NC}"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
check_prerequisites() {
|
|
59
|
+
log_info "Checking prerequisites..."
|
|
60
|
+
|
|
61
|
+
# Check Azure CLI
|
|
62
|
+
if ! command -v az &> /dev/null; then
|
|
63
|
+
log_error "Azure CLI not found. Install from: https://aka.ms/azure-cli"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Check login
|
|
68
|
+
if ! az account show &> /dev/null; then
|
|
69
|
+
log_error "Not logged in to Azure. Run: az login"
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Set subscription if provided
|
|
74
|
+
if [ -n "$SUBSCRIPTION_ID" ]; then
|
|
75
|
+
log_info "Setting subscription to: $SUBSCRIPTION_ID"
|
|
76
|
+
az account set --subscription "$SUBSCRIPTION_ID"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
log_success "Prerequisites checked"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
create_resource_group() {
|
|
83
|
+
log_info "Creating resource group: $RESOURCE_GROUP"
|
|
84
|
+
|
|
85
|
+
if az group exists -n "$RESOURCE_GROUP" | grep -q true; then
|
|
86
|
+
log_warning "Resource group already exists"
|
|
87
|
+
else
|
|
88
|
+
az group create \
|
|
89
|
+
--name "$RESOURCE_GROUP" \
|
|
90
|
+
--location "$LOCATION" \
|
|
91
|
+
--tags \
|
|
92
|
+
environment="$ENVIRONMENT" \
|
|
93
|
+
application="$APP_NAME" \
|
|
94
|
+
managedBy="bicep" \
|
|
95
|
+
framework="morph-spec"
|
|
96
|
+
|
|
97
|
+
log_success "Resource group created"
|
|
98
|
+
fi
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
generate_sql_password() {
|
|
102
|
+
# Generate secure random password
|
|
103
|
+
SQL_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-25)
|
|
104
|
+
log_success "SQL password generated (stored in Key Vault after deploy)"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
deploy_infrastructure() {
|
|
108
|
+
log_info "Deploying infrastructure..."
|
|
109
|
+
log_info " App Name: $APP_NAME"
|
|
110
|
+
log_info " Environment: $ENVIRONMENT"
|
|
111
|
+
log_info " Hosting Type: $HOSTING_TYPE"
|
|
112
|
+
log_info " Location: $LOCATION"
|
|
113
|
+
|
|
114
|
+
# Prepare parameters file
|
|
115
|
+
PARAMS_FILE="parameters.${ENVIRONMENT}.json"
|
|
116
|
+
|
|
117
|
+
if [ ! -f "$PARAMS_FILE" ]; then
|
|
118
|
+
log_error "Parameters file not found: $PARAMS_FILE"
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# Deploy
|
|
123
|
+
az deployment group create \
|
|
124
|
+
--resource-group "$RESOURCE_GROUP" \
|
|
125
|
+
--name "$DEPLOYMENT_NAME" \
|
|
126
|
+
--template-file main.bicep \
|
|
127
|
+
--parameters "@$PARAMS_FILE" \
|
|
128
|
+
--parameters \
|
|
129
|
+
appName="$APP_NAME" \
|
|
130
|
+
environment="$ENVIRONMENT" \
|
|
131
|
+
location="$LOCATION" \
|
|
132
|
+
hostingType="$HOSTING_TYPE" \
|
|
133
|
+
appServiceSku="$APP_SERVICE_SKU" \
|
|
134
|
+
containerImage="$CONTAINER_IMAGE" \
|
|
135
|
+
sqlAdminPassword="$SQL_PASSWORD" \
|
|
136
|
+
--output table
|
|
137
|
+
|
|
138
|
+
log_success "Infrastructure deployed"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
show_outputs() {
|
|
142
|
+
log_info "Retrieving deployment outputs..."
|
|
143
|
+
|
|
144
|
+
APP_URL=$(az deployment group show \
|
|
145
|
+
-g "$RESOURCE_GROUP" \
|
|
146
|
+
-n "$DEPLOYMENT_NAME" \
|
|
147
|
+
--query properties.outputs.appUrl.value -o tsv)
|
|
148
|
+
|
|
149
|
+
SQL_CONNECTION=$(az deployment group show \
|
|
150
|
+
-g "$RESOURCE_GROUP" \
|
|
151
|
+
-n "$DEPLOYMENT_NAME" \
|
|
152
|
+
--query properties.outputs.sqlConnectionString.value -o tsv)
|
|
153
|
+
|
|
154
|
+
APPINSIGHTS_CONNECTION=$(az deployment group show \
|
|
155
|
+
-g "$RESOURCE_GROUP" \
|
|
156
|
+
-n "$DEPLOYMENT_NAME" \
|
|
157
|
+
--query properties.outputs.appInsightsConnectionString.value -o tsv)
|
|
158
|
+
|
|
159
|
+
echo ""
|
|
160
|
+
echo "╔════════════════════════════════════════════════════════════════╗"
|
|
161
|
+
echo "║ DEPLOYMENT SUCCESSFUL ║"
|
|
162
|
+
echo "╚════════════════════════════════════════════════════════════════╝"
|
|
163
|
+
echo ""
|
|
164
|
+
echo "🌐 Application URL:"
|
|
165
|
+
echo " $APP_URL"
|
|
166
|
+
echo ""
|
|
167
|
+
echo "🗄️ SQL Connection String:"
|
|
168
|
+
echo " $SQL_CONNECTION"
|
|
169
|
+
echo ""
|
|
170
|
+
echo "📊 App Insights Connection String:"
|
|
171
|
+
echo " $APPINSIGHTS_CONNECTION"
|
|
172
|
+
echo ""
|
|
173
|
+
echo "💡 Next steps:"
|
|
174
|
+
|
|
175
|
+
if [ "$HOSTING_TYPE" = "appservice" ]; then
|
|
176
|
+
echo " 1. Deploy your code: az webapp up --name app-${APP_NAME}-${ENVIRONMENT}"
|
|
177
|
+
echo " 2. View logs: az webapp log tail --name app-${APP_NAME}-${ENVIRONMENT} -g $RESOURCE_GROUP"
|
|
178
|
+
else
|
|
179
|
+
echo " 1. Build and push container: az acr build --registry <ACR> --image ${APP_NAME}:latest ."
|
|
180
|
+
echo " 2. Update container app: az containerapp update -n ca-${APP_NAME}-${ENVIRONMENT} -g $RESOURCE_GROUP --image <IMAGE>"
|
|
181
|
+
echo " 3. View logs: az containerapp logs show -n ca-${APP_NAME}-${ENVIRONMENT} -g $RESOURCE_GROUP --follow"
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
echo ""
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# ==============================================================================
|
|
188
|
+
# MAIN
|
|
189
|
+
# ==============================================================================
|
|
190
|
+
|
|
191
|
+
main() {
|
|
192
|
+
echo ""
|
|
193
|
+
echo "╔════════════════════════════════════════════════════════════════╗"
|
|
194
|
+
echo "║ MORPH-SPEC - Azure Infrastructure Deploy ║"
|
|
195
|
+
echo "╚════════════════════════════════════════════════════════════════╝"
|
|
196
|
+
echo ""
|
|
197
|
+
|
|
198
|
+
check_prerequisites
|
|
199
|
+
create_resource_group
|
|
200
|
+
generate_sql_password
|
|
201
|
+
deploy_infrastructure
|
|
202
|
+
show_outputs
|
|
203
|
+
|
|
204
|
+
log_success "Deployment complete! 🚀"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Run main function
|
|
208
|
+
main
|
|
@@ -25,9 +25,17 @@ param appName string
|
|
|
25
25
|
@secure()
|
|
26
26
|
param sqlAdminPassword string
|
|
27
27
|
|
|
28
|
-
@description('Container image to deploy')
|
|
28
|
+
@description('Container image to deploy (only for Container Apps)')
|
|
29
29
|
param containerImage string = 'mcr.microsoft.com/hello-world:latest'
|
|
30
30
|
|
|
31
|
+
@description('Hosting type: appservice or containerapp')
|
|
32
|
+
@allowed(['appservice', 'containerapp'])
|
|
33
|
+
param hostingType string = 'appservice'
|
|
34
|
+
|
|
35
|
+
@description('App Service SKU (only for App Service hosting)')
|
|
36
|
+
@allowed(['F1', 'B1', 'S1', 'P1v2'])
|
|
37
|
+
param appServiceSku string = 'F1'
|
|
38
|
+
|
|
31
39
|
// ==============================================================================
|
|
32
40
|
// VARIABLES
|
|
33
41
|
// ==============================================================================
|
|
@@ -106,8 +114,28 @@ module sqlDatabase 'sql-database.bicep' = {
|
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
|
|
117
|
+
// ==============================================================================
|
|
118
|
+
// HOSTING - App Service (Conditional)
|
|
119
|
+
// ==============================================================================
|
|
120
|
+
|
|
121
|
+
module appService 'app-service.bicep' = if (hostingType == 'appservice') {
|
|
122
|
+
name: 'appService-${uniqueString(resourceGroup().id)}'
|
|
123
|
+
params: {
|
|
124
|
+
name: 'app-${resourcePrefix}'
|
|
125
|
+
location: location
|
|
126
|
+
tags: tags
|
|
127
|
+
sku: appServiceSku
|
|
128
|
+
appInsightsConnectionString: appInsights.outputs.connectionString
|
|
129
|
+
alwaysOn: appServiceSku != 'F1' // Only available on paid tiers
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ==============================================================================
|
|
134
|
+
// HOSTING - Container Apps (Conditional)
|
|
135
|
+
// ==============================================================================
|
|
136
|
+
|
|
109
137
|
// Container Apps Environment
|
|
110
|
-
module containerAppEnv 'container-app-env.bicep' = {
|
|
138
|
+
module containerAppEnv 'container-app-env.bicep' = if (hostingType == 'containerapp') {
|
|
111
139
|
name: 'containerAppEnv-${uniqueString(resourceGroup().id)}'
|
|
112
140
|
params: {
|
|
113
141
|
name: '${resourcePrefix}-env'
|
|
@@ -118,13 +146,13 @@ module containerAppEnv 'container-app-env.bicep' = {
|
|
|
118
146
|
}
|
|
119
147
|
|
|
120
148
|
// Container App
|
|
121
|
-
module containerApp 'container-app.bicep' = {
|
|
149
|
+
module containerApp 'container-app.bicep' = if (hostingType == 'containerapp') {
|
|
122
150
|
name: 'containerApp-${uniqueString(resourceGroup().id)}'
|
|
123
151
|
params: {
|
|
124
|
-
name: resourcePrefix
|
|
152
|
+
name: 'ca-${resourcePrefix}'
|
|
125
153
|
location: location
|
|
126
154
|
tags: tags
|
|
127
|
-
environmentId: containerAppEnv.outputs.id
|
|
155
|
+
environmentId: hostingType == 'containerapp' ? containerAppEnv.outputs.id : ''
|
|
128
156
|
containerImage: containerImage
|
|
129
157
|
appInsightsConnectionString: appInsights.outputs.connectionString
|
|
130
158
|
minReplicas: environment == 'prod' ? 1 : 0
|
|
@@ -136,8 +164,11 @@ module containerApp 'container-app.bicep' = {
|
|
|
136
164
|
// OUTPUTS
|
|
137
165
|
// ==============================================================================
|
|
138
166
|
|
|
139
|
-
@description('
|
|
140
|
-
output
|
|
167
|
+
@description('Application URL')
|
|
168
|
+
output appUrl string = hostingType == 'appservice' ? appService.outputs.url : containerApp.outputs.url
|
|
169
|
+
|
|
170
|
+
@description('Hosting Type')
|
|
171
|
+
output hostingType string = hostingType
|
|
141
172
|
|
|
142
173
|
@description('SQL Server connection string')
|
|
143
174
|
output sqlConnectionString string = sqlDatabase.outputs.connectionString
|
|
@@ -153,3 +184,6 @@ output appInsightsConnectionString string = appInsights.outputs.connectionString
|
|
|
153
184
|
|
|
154
185
|
@description('Log Analytics Workspace ID')
|
|
155
186
|
output logAnalyticsWorkspaceId string = logAnalytics.id
|
|
187
|
+
|
|
188
|
+
@description('App Service Principal ID (for Managed Identity)')
|
|
189
|
+
output appPrincipalId string = hostingType == 'appservice' ? appService.outputs.principalId : ''
|