Files
linux-scripts/gitlab-smoke-tests.ps1
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

865 lines
29 KiB
PowerShell

###############################################################################
# gitlab-smoke-tests.ps1 - Verify GitLab instance health after upgrades
#
# PowerShell port of gitlab-smoke-tests.sh. Zero external dependencies
# beyond PowerShell 5.1+ and git. Runs on Windows, Linux, and macOS.
#
# Author: Phil Connor
# Contact: contact@mylinux.work
# License: MIT
# Version 1.00
#
# Usage:
# $env:GITLAB_URL = "https://gitlab.example.com"
# $env:GITLAB_TOKEN = "glpat-xxxxxxxxxxxx"
# .\gitlab-smoke-tests.ps1
# .\gitlab-smoke-tests.ps1 -SkipGit -SkipRegistry
# .\gitlab-smoke-tests.ps1 -Insecure -Format junit
# .\gitlab-smoke-tests.ps1 -Format tap
###############################################################################
[CmdletBinding()]
param(
[string]$GitLabUrl = $env:GITLAB_URL,
[string]$GitLabToken = $env:GITLAB_TOKEN,
[string]$GitLabUser = $(if ($env:GITLAB_USER) { $env:GITLAB_USER } else { "root" }),
[string]$HealthToken = $env:GITLAB_HEALTH_TOKEN,
[string]$ProjectPrefix = $(if ($env:SMOKE_PROJECT_PREFIX) { $env:SMOKE_PROJECT_PREFIX } else { "smoke-test" }),
[int]$Timeout = $(if ($env:CURL_TIMEOUT) { [int]$env:CURL_TIMEOUT } else { 10 }),
[switch]$Insecure,
[switch]$SkipGit,
[switch]$SkipRegistry,
[switch]$SkipCleanup,
[ValidateSet("text","tap","junit")]
[string]$Format = "text",
[string]$JunitFile = "smoke-results.xml",
[switch]$NoColor
)
$ErrorActionPreference = "Continue"
# ============================================================================
# STATE
# ============================================================================
$script:Pass = 0
$script:Fail = 0
$script:Skip = 0
$script:Total = 0
$script:Results = @()
$script:CleanupProjectId = ""
$script:TmpDir = ""
$script:StartTime = $null
$script:GitCloneOk = $false
# ============================================================================
# COLORS
# ============================================================================
function Write-Color {
param([string]$Text, [string]$Color = "White")
if ($NoColor) {
Write-Host $Text
} else {
Write-Host $Text -ForegroundColor $Color
}
}
function Write-Log { param([string]$Msg) Write-Color "[INFO] $Msg" "Cyan" }
function Write-Warn { param([string]$Msg) Write-Color "[WARN] $Msg" "Yellow" }
function Write-Err { param([string]$Msg) Write-Color "[ERROR] $Msg" "Red" }
# ============================================================================
# TEST RESULT RECORDING
# ============================================================================
function Record-Pass {
param([string]$Name, [string]$Detail = "")
$script:Pass++
$script:Total++
$script:Results += [PSCustomObject]@{ Status="PASS"; Name=$Name; Detail=$Detail }
if ($Format -eq "tap") {
Write-Host "ok $($script:Total) - $Name"
} else {
$msg = " $(if($NoColor){'[PASS]'}else{[char]0x2713}) $Name"
if ($Detail) { $msg += " - $Detail" }
Write-Color $msg "Green"
}
}
function Record-Fail {
param([string]$Name, [string]$Detail = "")
$script:Fail++
$script:Total++
$script:Results += [PSCustomObject]@{ Status="FAIL"; Name=$Name; Detail=$Detail }
if ($Format -eq "tap") {
Write-Host "not ok $($script:Total) - $Name"
if ($Detail) { Write-Host " # $Detail" }
} else {
$msg = " $(if($NoColor){'[FAIL]'}else{[char]0x2717}) $Name"
if ($Detail) { $msg += " - $Detail" }
Write-Color $msg "Red"
}
}
function Record-Skip {
param([string]$Name, [string]$Reason = "")
$script:Skip++
$script:Total++
$script:Results += [PSCustomObject]@{ Status="SKIP"; Name=$Name; Detail=$Reason }
if ($Format -eq "tap") {
Write-Host "ok $($script:Total) - $Name # SKIP $Reason"
} else {
$msg = " $(if($NoColor){'[SKIP]'}else{[char]0x2298}) $Name"
if ($Reason) { $msg += " - $Reason" }
Write-Color $msg "Yellow"
}
}
# ============================================================================
# HTTP HELPERS
# ============================================================================
function Invoke-GitLabApi {
param(
[string]$Method,
[string]$Endpoint,
[string]$Body = $null,
[switch]$StatusOnly
)
$uri = "$GitLabUrl/api/v4$Endpoint"
$headers = @{ "Content-Type" = "application/json" }
if ($GitLabToken) { $headers["PRIVATE-TOKEN"] = $GitLabToken }
$params = @{
Uri = $uri
Method = $Method
Headers = $headers
TimeoutSec = $Timeout
UseBasicParsing = $true
ErrorAction = "Stop"
}
if ($Insecure -and $PSVersionTable.PSVersion.Major -ge 7) {
$params["SkipCertificateCheck"] = $true
}
if ($Body) {
$params["Body"] = $Body
}
try {
if ($StatusOnly) {
$response = Invoke-WebRequest @params
return [int]$response.StatusCode
} else {
return Invoke-RestMethod @params
}
} catch {
if ($StatusOnly) {
if ($_.Exception.Response) {
return [int]$_.Exception.Response.StatusCode
}
return 0
}
return $null
}
}
function Invoke-HealthCheck {
param([string]$Path)
$uri = "$GitLabUrl$Path"
if ($HealthToken) { $uri += "?token=$HealthToken" }
$params = @{
Uri = $uri
Method = "GET"
TimeoutSec = $Timeout
UseBasicParsing = $true
ErrorAction = "Stop"
}
if ($Insecure -and $PSVersionTable.PSVersion.Major -ge 7) {
$params["SkipCertificateCheck"] = $true
}
try {
$response = Invoke-WebRequest @params
return [int]$response.StatusCode
} catch {
if ($_.Exception.Response) {
return [int]$_.Exception.Response.StatusCode
}
return 0
}
}
# ============================================================================
# TLS HELPER
# ============================================================================
function Get-TlsCertExpiry {
param([string]$HostName, [int]$Port = 443)
try {
$tcpClient = New-Object System.Net.Sockets.TcpClient
$tcpClient.ReceiveTimeout = $Timeout * 1000
$tcpClient.SendTimeout = $Timeout * 1000
$tcpClient.Connect($HostName, $Port)
$sslStream = New-Object System.Net.Security.SslStream(
$tcpClient.GetStream(), $false,
{ param($s,$c,$ch,$e) return $true }
)
$sslStream.AuthenticateAsClient($HostName)
$cert = $sslStream.RemoteCertificate
$expiry = [DateTime]$cert.GetExpirationDateString()
$sslStream.Dispose()
$tcpClient.Dispose()
return $expiry
} catch {
return $null
}
}
# ============================================================================
# TEST SUITES
# ============================================================================
# -- 1. Connectivity --------------------------------------------------------
function Test-Connectivity {
Write-Host ""
Write-Color "Connectivity" "White"
# 1a. Health endpoint
$code = Invoke-HealthCheck "/-/health"
if ($code -eq 200) {
Record-Pass "GitLab health endpoint reachable" "HTTP $code"
} else {
Record-Fail "GitLab health endpoint reachable" "HTTP $code"
}
# 1b. Readiness
$code = Invoke-HealthCheck "/-/readiness"
if ($code -eq 200) {
Record-Pass "GitLab readiness check" "HTTP $code"
} else {
Record-Fail "GitLab readiness check" "HTTP $code"
}
# 1c. Liveness
$code = Invoke-HealthCheck "/-/liveness"
if ($code -eq 200) {
Record-Pass "GitLab liveness check" "HTTP $code"
} else {
Record-Fail "GitLab liveness check" "HTTP $code"
}
# 1d. TLS certificate
if ($GitLabUrl -match "^https://") {
$hostPart = $GitLabUrl -replace "^https://", "" -replace "/.*", "" -replace ":.*", ""
$portPart = 443
if ($GitLabUrl -match ":(\d+)") { $portPart = [int]$Matches[1] }
$expiry = Get-TlsCertExpiry -HostName $hostPart -Port $portPart
if ($expiry) {
$daysLeft = [math]::Floor(($expiry - (Get-Date)).TotalDays)
if ($daysLeft -gt 30) {
Record-Pass "TLS certificate valid" "$daysLeft days remaining"
} elseif ($daysLeft -gt 0) {
Record-Pass "TLS certificate valid" "$daysLeft days remaining (renew soon)"
} else {
Record-Fail "TLS certificate valid" "expired or expiring in $daysLeft days"
}
} else {
Record-Skip "TLS certificate check" "could not retrieve certificate"
}
} else {
Record-Skip "TLS certificate check" "not using HTTPS"
}
}
# -- 2. API ----------------------------------------------------------------
function Test-Api {
Write-Host ""
Write-Color "API" "White"
# 2a. Version
$versionData = Invoke-GitLabApi -Method GET -Endpoint "/version"
if ($versionData -and $versionData.version) {
Record-Pass "API version endpoint" "GitLab $($versionData.version) ($($versionData.revision))"
} else {
Record-Fail "API version endpoint" "no version returned"
}
# 2b. Authentication
$authStatus = Invoke-GitLabApi -Method GET -Endpoint "/user" -StatusOnly
if ($authStatus -eq 200) {
$userData = Invoke-GitLabApi -Method GET -Endpoint "/user"
Record-Pass "API authentication" "authenticated as $($userData.username)"
} elseif ($authStatus -eq 401) {
Record-Fail "API authentication" "token rejected (HTTP 401)"
} else {
Record-Fail "API authentication" "HTTP $authStatus"
}
# 2c. List projects
$projStatus = Invoke-GitLabApi -Method GET -Endpoint "/projects?per_page=1" -StatusOnly
if ($projStatus -eq 200) {
Record-Pass "API list projects" "database responding"
} else {
Record-Fail "API list projects" "HTTP $projStatus"
}
# 2d. List users
$userStatus = Invoke-GitLabApi -Method GET -Endpoint "/users?per_page=1" -StatusOnly
if ($userStatus -eq 200) {
Record-Pass "API list users" "user directory accessible"
} else {
Record-Fail "API list users" "HTTP $userStatus"
}
# 2e. Sidekiq
$sidekiq = Invoke-GitLabApi -Method GET -Endpoint "/sidekiq/compound_metrics"
if ($sidekiq -and -not $sidekiq.error) {
$procCount = 0
if ($sidekiq.processes) { $procCount = @($sidekiq.processes).Count }
Record-Pass "Sidekiq running" "$procCount process(es) responding"
} else {
Record-Fail "Sidekiq running" "could not query Sidekiq metrics"
}
# 2f. Runners
$runnerStatus = Invoke-GitLabApi -Method GET -Endpoint "/runners/all?per_page=1" -StatusOnly
if ($runnerStatus -eq 200) {
Record-Pass "API runners endpoint" "runner management accessible"
} elseif ($runnerStatus -eq 403) {
Record-Skip "API runners endpoint" "token lacks admin scope"
} else {
Record-Fail "API runners endpoint" "HTTP $runnerStatus"
}
# 2g. Search
$searchStatus = Invoke-GitLabApi -Method GET -Endpoint "/search?scope=projects&search=test" -StatusOnly
if ($searchStatus -eq 200) {
Record-Pass "API search" "search index responding"
} elseif ($searchStatus -eq 403) {
Record-Skip "API search" "search disabled or token lacks scope"
} else {
Record-Fail "API search" "HTTP $searchStatus"
}
}
# -- 3. Git Operations -----------------------------------------------------
function Test-Git {
if ($SkipGit) {
Write-Host ""
Write-Color "Git Operations" "White"
Record-Skip "Git clone" "SkipGit specified"
Record-Skip "Git push" "SkipGit specified"
return
}
Write-Host ""
Write-Color "Git Operations" "White"
# Create test project
$projectName = "$ProjectPrefix-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())"
$body = @{ name = $projectName; visibility = "private"; initialize_with_readme = $true } | ConvertTo-Json
$project = Invoke-GitLabApi -Method POST -Endpoint "/projects" -Body $body
if (-not $project -or -not $project.id) {
Record-Fail "Create test project" "API returned no project ID"
Record-Skip "Git clone" "no test project"
Record-Skip "Git push" "no test project"
return
}
$script:CleanupProjectId = $project.id
Record-Pass "Create test project" "$projectName (ID: $($project.id))"
# Build clone URL
$httpUrl = $project.http_url_to_repo
if (-not $httpUrl) {
$httpUrl = "$GitLabUrl/$GitLabUser/$projectName.git"
}
# Rewrite origin if API returns an internal hostname
$apiOrigin = if ($httpUrl -match "^(https?://[^/]+)") { $Matches[1] } else { "" }
if ($apiOrigin -and $apiOrigin -ne $GitLabUrl) {
$httpUrl = $httpUrl -replace [regex]::Escape($apiOrigin), $GitLabUrl
}
# Inject token
if ($httpUrl -match "^https://") {
$cloneUrl = $httpUrl -replace "^https://", "https://oauth2:${GitLabToken}@"
} elseif ($httpUrl -match "^http://") {
$cloneUrl = $httpUrl -replace "^http://", "http://oauth2:${GitLabToken}@"
} else {
$cloneUrl = $httpUrl
}
# Temp directory
$script:TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "gitlab-smoke-$([guid]::NewGuid().ToString('N').Substring(0,8))"
New-Item -ItemType Directory -Path $script:TmpDir -Force | Out-Null
# Wait for repo init
Start-Sleep -Seconds 2
# Clone
$gitArgs = @("clone")
if ($Insecure) { $env:GIT_SSL_NO_VERIFY = "true" }
$repoDir = Join-Path $script:TmpDir "repo"
$cloneOutput = & git clone $cloneUrl $repoDir 2>&1
$cloneRc = $LASTEXITCODE
if ($cloneRc -eq 0) {
$script:GitCloneOk = $true
Record-Pass "Git clone (HTTPS)" "Gitaly responding"
} else {
$shortErr = ($cloneOutput | Select-String -Pattern "fatal|error" | Select-Object -First 1) -replace [regex]::Escape($GitLabToken), "[REDACTED]"
Record-Fail "Git clone (HTTPS)" "$shortErr"
return
}
# Push
Push-Location $repoDir
try {
& git config user.email "smoke-test@example.com"
& git config user.name "Smoke Test"
"smoke test $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')" | Out-File -FilePath "smoke-test.txt" -Encoding utf8
& git add smoke-test.txt
& git commit -m "smoke test commit" 2>&1 | Out-Null
$pushOutput = & git push origin main 2>&1
$pushRc = $LASTEXITCODE
if ($pushRc -ne 0) {
$pushOutput = & git push origin master 2>&1
$pushRc = $LASTEXITCODE
}
if ($pushRc -eq 0) {
Record-Pass "Git push (HTTPS)" "write to Gitaly succeeded"
} else {
Record-Fail "Git push (HTTPS)" "push failed"
}
} finally {
Pop-Location
}
}
# -- 4. Container Registry -------------------------------------------------
function Test-Registry {
if ($SkipRegistry) {
Write-Host ""
Write-Color "Container Registry" "White"
Record-Skip "Registry API" "SkipRegistry specified"
return
}
Write-Host ""
Write-Color "Container Registry" "White"
# Check if registry is enabled
$registryEnabled = ""
$settings = Invoke-GitLabApi -Method GET -Endpoint "/application/settings"
if ($settings) {
$registryEnabled = $settings.container_registry_enabled
}
if ($registryEnabled -eq $false) {
Record-Skip "Registry API reachable" "container registry disabled in application settings"
Record-Skip "Registry project endpoint" "container registry disabled in application settings"
return
}
# Try registry v2 API
$hostPart = $GitLabUrl -replace "^https?://", "" -replace "/.*", ""
$registryStatus = 0
$registryUrls = @(
"$GitLabUrl`:5050/v2/",
"https://${hostPart}:5050/v2/",
"https://registry.${hostPart}/v2/"
)
foreach ($regUrl in $registryUrls) {
try {
$params = @{
Uri = $regUrl
Method = "GET"
TimeoutSec = $Timeout
UseBasicParsing = $true
ErrorAction = "Stop"
}
if ($Insecure -and $PSVersionTable.PSVersion.Major -ge 7) {
$params["SkipCertificateCheck"] = $true
}
$response = Invoke-WebRequest @params
$registryStatus = [int]$response.StatusCode
break
} catch {
if ($_.Exception.Response) {
$registryStatus = [int]$_.Exception.Response.StatusCode
if ($registryStatus -eq 401) { break }
}
}
}
if ($registryStatus -eq 200 -or $registryStatus -eq 401) {
Record-Pass "Registry API reachable" "HTTP $registryStatus"
} elseif ($registryStatus -eq 0) {
if ($registryEnabled -eq $true) {
Record-Fail "Registry API reachable" "enabled in settings but not reachable at standard ports/hosts"
} else {
Record-Skip "Registry API reachable" "not found at standard ports/hosts (settings unreadable - may need admin token)"
}
} else {
Record-Fail "Registry API reachable" "HTTP $registryStatus"
}
# Project-level registry
if ($script:CleanupProjectId) {
$regStatus = Invoke-GitLabApi -Method GET -Endpoint "/projects/$($script:CleanupProjectId)/registry/repositories" -StatusOnly
if ($regStatus -eq 200) {
Record-Pass "Registry project endpoint" "project registry accessible"
} elseif ($regStatus -eq 404) {
Record-Skip "Registry project endpoint" "container registry not enabled for project"
} else {
Record-Fail "Registry project endpoint" "HTTP $regStatus"
}
}
}
# -- 5. CI/CD --------------------------------------------------------------
function Test-CICD {
Write-Host ""
Write-Color "CI/CD" "White"
# Runners
$runners = Invoke-GitLabApi -Method GET -Endpoint "/runners/all?per_page=100"
if ($runners -is [array]) {
$runnerCount = $runners.Count
$onlineCount = @($runners | Where-Object { $_.status -eq "online" }).Count
if ($onlineCount -gt 0) {
Record-Pass "CI/CD runners online" "$onlineCount/$runnerCount runners online"
} elseif ($runnerCount -gt 0) {
Record-Fail "CI/CD runners online" "0/$runnerCount runners online"
} else {
Record-Skip "CI/CD runners online" "no runners registered"
}
} else {
Record-Skip "CI/CD runners" "could not query runners (admin token required)"
}
# CI/CD settings
$cicdStatus = Invoke-GitLabApi -Method GET -Endpoint "/application/settings" -StatusOnly
if ($cicdStatus -eq 200) {
Record-Pass "CI/CD settings accessible" "application settings readable"
} elseif ($cicdStatus -eq 403) {
Record-Skip "CI/CD settings accessible" "admin token required"
} else {
Record-Fail "CI/CD settings accessible" "HTTP $cicdStatus"
}
}
# -- 6. Background Migrations ----------------------------------------------
function Test-Migrations {
Write-Host ""
Write-Color "Background Migrations" "White"
$migrations = Invoke-GitLabApi -Method GET -Endpoint "/admin/batched_background_migrations?database=main"
if ($migrations -is [array]) {
$totalMig = $migrations.Count
$failedMig = @($migrations | Where-Object { $_.status -eq "failed" }).Count
$activeMig = @($migrations | Where-Object { $_.status -eq "active" }).Count
$pausedMig = @($migrations | Where-Object { $_.status -eq "paused" }).Count
$finishedMig = @($migrations | Where-Object { $_.status -eq "finished" }).Count
if ($failedMig -gt 0) {
Record-Fail "Background migrations" "$failedMig failed, $activeMig active, $pausedMig paused, $finishedMig finished of $totalMig"
} elseif ($pausedMig -gt 0) {
Record-Fail "Background migrations" "$pausedMig paused, $activeMig active, $finishedMig finished of $totalMig"
} elseif ($activeMig -gt 0) {
Record-Pass "Background migrations" "$activeMig active, $finishedMig finished of $totalMig (in progress)"
} else {
Record-Pass "Background migrations" "all $totalMig finished"
}
} else {
$migStatus = Invoke-GitLabApi -Method GET -Endpoint "/admin/batched_background_migrations?database=main" -StatusOnly
if ($migStatus -eq 403) {
Record-Skip "Background migrations" "admin token required"
} else {
Record-Skip "Background migrations" "could not query (HTTP $migStatus)"
}
}
}
# -- 7. Components ---------------------------------------------------------
function Test-Components {
Write-Host ""
Write-Color "Components" "White"
# Metadata
$metadata = Invoke-GitLabApi -Method GET -Endpoint "/metadata"
if ($metadata -and $metadata.version) {
$edition = if ($metadata.enterprise -eq $true) { "EE" } else { "CE" }
Record-Pass "GitLab metadata" "$($metadata.version) $edition"
} elseif ($metadata) {
Record-Pass "GitLab metadata" "endpoint reachable"
} else {
Record-Skip "GitLab metadata" "metadata endpoint not available"
}
# Statistics
$stats = Invoke-GitLabApi -Method GET -Endpoint "/application/statistics"
if ($stats -and $stats.active_users) {
Record-Pass "Instance statistics" "$($stats.active_users) users, $($stats.projects) projects, $($stats.groups) groups"
} elseif ($stats) {
Record-Pass "Instance statistics" "endpoint reachable"
} else {
Record-Skip "Instance statistics" "admin token required"
}
# Gitaly (inferred)
if ($script:GitCloneOk) {
Record-Pass "Gitaly storage" "project created and cloned successfully"
} elseif ($script:CleanupProjectId) {
Record-Skip "Gitaly storage" "project created but clone was not tested or failed"
}
# PostgreSQL (inferred)
$pgStatus = Invoke-GitLabApi -Method GET -Endpoint "/projects?per_page=1&order_by=updated_at" -StatusOnly
if ($pgStatus -eq 200) {
Record-Pass "PostgreSQL" "database queries succeeding"
} else {
Record-Fail "PostgreSQL" "sorted query failed (HTTP $pgStatus)"
}
# Redis (inferred)
$redisStatus = Invoke-GitLabApi -Method GET -Endpoint "/user" -StatusOnly
if ($redisStatus -eq 200) {
Record-Pass "Redis" "session/cache operational (auth succeeded)"
} else {
Record-Skip "Redis" "cannot verify independently"
}
}
# ============================================================================
# OUTPUT
# ============================================================================
function Write-Summary {
$duration = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds)
Write-Host ""
$separator = [string]::new([char]0x2500, 40)
Write-Color $separator "White"
Write-Color "Summary $GitLabUrl" "White"
$summaryLine = " $($script:Pass) passed $($script:Fail) failed $($script:Skip) skipped (${duration}s)"
Write-Host $summaryLine
Write-Color $separator "White"
if ($script:Fail -eq 0) {
Write-Color "All tests passed." "Green"
} else {
Write-Color "$($script:Fail) test(s) failed." "Red"
}
}
function Write-TapHeader {
Write-Host "TAP version 13"
}
function Write-TapFooter {
Write-Host "1..$($script:Total)"
Write-Host "# pass $($script:Pass)"
Write-Host "# fail $($script:Fail)"
Write-Host "# skip $($script:Skip)"
}
function Write-JunitReport {
$duration = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds)
$xml = @"
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="$($script:Total)" failures="$($script:Fail)" skipped="$($script:Skip)" time="$duration">
<testsuite name="gitlab-smoke-tests" tests="$($script:Total)" failures="$($script:Fail)" skipped="$($script:Skip)" time="$duration">
"@
foreach ($r in $script:Results) {
$safeName = $r.Name -replace '&','&amp;' -replace '<','&lt;' -replace '>','&gt;' -replace '"','&quot;'
$safeDetail = $r.Detail -replace '&','&amp;' -replace '<','&lt;' -replace '>','&gt;' -replace '"','&quot;'
switch ($r.Status) {
"PASS" {
$xml += "`n <testcase name=`"$safeName`" classname=`"smoke`">"
if ($r.Detail) { $xml += "`n <system-out>$safeDetail</system-out>" }
$xml += "`n </testcase>"
}
"FAIL" {
$xml += "`n <testcase name=`"$safeName`" classname=`"smoke`">"
$xml += "`n <failure message=`"$safeDetail`">FAILED: $safeName - $safeDetail</failure>"
$xml += "`n </testcase>"
}
"SKIP" {
$xml += "`n <testcase name=`"$safeName`" classname=`"smoke`">"
$xml += "`n <skipped message=`"$safeDetail`"/>"
$xml += "`n </testcase>"
}
}
}
$xml += "`n </testsuite>"
$xml += "`n</testsuites>"
$xml | Out-File -FilePath $JunitFile -Encoding utf8
Write-Log "JUnit report written to $JunitFile"
}
# ============================================================================
# CLEANUP
# ============================================================================
function Invoke-Cleanup {
if ($script:CleanupProjectId -and -not $SkipCleanup) {
try {
Invoke-GitLabApi -Method DELETE -Endpoint "/projects/$($script:CleanupProjectId)" | Out-Null
} catch { }
}
if ($script:TmpDir -and (Test-Path $script:TmpDir)) {
Remove-Item -Recurse -Force $script:TmpDir -ErrorAction SilentlyContinue
}
if ($env:GIT_SSL_NO_VERIFY) {
Remove-Item Env:\GIT_SSL_NO_VERIFY -ErrorAction SilentlyContinue
}
}
# ============================================================================
# MAIN
# ============================================================================
function Show-Usage {
@"
Usage: .\gitlab-smoke-tests.ps1 [OPTIONS]
Smoke-test a GitLab instance. PowerShell 5.1+, git only.
Designed for air-gapped environments.
Required environment variables:
GITLAB_URL GitLab base URL (https://gitlab.example.com)
GITLAB_TOKEN Personal access token (api scope; admin for full coverage)
Optional environment variables:
GITLAB_HEALTH_TOKEN Health check access token
GITLAB_USER Username for git operations (default: root)
Parameters:
-SkipGit Skip git clone/push tests
-SkipRegistry Skip container registry tests
-SkipCleanup Don't delete the test project after run
-Insecure Allow self-signed TLS certificates
-Timeout N HTTP timeout in seconds (default: 10)
-Format FORMAT Output: text (default), tap, junit
-JunitFile FILE JUnit output path (default: smoke-results.xml)
-NoColor Disable colored output
-Verbose Show debug output
Examples:
`$env:GITLAB_URL = "https://gitlab.example.com"
`$env:GITLAB_TOKEN = "glpat-xxxxxxxxxxxx"
.\gitlab-smoke-tests.ps1
.\gitlab-smoke-tests.ps1 -Insecure -Format junit
.\gitlab-smoke-tests.ps1 -SkipGit -SkipRegistry
.\gitlab-smoke-tests.ps1 -Format tap
"@
}
# Handle PS 5.1 TLS and self-signed certs
if ($PSVersionTable.PSVersion.Major -lt 7) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if ($Insecure) {
Add-Type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAll : ICertificatePolicy {
public bool CheckValidationResult(ServicePoint sp, X509Certificate cert,
WebRequest req, int problem) { return true; }
}
"@ -ErrorAction SilentlyContinue
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAll
}
}
# Validate
if (-not $GitLabUrl) {
Write-Err "GITLAB_URL is required"
Write-Host ""
Show-Usage
exit 1
}
if (-not $GitLabToken) {
Write-Err "GITLAB_TOKEN is required"
Write-Host ""
Show-Usage
exit 1
}
$GitLabUrl = $GitLabUrl.TrimEnd("/")
$script:StartTime = Get-Date
if ($Format -eq "tap") {
Write-TapHeader
} else {
Write-Host ""
Write-Color "GitLab Smoke Tests" "White"
Write-Host "Target: $GitLabUrl"
Write-Host "Time: $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')"
Write-Host ""
}
try {
Test-Connectivity
Test-Api
Test-Git
Test-Registry
Test-CICD
Test-Migrations
Test-Components
} finally {
Invoke-Cleanup
}
if ($Format -eq "tap") {
Write-TapFooter
} elseif ($Format -eq "junit") {
Write-Summary
Write-JunitReport
} else {
Write-Summary
}
if ($script:Fail -eq 0) { exit 0 } else { exit 1 }