diff --git a/C4BImplementationJourney4LocalGovernment-Hein/demo/ArcGISPackageDemo.ps1 b/C4BImplementationJourney4LocalGovernment-Hein/demo/ArcGISPackageDemo.ps1 new file mode 100644 index 0000000..552653b --- /dev/null +++ b/C4BImplementationJourney4LocalGovernment-Hein/demo/ArcGISPackageDemo.ps1 @@ -0,0 +1,181 @@ +$ErrorActionPreference = 'Stop' + +# ------------------------------------------------------------------ +# Global Safety & TLS +# ------------------------------------------------------------------ +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# ------------------------------------------------------------------ +# Package Context +# ------------------------------------------------------------------ +$toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$packageName = $env:ChocolateyPackageName +$packageVersion = $env:ChocolateyPackageVersion + +$zipName = 'arcgistools3.6.zip' +$zipPath = Join-Path -Path $toolsDir -ChildPath $zipName +$expectedMsiName = 'ArcGISPro.msi' +$logPath = Join-Path -Path $env:TEMP -ChildPath "${packageName}.${packageVersion}.log" + +# ------------------------------------------------------------------ +# ProGet Asset Download Configuration +# ------------------------------------------------------------------ +$zipUrl = 'https://proget/assets/Choco-Applications/content/ArcGIS/ArcGIS%20Pro/arcgistools3.6.zip' +$zipChecksum = '0e114dd159d16f71bb06aa333b3ce898e756cfc12ddb417d21079749952bcb30' +$zipChecksumType = 'sha256' + +# API key must be supplied securely by the deployment environment +$proGetAssetApiKey = '#############################' + +# ------------------------------------------------------------------ +# Validate Required Environment Values +# ------------------------------------------------------------------ +if ([string]::IsNullOrWhiteSpace($packageName)) { + throw "Chocolatey package name environment variable is missing." +} + +if ([string]::IsNullOrWhiteSpace($packageVersion)) { + throw "Chocolatey package version environment variable is missing." +} + +if ([string]::IsNullOrWhiteSpace($proGetAssetApiKey)) { + throw "Missing ProGet asset API key. Set environment variable 'ProGetAssetApiKey' before installation." +} + +# ------------------------------------------------------------------ +# Clean Previous Package Artifacts +# ------------------------------------------------------------------ +Write-Host "Cleaning previous package artifacts from '${toolsDir}'..." + +if (Test-Path -Path $zipPath) { + Remove-Item -Path $zipPath -Force -ErrorAction Stop +} + +Get-ChildItem -Path $toolsDir -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $_.Extension -ieq '.msi' } | + ForEach-Object { + try { + Remove-Item -Path $_.FullName -Force -ErrorAction Stop + } + catch { + Write-Warning "Unable to remove stale file '${($_.FullName)}'. Continuing." + } + } + +# ------------------------------------------------------------------ +# Download ZIP from ProGet HTTP Endpoint using X-ApiKey +# ------------------------------------------------------------------ +Write-Host "Downloading ArcGIS Pro ZIP from ProGet endpoint using API key authentication..." + +$headers = @{ + 'X-ApiKey' = $proGetAssetApiKey + 'User-Agent' = 'chocolatey command line' +} + +try { + Invoke-WebRequest ` + -Uri $zipUrl ` + -Headers $headers ` + -OutFile $zipPath ` + -UseBasicParsing ` + -ErrorAction Stop +} +catch { + throw "Failed to download ArcGIS Pro ZIP from ProGet endpoint. URL: '${zipUrl}'. Error: $($_.Exception.Message)" +} + +if (-not (Test-Path -Path $zipPath)) { + throw "ZIP download failed. File not found at '${zipPath}'." +} + +$fileInfo = Get-Item -Path $zipPath -ErrorAction Stop +Write-Host "Downloaded file size: $($fileInfo.Length) bytes" + +if ($fileInfo.Length -le 0) { + throw "Downloaded ZIP file is empty: '${zipPath}'" +} + +# ------------------------------------------------------------------ +# Validate Downloaded Content Is Not HTML/Login Page +# ------------------------------------------------------------------ +$firstLine = $null +try { + $firstLine = Get-Content -Path $zipPath -TotalCount 1 -ErrorAction Stop +} +catch { + $firstLine = $null +} + +if ($firstLine -match 'Log In') { + throw "Downloaded content is HTML instead of the expected ZIP. Verify the ProGet endpoint URL and API key scope. URL: '${zipUrl}'" +} + +# ------------------------------------------------------------------ +# Validate ZIP Checksum +# ------------------------------------------------------------------ +Get-ChecksumValid -File $zipPath -Checksum $zipChecksum -ChecksumType $zipChecksumType + +# ------------------------------------------------------------------ +# Extract ZIP +# ------------------------------------------------------------------ +Write-Host "Extracting ZIP to '${toolsDir}'..." + +Get-ChocolateyUnzip -FileFullPath $zipPath -Destination $toolsDir + +# ------------------------------------------------------------------ +# Locate MSI Recursively +# ------------------------------------------------------------------ +Write-Host "Searching recursively for '${expectedMsiName}'..." + +$msiCandidates = Get-ChildItem -Path $toolsDir -Recurse -File -ErrorAction Stop | + Where-Object { $_.Name -ieq $expectedMsiName } + +if (-not $msiCandidates) { + throw "Expected MSI '${expectedMsiName}' was not found under '${toolsDir}' after extraction." +} + +if ($msiCandidates.Count -gt 1) { + $candidateList = ($msiCandidates | Select-Object -ExpandProperty FullName) -join [Environment]::NewLine + throw "Multiple MSI files named '${expectedMsiName}' were found. Cannot continue safely.`n${candidateList}" +} + +$msiPath = $msiCandidates[0].FullName + +if (-not (Test-Path -Path $msiPath)) { + throw "Resolved MSI path does not exist: '${msiPath}'" +} + +Write-Host "Resolved MSI path: ${msiPath}" + +# ------------------------------------------------------------------ +# Install MSI +# ------------------------------------------------------------------ +Write-Host "Installing ArcGIS Pro..." + +$packageArgs = @{ + packageName = $packageName + fileType = 'MSI' + file = $msiPath + softwareName = 'arcgispro*' + checksum = '703da43c59dbbdd2bbcc47c4173e6c90a110cc6ad697127c84158ddfd35acf0e' + checksumType = 'sha256' + silentArgs = @( + '/qn' + '/norestart' + 'ACCEPTEULA=yes' + 'ALLUSERS=1' + 'CHECKFORUPDATESATSTARTUP=0' + 'AUTHORIZATION_TYPE=NAMED_USER' + 'LOCK_AUTH_SETTINGS=TRUE' + 'ESRI_LICENSE_HOST=111.11.11.11' + 'SOFTWARE_CLASS=Professional' + "/l*v `"$logPath`"" + ) -join ' ' + validExitCodes = @(0, 3010) + useOriginalLocation = $true +} + +Install-ChocolateyInstallPackage @packageArgs + +Write-Host "Installation completed successfully." +Write-Host "MSI log file: ${logPath}" \ No newline at end of file diff --git a/C4BImplementationJourney4LocalGovernment-Hein/demo/BootstrappingCoreAppsDemo.ps1 b/C4BImplementationJourney4LocalGovernment-Hein/demo/BootstrappingCoreAppsDemo.ps1 new file mode 100644 index 0000000..7c1fc35 --- /dev/null +++ b/C4BImplementationJourney4LocalGovernment-Hein/demo/BootstrappingCoreAppsDemo.ps1 @@ -0,0 +1,173 @@ +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +# ------------------------------------------------------------------------------------------------- +# Chocolatey Core Apps Installer (MDT-safe) +# - Reads core_apps.config in the same folder as this script +# - Ensures Chocolatey exists (optionally bootstraps via New-ClientReg-Admin.ps1 if present) +# - Installs packages one-by-one with retries +# - Returns 0 (success), 3010 (reboot required), or a real Chocolatey exit code on failure +# ------------------------------------------------------------------------------------------------- + +# --- Logging (Transcript) ------------------------------------------------------------------------ +$logRoot = 'C:\Windows\Temp\Choco' +New-Item -ItemType Directory -Path $logRoot -Force | Out-Null +$logFile = Join-Path $logRoot ("ChocolateyCoreAppsInstall_{0:yyyyMMdd_HHmmss}.log" -f (Get-Date)) + +try { Start-Transcript -Path $logFile -Append | Out-Null } catch { } + +function Write-Log { + param( + [Parameter(Mandatory)][string]$Message + ) + Write-Host $Message +} + +function Resolve-ChocoExe { + $candidates = @() + + if ($env:ProgramData) { + $c1 = Join-Path $env:ProgramData 'chocolatey\bin\choco.exe' + if (Test-Path -LiteralPath $c1) { $candidates += $c1 } + } + if ($env:ChocolateyInstall) { + $c2 = Join-Path $env:ChocolateyInstall 'bin\choco.exe' + if (Test-Path -LiteralPath $c2) { $candidates += $c2 } + } + + if ($candidates.Count -gt 0) { return $candidates[0] } + + $cmd = Get-Command choco -ErrorAction SilentlyContinue + if ($cmd -and (Test-Path -LiteralPath $cmd.Source)) { return $cmd.Source } + + return $null +} + +function Get-CorePackagesFromConfig { + param( + [Parameter(Mandatory)][string]$ConfigPath + ) + + if (-not (Test-Path -LiteralPath $ConfigPath)) { + throw "Core apps config not found: $ConfigPath" + } + + [xml]$xml = Get-Content -LiteralPath $ConfigPath -Encoding UTF8 + + if (-not $xml.packages -or -not $xml.packages.package) { + throw "No entries were found in: $ConfigPath" + } + + $ids = @() + foreach ($p in $xml.packages.package) { + if ($p.id -and ($p.id.Trim().Length -gt 0)) { + $ids += $p.id.Trim() + } + } + + $ids = $ids | Select-Object -Unique + if ($ids.Count -eq 0) { throw "Config contained no valid package IDs: $ConfigPath" } + + return $ids +} + +function Invoke-ChocoInstall { + param( + [Parameter(Mandatory)][string]$ChocoExe, + [Parameter(Mandatory)][string]$PackageId, + [int]$MaxAttempts = 2 + ) + + for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { + Write-Log ("[INFO] Installing '{0}' (attempt {1}/{2})" -f $PackageId, $attempt, $MaxAttempts) + + # Keep output controlled for MDT logs; no progress bar. + & $ChocoExe install $PackageId -y --no-progress --limit-output + $code = [int]$LASTEXITCODE + + if ($code -eq 0) { + Write-Log ("[OK] '{0}' installed/verified successfully." -f $PackageId) + return 0 + } + + if ($code -eq 3010) { + Write-Log ("[OK] '{0}' installed; reboot required (3010)." -f $PackageId) + return 3010 + } + + Write-Log ("[WARN] '{0}' returned exit code {1}." -f $PackageId, $code) + + # Retry only if attempts remain + if ($attempt -lt $MaxAttempts) { + Start-Sleep -Seconds 5 + continue + } + + # No more retries -> fail with the real Chocolatey exit code + throw "Package '$PackageId' failed after $MaxAttempts attempts (exit code $code)." + } +} + +try { + Write-Log ("[INFO] Log: {0}" -f $logFile) + + $configPath = Join-Path $PSScriptRoot 'core_apps.config' + + # --- Ensure Chocolatey exists (bootstrap if needed) ------------------------------------------ + $chocoExe = Resolve-ChocoExe + if (-not $chocoExe) { + Write-Log "[WARN] choco.exe not found. Attempting bootstrap via New-ClientReg-Admin.ps1 (if present)." + + $bootstrapCandidates = @( + (Join-Path $PSScriptRoot 'New-ClientReg-Admin.ps1'), + (Join-Path $PSScriptRoot 'New-ClientReg-Admin-ProGet.ps1') + ) + + $bootstrap = $bootstrapCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 + if (-not $bootstrap) { + throw "Chocolatey is missing and no bootstrap script was found beside this installer." + } + + Write-Log ("[INFO] Bootstrapping Chocolatey: {0}" -f $bootstrap) + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootstrap + $bCode = [int]$LASTEXITCODE + if ($bCode -ne 0 -and $bCode -ne 3010) { + throw "Bootstrap failed with exit code $bCode." + } + + $chocoExe = Resolve-ChocoExe + if (-not $chocoExe) { + throw "Bootstrap completed but choco.exe is still not available." + } + + Write-Log ("[OK] Chocolatey found at: {0}" -f $chocoExe) + } else { + Write-Log ("[OK] Chocolatey found at: {0}" -f $chocoExe) + } + + # --- Read config and install packages -------------------------------------------------------- + $packages = Get-CorePackagesFromConfig -ConfigPath $configPath + Write-Log ("[INFO] Core apps from config: {0}" -f ($packages -join ', ')) + + $needsReboot = $false + + foreach ($pkg in $packages) { + $code = Invoke-ChocoInstall -ChocoExe $chocoExe -PackageId $pkg -MaxAttempts 2 + if ($code -eq 3010) { $needsReboot = $true } + } + + if ($needsReboot) { + Write-Log "[INFO] One or more installs requested a reboot." + exit 3010 + } + + Write-Log "[OK] Core apps installation completed successfully." + exit 0 +} +catch { + Write-Log ("[ERROR] {0}" -f $_.Exception.Message) + exit 1 +} +finally { + try { Stop-Transcript | Out-Null } catch { } +} diff --git a/C4BImplementationJourney4LocalGovernment-Hein/demo/core_apps.config b/C4BImplementationJourney4LocalGovernment-Hein/demo/core_apps.config new file mode 100644 index 0000000..2c4bbfc --- /dev/null +++ b/C4BImplementationJourney4LocalGovernment-Hein/demo/core_apps.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/C4BImplementationJourney4LocalGovernment-Hein/slides/C4BImplementationJourneyforLocalGov-Hein.pptx b/C4BImplementationJourney4LocalGovernment-Hein/slides/C4BImplementationJourneyforLocalGov-Hein.pptx new file mode 100644 index 0000000..4ba6c50 Binary files /dev/null and b/C4BImplementationJourney4LocalGovernment-Hein/slides/C4BImplementationJourneyforLocalGov-Hein.pptx differ