Skip to content
Open
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: 8 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# CODEOWNERS for CodePals.io
# Only @rmjoia is authorized for core governance & policy areas at this stage.
# @rmjoia must approve all PRs unless they are the creator

# Default owner for everything in the repo
* @rmjoia

# Governance & Constitution
.specify/memory/constitution.md @rmjoia
Expand All @@ -16,3 +19,7 @@ CODE_OF_CONDUCT.md @rmjoia

# Speckit / Spec-Kit related templates
.specify/templates/* @rmjoia

# Infrastructure and Deployment
/infra/ @rmjoia
/.github/workflows/ @rmjoia
74 changes: 74 additions & 0 deletions infra/CodePals.Infra.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
@{
# Script module or binary module file associated with this manifest.
RootModule = 'CodePals.Infra.psm1'

# Version number of this module.
ModuleVersion = '1.0.0'

# ID used to uniquely identify this module
GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'

# Author of this module
Author = 'CodePals Team'

# Company or vendor of this module
CompanyName = 'CodePals.io'

# Copyright statement for this module
Copyright = '(c) 2025 CodePals.io. All rights reserved.'

# Description of the functionality provided by this module
Description = 'Infrastructure management module for CodePals.io Azure resources. Provides functions for provisioning and configuring Static Web Apps, Key Vaults, Cosmos DB, DNS, and GitHub OAuth.'

# Minimum version of the PowerShell engine required by this module
PowerShellVersion = '7.0'

# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @(
@{ ModuleName = 'Az.Resources'; ModuleVersion = '6.0.0' }
@{ ModuleName = 'Az.KeyVault'; ModuleVersion = '4.0.0' }
@{ ModuleName = 'Az.ManagedServiceIdentity'; ModuleVersion = '1.0.0' }
@{ ModuleName = 'Az.Websites'; ModuleVersion = '3.0.0' }
@{ ModuleName = 'Az.Dns'; ModuleVersion = '1.0.0' }
@{ ModuleName = 'Az.CosmosDB'; ModuleVersion = '1.0.0' }
)

# Functions to export from this module
FunctionsToExport = @(
'Initialize-Infra'
'Initialize-DNS'
'Initialize-GitHubOAuth'
)

# Cmdlets to export from this module
CmdletsToExport = @()

# Variables to export from this module
VariablesToExport = @()

# Aliases to export from this module
AliasesToExport = @()

# Private data to pass to the module specified in RootModule/ModuleToProcess
PrivateData = @{
PSData = @{
# Tags applied to this module
Tags = @('Azure', 'Infrastructure', 'DevOps', 'IaC', 'CodePals')

# A URL to the license for this module
LicenseUri = 'https://github.com/rmjoia/codepalsio/blob/main/LICENSE'

# A URL to the main website for this project
ProjectUri = 'https://github.com/rmjoia/codepalsio'

# ReleaseNotes of this module
ReleaseNotes = @'
# Version 1.0.0
- Initial release
- Initialize-Infra: Provision complete Azure infrastructure
- Initialize-DNS: Configure DNS records for custom domains
- Initialize-GitHubOAuth: Set up GitHub OAuth applications
'@
}
}
}
34 changes: 34 additions & 0 deletions infra/CodePals.Infra.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<#
.SYNOPSIS
CodePals Infrastructure Management Module

.DESCRIPTION
Main module for managing CodePals infrastructure on Azure.
Imports all infrastructure functions and exposes them for use.
#>

# Get the directory where this module is located
$ModuleRoot = $PSScriptRoot

# Import all function scripts
$FunctionScripts = @(
'Initialize-Infra.ps1'
'Initialize-DNS.ps1'
'Initialize-GitHubOAuth.ps1'
)

foreach ($script in $FunctionScripts) {
$scriptPath = Join-Path $ModuleRoot $script
if (Test-Path $scriptPath) {
. $scriptPath
} else {
Write-Warning "Function script not found: $scriptPath"
}
}

# Export all public functions
Export-ModuleMember -Function @(
'Initialize-Infra'
'Initialize-DNS'
'Initialize-GitHubOAuth'
)
201 changes: 201 additions & 0 deletions infra/Initialize-CodePals.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<#
.SYNOPSIS
Initialize CodePals landing page infrastructure.

.DESCRIPTION
Provisions Static Web App, Key Vault, Managed Identity for CodePals landing page deployment.

.EXAMPLE
. ./Initialize-CodePals.ps1
Initialize-Infra -Environment dev

.EXAMPLE
Initialize-Infra -Environment prod -SubscriptionId "6561ed3a-120c-4692-880a-c1994e86999f"
#>

function Initialize-Infra {
<#
.SYNOPSIS
Initialize CodePals infrastructure

.PARAMETER Environment
'dev' or 'prod'

.PARAMETER SubscriptionId
Azure subscription ID (GUID). If not provided, uses current logged-in subscription.

.PARAMETER Location
Azure region. Default: northeurope
#>

param(
[Parameter(Mandatory = $true)]
[ValidateSet('dev', 'prod')]
[string]$Environment,

[Parameter(Mandatory = $false)]
[string]$SubscriptionId,

[Parameter(Mandatory = $false)]
[ValidateSet('westus2', 'centralus', 'eastus2', 'westeurope', 'eastasia')]
[string]$Location = 'westeurope'
)

$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'

# Load required modules
Write-Host "→ Loading required Azure modules..."
$requiredModules = @('Az.Resources', 'Az.KeyVault', 'Az.ManagedServiceIdentity', 'Az.Websites', 'Az.Dns')
foreach ($module in $requiredModules) {
if (-not (Get-Module -Name $module -ListAvailable)) {
Write-Host " Installing $module..." -ForegroundColor Yellow
Install-Module -Name $module -Force -AllowClobber -Scope CurrentUser
}
Import-Module $module -ErrorAction Stop
}

# Configuration
$Project = 'codepals'
$ResourceGroupName = "$Project-$Environment-rg"
$StaticWebAppName = "$Project-$Environment"
$KeyVaultName = "$Project-$Environment-kv"
$ManagedIdentityName = "$Project-$Environment-mi"

Write-Host "🚀 Initializing CodePals infrastructure" -ForegroundColor Cyan
Write-Host "Environment: $Environment | Location: $Location" -ForegroundColor Green

# 1. Set subscription
Write-Host "`n→ Setting subscription..."
if ($SubscriptionId) {
Set-AzContext -SubscriptionId $SubscriptionId | Out-Null
Write-Host " Using subscription: $SubscriptionId"
} else {
$context = Get-AzContext
if (-not $context) {
Write-Host " No Azure context. Run: Connect-AzAccount" -ForegroundColor Red
return
}
$SubscriptionId = $context.Subscription.Id
Write-Host " Using current subscription: $SubscriptionId"
}

# 2. Create resource group
Write-Host "→ Creating resource group: $ResourceGroupName"
New-AzResourceGroup -Name $ResourceGroupName -Location $Location -Tag @{ project=$Project; environment=$Environment } -Force | Out-Null

# 3. Deploy Bicep template
Write-Host "→ Deploying infrastructure via Bicep..."

# Resolve bicep file path - works when imported as module or called directly
$BicepFile = if ($MyInvocation.MyCommand.Path) {
Join-Path (Split-Path $MyInvocation.MyCommand.Path) "main.bicep"
} else {
Join-Path (Get-Location) "main.bicep"
}

$BicepFile = Resolve-Path $BicepFile -ErrorAction Stop

# Get current user's object ID for Key Vault access policy
$currentUser = Get-AzADUser -UserPrincipalName (Get-AzContext).Account.Id -ErrorAction SilentlyContinue
$currentUserObjectId = if ($currentUser) { $currentUser.Id } else { '' }

$Deployment = New-AzResourceGroupDeployment `
-ResourceGroupName $ResourceGroupName `
-TemplateFile $BicepFile `
-location $Location `
-environment $Environment `
-currentUserObjectId $currentUserObjectId

$SWAUrl = $Deployment.Outputs['staticWebAppDefaultHostname'].Value
$MIClientId = $Deployment.Outputs['managedIdentityClientId'].Value
$MIPrincipalId = $Deployment.Outputs['managedIdentityPrincipalId'].Value
$Domain = $Deployment.Outputs['domain'].Value

# Wait for Key Vault to be accessible
Write-Host "→ Waiting for Key Vault access..."
$maxRetries = 10
$retry = 0
$kvAccessible = $false

while ($retry -lt $maxRetries -and -not $kvAccessible) {
try {
Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "placeholder" -ErrorAction SilentlyContinue | Out-Null
$kvAccessible = $true
}
catch {
$retry++
if ($retry -lt $maxRetries) {
Start-Sleep -Seconds 2
}
}
}

# 4. Store managed identity credentials in Key Vault
Write-Host "→ Storing managed identity credentials in Key Vault"
$kvSecret = ConvertTo-SecureString -String $MIClientId -AsPlainText -Force

Check failure on line 136 in infra/Initialize-CodePals.ps1

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

infra/Initialize-CodePals.ps1#L136

File 'Initialize-CodePals.ps1' uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead.
Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "MANAGED-IDENTITY-CLIENT-ID" -SecretValue $kvSecret -Verbose:$false | Out-Null

# 5. Get Static Web App and deployment token
Write-Host "→ Getting deployment token..."
$swa = Get-AzStaticWebApp -ResourceGroupName $ResourceGroupName -Name $StaticWebAppName -ErrorAction Stop

# Get API token using list secrets
$secrets = Invoke-AzResourceAction -ResourceId $swa.Id -Action 'listSecrets' -ApiVersion '2023-01-01' -Force
$Token = $secrets.properties.apiKey

# Store token in Key Vault
if ($Token) {
$tokenSecret = ConvertTo-SecureString -String $Token -AsPlainText -Force

Check failure on line 149 in infra/Initialize-CodePals.ps1

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

infra/Initialize-CodePals.ps1#L149

File 'Initialize-CodePals.ps1' uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead.
Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AZURE-STATIC-WEB-APPS-TOKEN" -SecretValue $tokenSecret -Verbose:$false | Out-Null
}

# 6. Create federated identity credential for GitHub Actions
Write-Host "→ Creating federated identity for GitHub Actions"
$GitHubRepo = "rmjoia/codepalsio"
$FederatedCredentialName = "github-actions-$Environment"

$federatedCredentialParams = @{
ResourceGroupName = $ResourceGroupName
UserAssignedIdentityName = $ManagedIdentityName
Name = $FederatedCredentialName
Issuer = "https://token.actions.githubusercontent.com"
Subject = "repo:$($GitHubRepo):ref:refs/heads/main"
}
New-AzFederatedIdentityCredential @federatedCredentialParams -Verbose:$false | Out-Null

# 7. Configure DNS records
Write-Host "`n→ Configuring DNS records..."
$dnsScript = Join-Path (Split-Path $BicepFile) "Initialize-DNS.ps1"
. $dnsScript
Initialize-DNS -Environment $Environment -StaticWebAppDomain $SWAUrl -SubscriptionId $SubscriptionId -ErrorAction SilentlyContinue

Write-Host "`n✅ Infrastructure initialized!" -ForegroundColor Green

Write-Host "`n📋 Resources created:"
Write-Host " Resource Group: $ResourceGroupName"
Write-Host " Static Web App: $StaticWebAppName"
Write-Host " Default URL: https://$SWAUrl"
Write-Host " Custom Domain: https://$Domain (CNAME configured)"
Write-Host " Managed Identity: $ManagedIdentityName"
Write-Host " Key Vault: $KeyVaultName"

Write-Host "`n🔑 Secrets stored in Key Vault ($KeyVaultName):"
Write-Host " - MANAGED-IDENTITY-CLIENT-ID"
Write-Host " - AZURE-STATIC-WEB-APPS-TOKEN"

Write-Host "`n🔐 GitHub Actions Authentication:"
Write-Host " - Federated Identity: github-actions-$Environment"
Write-Host " - No secrets stored (uses OIDC)"
Write-Host " - Workflows use: @azure/login with federated token"

Write-Host "`n🌐 DNS Configuration:"
Write-Host " - Domain: $Domain"
Write-Host " - CNAME: $Domain → $SWAUrl"

Write-Host "`n📝 GitHub Secrets Setup:"
Write-Host " Name: AZURE_STATIC_WEB_APPS_TOKEN"
Write-Host " Value: (retrieve from Key Vault)"

Write-Host "`n✨ Next: Add GitHub secrets, then create CI/CD workflows"
}
Loading