320 lines
9.5 KiB
PowerShell
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
|