Files
MercuryToolbox/scripts/setup-gitea-runner.ps1

320 lines
9.5 KiB
PowerShell

[CmdletBinding()]
param(
[string]$InstanceUrl,
[string]$Owner,
[string]$Repo,
[string]$RunnerName,
[string]$RunnerRoot,
[string]$ApiToken,
[switch]$SkipInstall,
[switch]$SkipStartupRegistration
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
function Invoke-GitCapture {
param(
[Parameter(Mandatory = $true)]
[string[]]$ArgumentList
)
$output = & git @ArgumentList 2>&1
if ($LASTEXITCODE -ne 0) {
throw "git $($ArgumentList -join ' ') failed: $($output -join "`n")"
}
return (($output | ForEach-Object { [string]$_ }) -join "`n").Trim()
}
function Resolve-GiteaRepositoryContext {
$remoteUrl = Invoke-GitCapture -ArgumentList @('remote', 'get-url', 'origin')
$normalized = $remoteUrl.Trim()
if ($normalized -match '^(https?://[^/]+)/([^/]+)/([^/]+?)(?:\.git)?$') {
return [pscustomobject]@{
BaseUrl = $Matches[1]
Owner = $Matches[2]
Repo = $Matches[3]
}
}
if ($normalized -match '^ssh://git@([^/:]+)(?::(\d+))?/([^/]+)/([^/]+?)(?:\.git)?$') {
$giteaHost = $Matches[1]
$port = if ([string]::IsNullOrWhiteSpace($Matches[2])) { '' } else { ":$($Matches[2])" }
return [pscustomobject]@{
BaseUrl = "https://$giteaHost$port"
Owner = $Matches[3]
Repo = $Matches[4]
}
}
throw "Could not parse Gitea origin remote URL: $normalized"
}
function Resolve-ApiToken {
param(
[Parameter(Mandatory = $true)]
[string]$BaseUrl,
[Parameter(Mandatory = $true)]
[string]$RepoOwner,
[Parameter(Mandatory = $true)]
[string]$RepoName,
[string]$ExplicitToken
)
if (-not [string]::IsNullOrWhiteSpace($ExplicitToken)) {
return $ExplicitToken
}
foreach ($name in @('GITEA_API_TOKEN', 'GITEA_TOKEN', 'GITHUB_TOKEN')) {
$value = [Environment]::GetEnvironmentVariable($name)
if (-not [string]::IsNullOrWhiteSpace($value)) {
return $value
}
}
$uri = [Uri]$BaseUrl
$lines = @(
'protocol=https'
"host=$($uri.Authority)"
"path=$RepoOwner/$RepoName.git"
''
) | git credential fill
$passwordLine = $lines | Where-Object { $_ -like 'password=*' } | Select-Object -First 1
if ($null -eq $passwordLine) {
throw 'Could not resolve a Gitea API token from the current environment or git credential manager.'
}
return $passwordLine.Substring(9)
}
function Invoke-GiteaApi {
param(
[Parameter(Mandatory = $true)]
[string]$Method,
[Parameter(Mandatory = $true)]
[string]$Uri,
[Parameter(Mandatory = $true)]
[string]$Token
)
$headers = @{
Authorization = "token $Token"
}
try {
return Invoke-RestMethod -Method $Method -Uri $Uri -Headers $headers -ErrorAction Stop
}
catch {
$response = $_.Exception.Response
if ($null -eq $response) {
throw
}
$statusCode = [int]$response.StatusCode
$payload = ''
if ($response -is [System.Net.Http.HttpResponseMessage]) {
if ($null -ne $_.ErrorDetails -and -not [string]::IsNullOrWhiteSpace($_.ErrorDetails.Message)) {
$payload = $_.ErrorDetails.Message
}
elseif ($null -ne $response.Content) {
try {
$payload = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
}
catch {
$payload = ''
}
}
}
elseif ($response.PSObject.Methods.Name -contains 'GetResponseStream') {
$stream = $response.GetResponseStream()
if ($null -ne $stream) {
$reader = [System.IO.StreamReader]::new($stream)
try {
$payload = $reader.ReadToEnd()
}
finally {
$reader.Dispose()
$stream.Dispose()
}
}
}
throw "Gitea API $Method $Uri failed with HTTP ${statusCode}: $payload"
}
}
function Resolve-RunnerExe {
$command = Get-Command -Name 'gitea-runner.exe' -ErrorAction SilentlyContinue
if ($null -ne $command) {
return $command.Source
}
$command = Get-Command -Name 'gitea-runner' -ErrorAction SilentlyContinue
if ($null -ne $command) {
return $command.Source
}
$candidate = Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages\Gitea.Runner_Microsoft.Winget.Source_8wekyb3d8bbwe\gitea-runner.exe'
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
return $candidate
}
if ($SkipInstall) {
throw 'gitea-runner was not found and -SkipInstall was requested.'
}
& winget install --id Gitea.Runner -e --accept-package-agreements --accept-source-agreements
if ($LASTEXITCODE -ne 0) {
throw "winget install Gitea.Runner failed with exit code $LASTEXITCODE"
}
if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
throw "gitea-runner was installed but not found at expected path: $candidate"
}
return $candidate
}
$context = Resolve-GiteaRepositoryContext
if ([string]::IsNullOrWhiteSpace($InstanceUrl)) {
$InstanceUrl = $context.BaseUrl
}
if ([string]::IsNullOrWhiteSpace($Owner)) {
$Owner = $context.Owner
}
if ([string]::IsNullOrWhiteSpace($Repo)) {
$Repo = $context.Repo
}
if ([string]::IsNullOrWhiteSpace($RunnerName)) {
$RunnerName = "$Repo-local-win"
}
if ([string]::IsNullOrWhiteSpace($RunnerRoot)) {
$RunnerRoot = Join-Path $env:LOCALAPPDATA "GiteaRunner\$Repo"
}
$runnerExe = Resolve-RunnerExe
$ApiToken = Resolve-ApiToken -BaseUrl $InstanceUrl -RepoOwner $Owner -RepoName $Repo -ExplicitToken $ApiToken
$registration = Invoke-GiteaApi -Method 'POST' -Uri "$InstanceUrl/api/v1/repos/$Owner/$Repo/actions/runners/registration-token" -Token $ApiToken
$registrationToken = [string]$registration.token
if ([string]::IsNullOrWhiteSpace($registrationToken)) {
throw 'Gitea did not return a runner registration token.'
}
$configPath = Join-Path $RunnerRoot 'config.yaml'
$registrationPath = Join-Path $RunnerRoot '.runner'
$logPath = Join-Path $RunnerRoot 'daemon.log'
$startScriptPath = Join-Path $RunnerRoot 'start-runner.ps1'
$startupFolder = [Environment]::GetFolderPath('Startup')
$startupCmdPath = Join-Path $startupFolder "$Repo-gitea-runner.cmd"
$workdirParent = Join-Path $RunnerRoot 'workdir'
New-Item -ItemType Directory -Force -Path $RunnerRoot, $workdirParent | Out-Null
$fullPath = [string]::Join(';', (@(
[Environment]::GetEnvironmentVariable('Path', 'User'),
[Environment]::GetEnvironmentVariable('Path', 'Machine')
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }))
$cargoHome = if ($env:CARGO_HOME) { $env:CARGO_HOME } else { Join-Path $HOME '.cargo' }
$rustupHome = if ($env:RUSTUP_HOME) { $env:RUSTUP_HOME } else { Join-Path $HOME '.rustup' }
$repoHostLabel = "${Repo}:host"
$config = @"
log:
level: info
runner:
file: .runner
capacity: 1
envs:
PATH: $fullPath
CARGO_HOME: $cargoHome
RUSTUP_HOME: $rustupHome
CARGO_INCREMENTAL: '0'
env_file: .env
timeout: 3h
shutdown_timeout: 30s
insecure: false
fetch_timeout: 5s
fetch_interval: 2s
fetch_interval_max: 5s
workdir_cleanup_age: 24h
idle_cleanup_interval: 10m
labels:
- 'windows-amd64:host'
- 'windows:host'
- '$repoHostLabel'
allocate_pty: false
cache:
enabled: true
dir: ''
host: ''
port: 0
external_server: ''
external_secret: ''
offline_mode: false
container:
network: ''
privileged: false
options:
workdir_parent:
valid_volumes: []
docker_host: ''
force_pull: true
force_rebuild: false
require_docker: false
docker_timeout: 0s
bind_workdir: false
host:
workdir_parent: '$($workdirParent -replace '\\', '/')'
metrics:
enabled: false
addr: '127.0.0.1:9101'
"@
Set-Content -LiteralPath $configPath -Value $config -Encoding utf8NoBOM
if (-not (Test-Path -LiteralPath $registrationPath)) {
Push-Location -LiteralPath $RunnerRoot
try {
& $runnerExe register --no-interactive --config $configPath --instance $InstanceUrl --token $registrationToken --name $RunnerName
if ($LASTEXITCODE -ne 0) {
throw "gitea-runner register failed with exit code $LASTEXITCODE"
}
}
finally {
Pop-Location
}
}
$startScript = @"
`$ErrorActionPreference = 'Stop'
Set-Location '$RunnerRoot'
& '$runnerExe' daemon --config '$configPath' *>> '$logPath'
"@
Set-Content -LiteralPath $startScriptPath -Value $startScript -Encoding utf8NoBOM
if (-not $SkipStartupRegistration) {
$startupCmd = @"
@echo off
powershell -NoProfile -WindowStyle Hidden -File "$startScriptPath"
"@
Set-Content -LiteralPath $startupCmdPath -Value $startupCmd -Encoding ascii
}
$existing = Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'gitea-runner.exe' -and $_.CommandLine -like "*$configPath*" }
if ($null -eq $existing) {
Start-Process -FilePath $runnerExe -ArgumentList @('daemon', '--config', $configPath) -WorkingDirectory $RunnerRoot -WindowStyle Hidden | Out-Null
Start-Sleep -Seconds 3
}
$runners = Invoke-GiteaApi -Method 'GET' -Uri "$InstanceUrl/api/v1/repos/$Owner/$Repo/actions/runners" -Token $ApiToken
$matched = @($runners.runners | Where-Object { $_.name -eq $RunnerName })
if ($matched.Count -eq 0) {
throw "Runner $RunnerName was not visible in the repository runner list after setup."
}
$matched | ConvertTo-Json -Depth 6