#Requires -RunAsAdministrator #Requires -Version 5.1 <# .SYNOPSIS Bootstraps the CI host machine from scratch (run ONCE on the host). .DESCRIPTION Creates the F:\CI\ directory structure, installs prerequisites, registers Windows Credential Manager entries, and optionally installs + registers act_runner as a Windows service (via NSSM). Run this script elevated (as Administrator) on the Windows host before provisioning the template VM or running any CI jobs. What this script does: 1. Create F:\CI\ directory tree 2. Verify VMware Workstation / vmrun.exe is present 3. Install PowerShell CredentialManager module (if missing) 4. Store guest VM credentials in Windows Credential Manager 5. Install act_runner as a Windows service via NSSM (optional) 6. Configure SSH alias for Gitea (optional) 7. Print a post-setup checklist .PARAMETER CIRoot Base directory for all CI data. Default: F:\CI .PARAMETER GuestCredentialTarget Windows Credential Manager target name for guest VM credentials. Default: BuildVMGuest .PARAMETER GuestUsername Build user account inside the VM. Default: ci_build .PARAMETER GuestPassword Password for the guest build account. CHANGE THIS before production use. Default: CIBuild!ChangeMe2026 .PARAMETER ActRunnerExe Path to the act_runner binary. If the file does not exist the script warns and skips the service installation step. Default: \act_runner\act_runner.exe .PARAMETER ActRunnerConfigYaml Path to act_runner config.yaml to copy into \act_runner\. If not specified, copies runner\config.yaml from the repo (if it exists next to this script). Default: \runner\config.yaml .PARAMETER GiteaUrl Gitea server URL used for runner registration. Default: http://10.10.20.11:3100 .PARAMETER GiteaRunnerToken Registration token for act_runner. If not supplied the script prints instructions and skips registration (you can register later manually). .PARAMETER SkipRunnerInstall Skip act_runner service installation entirely. .PARAMETER SkipSSHConfig Skip writing the SSH config alias for Gitea. .PARAMETER GiteaSSHHost Gitea SSH hostname/IP. Default: 10.10.20.11 .PARAMETER GiteaSSHPort Gitea SSH port. Default: 2222 .EXAMPLE # Minimal — creates dirs, stores credentials, skips runner install: .\Setup-Host.ps1 -SkipRunnerInstall # Full setup with runner registration: .\Setup-Host.ps1 -GiteaRunnerToken 'abc123token' # Custom CI root on a different drive: .\Setup-Host.ps1 -CIRoot 'D:\CI' -GiteaRunnerToken 'abc123token' #> [CmdletBinding(SupportsShouldProcess)] param( [string] $CIRoot = 'F:\CI', [string] $GuestCredentialTarget = 'BuildVMGuest', [string] $GuestUsername = 'ci_build', [string] $GuestPassword = '', [string] $ActRunnerExe = '', [string] $ActRunnerConfigYaml = '', [string] $GiteaUrl = 'http://10.10.20.11:3100', [string] $GiteaRunnerToken = '', [switch] $SkipRunnerInstall, [switch] $SkipSSHConfig, [string] $GiteaSSHHost = '10.10.20.11', [int] $GiteaSSHPort = 2222 ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent function Write-Step { param([string]$Message) Write-Host "`n=== $Message ===" -ForegroundColor Cyan } function Write-OK { param([string]$m) Write-Host " [OK] $m" -ForegroundColor Green } function Write-Warn { param([string]$m) Write-Host " [WARN] $m" -ForegroundColor Yellow } function Write-Fail { param([string]$m) Write-Host " [FAIL] $m" -ForegroundColor Red } # Resolve defaults that depend on $CIRoot / $scriptDir if (-not $ActRunnerExe) { $ActRunnerExe = Join-Path $CIRoot 'act_runner\act_runner.exe' } if (-not $ActRunnerConfigYaml) { $ActRunnerConfigYaml = Join-Path $scriptDir 'runner\config.yaml' } # ── Step 1: Create F:\CI\ directory tree ───────────────────────────────────── Write-Step "Creating CI directory tree under $CIRoot" $dirs = @( "$CIRoot\BuildVMs", "$CIRoot\Artifacts", "$CIRoot\Logs", "$CIRoot\Templates\WinBuild", "$CIRoot\act_runner\logs", "$CIRoot\Cache\NuGet", "$CIRoot\RunnerWork", "$CIRoot\ISO" ) foreach ($dir in $dirs) { if (Test-Path $dir) { Write-OK "$dir (already exists)" } else { New-Item -ItemType Directory -Path $dir -Force | Out-Null Write-OK "Created: $dir" } } # ── Step 2: Verify vmrun.exe ────────────────────────────────────────────────── Write-Step "Verifying VMware Workstation (vmrun.exe)" $vmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe' if (Test-Path $vmrunPath) { Write-OK "vmrun.exe found: $vmrunPath" } else { Write-Fail "vmrun.exe NOT found at expected path: $vmrunPath" Write-Host " Install VMware Workstation Pro, then re-run this script." -ForegroundColor Yellow } # ── Step 3: Install CredentialManager module ───────────────────────────────── Write-Step "Checking PowerShell CredentialManager module" $credMgr = Get-Module -ListAvailable -Name CredentialManager -ErrorAction SilentlyContinue if ($credMgr) { Write-OK "CredentialManager $($credMgr[0].Version) already installed" } else { Write-Host " Installing CredentialManager module (CurrentUser scope)..." Install-Module -Name CredentialManager -Scope CurrentUser -Force -AllowClobber Write-OK "CredentialManager installed" } Import-Module CredentialManager -ErrorAction SilentlyContinue # ── Step 4: Store guest VM credentials ─────────────────────────────────────── Write-Step "Storing guest VM credentials in Windows Credential Manager" $existingCred = Get-StoredCredential -Target $GuestCredentialTarget -ErrorAction SilentlyContinue if ($existingCred) { Write-OK "Credential '$GuestCredentialTarget' already exists (user: $($existingCred.UserName))" Write-Warn "To update: Remove-StoredCredential -Target '$GuestCredentialTarget', then re-run." } else { if ($GuestPassword -eq '') { Write-Host " No -GuestPassword supplied. Enter password for '$GuestUsername' (will be stored in Credential Manager):" $secPwd = Read-Host -Prompt " Password" -AsSecureString $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secPwd) $GuestPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } New-StoredCredential ` -Target $GuestCredentialTarget ` -UserName $GuestUsername ` -Password $GuestPassword ` -Persist LocalMachine | Out-Null Write-OK "Credential '$GuestCredentialTarget' stored (user: $GuestUsername)" } # ── Step 5: Copy runner\config.yaml ────────────────────────────────────────── Write-Step "Copying act_runner config.yaml to $CIRoot\act_runner\" $destConfig = Join-Path $CIRoot 'act_runner\config.yaml' if (Test-Path $ActRunnerConfigYaml) { if (Test-Path $destConfig) { Write-Warn "config.yaml already exists at $destConfig — skipping copy." Write-Warn "Delete it manually if you want to overwrite." } else { Copy-Item -Path $ActRunnerConfigYaml -Destination $destConfig -Force Write-OK "Copied $ActRunnerConfigYaml -> $destConfig" } } else { Write-Warn "Source config.yaml not found at: $ActRunnerConfigYaml" Write-Warn "Copy runner\config.yaml manually to $destConfig before starting the runner." } # ── Step 6: Install act_runner as Windows service (via NSSM) ───────────────── if ($SkipRunnerInstall) { Write-Host "`n=== act_runner service setup SKIPPED (-SkipRunnerInstall) ===" -ForegroundColor Yellow } else { Write-Step "Installing act_runner as Windows service" if (-not (Test-Path $ActRunnerExe)) { Write-Warn "act_runner binary not found at: $ActRunnerExe" Write-Warn "Download act_runner v1.0.2 from https://gitea.com/gitea/act_runner/releases" Write-Warn "Place it at $ActRunnerExe, then re-run this script (or install the service manually)." } else { # Check for NSSM $nssm = Get-Command nssm -ErrorAction SilentlyContinue if (-not $nssm) { Write-Warn "NSSM not found in PATH. Attempting install via Chocolatey..." $choco = Get-Command choco -ErrorAction SilentlyContinue if ($choco) { choco install nssm -y | Out-Null $env:PATH = [System.Environment]::GetEnvironmentVariable('PATH','Machine') + ';' + $env:PATH $nssm = Get-Command nssm -ErrorAction SilentlyContinue } } if (-not $nssm) { Write-Fail "NSSM not available. Install NSSM manually (https://nssm.cc) and re-run." } else { $serviceName = 'act_runner' $existingSvc = Get-Service -Name $serviceName -ErrorAction SilentlyContinue if ($existingSvc) { Write-OK "Service '$serviceName' already exists (Status: $($existingSvc.Status))" } else { $actRunnerDir = Split-Path $ActRunnerExe -Parent # Register act_runner if ($GiteaRunnerToken) { Write-Host " Registering act_runner with Gitea ($GiteaUrl)..." Push-Location $actRunnerDir & $ActRunnerExe register ` --no-interactive ` --instance $GiteaUrl ` --token $GiteaRunnerToken ` --name 'local-windows-runner' ` --labels 'windows-build:host,dotnet:host,msbuild:host' $regExit = $LASTEXITCODE Pop-Location if ($regExit -ne 0) { Write-Fail "act_runner registration failed (exit $regExit). Check token and Gitea URL." } else { Write-OK "act_runner registered with Gitea." } } else { Write-Warn "No -GiteaRunnerToken supplied — skipping registration." Write-Warn "Register manually: cd $actRunnerDir ; .\act_runner.exe register" } # Install service Write-Host " Installing service via NSSM..." & nssm install $serviceName $ActRunnerExe 2>&1 | Out-Null & nssm set $serviceName AppDirectory $actRunnerDir 2>&1 | Out-Null & nssm set $serviceName AppParameters "daemon --config `"$destConfig`"" 2>&1 | Out-Null & nssm set $serviceName DisplayName 'Gitea act_runner' 2>&1 | Out-Null & nssm set $serviceName Description 'Gitea Actions runner (local CI/CD)' 2>&1 | Out-Null & nssm set $serviceName Start SERVICE_AUTO_START 2>&1 | Out-Null & nssm set $serviceName AppStdout (Join-Path $actRunnerDir 'logs\stdout.log') 2>&1 | Out-Null & nssm set $serviceName AppStderr (Join-Path $actRunnerDir 'logs\stderr.log') 2>&1 | Out-Null & nssm set $serviceName AppRotateFiles 1 2>&1 | Out-Null & nssm set $serviceName AppRotateBytes 10485760 2>&1 | Out-Null # 10 MB Start-Service -Name $serviceName -ErrorAction SilentlyContinue $svc = Get-Service -Name $serviceName -ErrorAction SilentlyContinue if ($svc -and $svc.Status -eq 'Running') { Write-OK "Service '$serviceName' installed and running." } else { Write-Warn "Service installed but not running — check logs at $(Join-Path $actRunnerDir 'logs\')." } } } } } # ── Step 7: SSH config alias for Gitea ─────────────────────────────────────── if ($SkipSSHConfig) { Write-Host "`n=== SSH config SKIPPED (-SkipSSHConfig) ===" -ForegroundColor Yellow } else { Write-Step "Configuring SSH alias for Gitea (gitea-ci)" $sshDir = Join-Path $env:USERPROFILE '.ssh' $sshConfig = Join-Path $sshDir 'config' if (-not (Test-Path $sshDir)) { New-Item -ItemType Directory -Path $sshDir -Force | Out-Null } $aliasBlock = @" # Gitea CI alias — added by Setup-Host.ps1 Host gitea-ci HostName $GiteaSSHHost Port $GiteaSSHPort User git IdentityFile ~/.ssh/id_rsa "@ $existingConfig = '' if (Test-Path $sshConfig) { $existingConfig = Get-Content $sshConfig -Raw -ErrorAction SilentlyContinue } if ($existingConfig -match 'Host gitea-ci') { Write-OK "SSH alias 'gitea-ci' already present in $sshConfig" } else { Add-Content -Path $sshConfig -Value $aliasBlock -Encoding UTF8 Write-OK "SSH alias 'gitea-ci' written to $sshConfig" Write-Warn "Ensure your public SSH key is registered in Gitea under User Settings → SSH Keys." } } # ── Done ────────────────────────────────────────────────────────────────────── Write-Host "" Write-Host "============================================================" -ForegroundColor Green Write-Host " Host setup complete!" -ForegroundColor Green Write-Host "============================================================" -ForegroundColor Green Write-Host "" Write-Host " POST-SETUP CHECKLIST:" -ForegroundColor White Write-Host "" Write-Host " 1. Place act_runner binary (if not done):" Write-Host " $ActRunnerExe" Write-Host " Download: https://gitea.com/gitea/act_runner/releases/tag/v1.0.2" Write-Host "" Write-Host " 2. Place Windows Server 2025 ISO:" Write-Host " $CIRoot\ISO\" Write-Host "" Write-Host " 3. Provision the template VM:" Write-Host " a. Create VM in VMware Workstation (4 vCPU, 6 GB RAM, 80 GB thin)" Write-Host " VMX: $CIRoot\Templates\WinBuild2025\WinBuild2025.vmx" Write-Host " NIC: VMnet8 (NAT) — internet required during provisioning" Write-Host " b. Install Windows Server 2025 + enable WinRM inside VM" Write-Host " c. From this host run:" Write-Host " cd $scriptDir\template" Write-Host " .\Prepare-WinBuild2025.ps1 -VMIPAddress -SkipWindowsUpdate" Write-Host " d. Shut down VM, take snapshot named exactly: BaseClean" Write-Host "" Write-Host " 4. Register SSH key with Gitea:" Write-Host " Gitea UI → User Settings → SSH Keys → Add Key" Write-Host " (key: $env:USERPROFILE\.ssh\id_rsa.pub)" Write-Host "" Write-Host " 5. Verify act_runner is Online in Gitea:" Write-Host " $GiteaUrl/-/admin/runners" Write-Host ""