derive-gitlab.ps1 7.25 KB
#!/usr/bin/env pwsh
# derive-gitlab.ps1 — Windows 对应版(PowerShell 5.1+ 原生可跑,无外部依赖)
#                    与同目录 derive-gitlab.sh 严格等价 — 改动任一份必须同步对方。
#
# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File derive-gitlab.ps1 [.env.local 路径]
#
# 派生字段、退出码、回填策略 见 derive-gitlab.sh 头部注释。

[CmdletBinding()]
param([string]$EnvFile = '.env.local')

# 让 Chinese 输出在 cmd / PowerShell 控制台正常显示
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}

if (-not (Test-Path -LiteralPath $EnvFile -PathType Leaf)) {
    [Console]::Error.WriteLine("derive-gitlab.ps1: env file not found: $EnvFile")
    exit 2
}

# ---- 取 origin URL ----
$url = ''
try {
    $raw = & git remote get-url origin 2>$null
    if ($LASTEXITCODE -eq 0 -and $raw) { $url = ($raw -join '').Trim() }
} catch { $url = '' }

if ([string]::IsNullOrEmpty($url)) {
    [Console]::Error.WriteLine("derive-gitlab.ps1: 未配置 origin 远程,GITLAB_* 留给用户手填")
    exit 0
}

# ---- 仅支持 http(s) ----
$scheme = $null
if ($url.StartsWith('https://')) { $scheme = 'https' }
elseif ($url.StartsWith('http://')) { $scheme = 'http' }
else {
    [Console]::Error.WriteLine("derive-gitlab.ps1: origin 不是 http(s) URL ($url),跳过派生")
    exit 0
}

$rest = $url.Substring("${scheme}://".Length)
$slashIdx = $rest.IndexOf('/')
if ($slashIdx -lt 0) {
    [Console]::Error.WriteLine("derive-gitlab.ps1: origin URL 缺少路径段 ($url),跳过派生")
    exit 0
}
$gitlabHost = $rest.Substring(0, $slashIdx)
$pathRaw = $rest.Substring($slashIdx + 1)
if ($pathRaw.EndsWith('.git')) { $pathRaw = $pathRaw.Substring(0, $pathRaw.Length - 4) }
$repoName = Split-Path -Leaf $pathRaw

$apiUrl = "${scheme}://${gitlabHost}/api/v3"

# ---- 读 .env.local 全文,按原行尾切分 ----
$rawBytes = [System.IO.File]::ReadAllBytes($EnvFile)
$hasBom = ($rawBytes.Length -ge 3 -and $rawBytes[0] -eq 0xEF -and $rawBytes[1] -eq 0xBB -and $rawBytes[2] -eq 0xBF)
$rawText = if ($hasBom) {
    [System.Text.Encoding]::UTF8.GetString($rawBytes, 3, $rawBytes.Length - 3)
} else {
    [System.Text.Encoding]::UTF8.GetString($rawBytes)
}
$eol = if ($rawText -match "`r`n") { "`r`n" } else { "`n" }
$hadTrailingNewline = $rawText.EndsWith("`n")
$lines = [System.Collections.Generic.List[string]]::new()
foreach ($l in ($rawText -split "`r?`n")) { $lines.Add($l) | Out-Null }
if ($hadTrailingNewline -and $lines.Count -gt 0 -and $lines[$lines.Count - 1] -eq '') {
    $lines.RemoveAt($lines.Count - 1)
}

function Get-FieldStripped {
    param([string]$Key)
    for ($i = 0; $i -lt $script:lines.Count; $i++) {
        $ln = $script:lines[$i]
        if ($ln.StartsWith("$Key=")) {
            $val = $ln.Substring($Key.Length + 1)
            # 剥外层单/双引号
            if ($val.Length -ge 2) {
                $f = $val[0]; $l = $val[$val.Length - 1]
                if (($f -eq "'" -and $l -eq "'") -or ($f -eq '"' -and $l -eq '"')) {
                    $val = $val.Substring(1, $val.Length - 2)
                }
            }
            return @{ Found = $true; Index = $i; Stripped = $val }
        }
    }
    return @{ Found = $false }
}

function Update-Field {
    param([string]$Key, [string]$NewVal)
    $info = Get-FieldStripped -Key $Key
    if (-not $info.Found) {
        [Console]::Error.WriteLine("  $Key = (.env.local 中无此行,跳过)")
        return
    }
    $stripped = $info.Stripped
    if ([string]::IsNullOrEmpty($stripped) -or $stripped -eq 'TBD(A5 自动补)') {
        $script:lines[$info.Index] = "$Key=$NewVal"
        $script:modified = $true
        Write-Host "  $Key = $NewVal  [已派生填入]"
    } elseif ($stripped -eq $NewVal) {
        Write-Host "  $Key = $NewVal  [已是派生值,无需更新]"
    } else {
        Write-Host "  $Key = $stripped  [保留用户手填,未覆盖派生值 $NewVal]"
    }
}

function Report-Token {
    $info = Get-FieldStripped -Key 'GITLAB_TOKEN'
    if (-not $info.Found) {
        [Console]::Error.WriteLine("  GITLAB_TOKEN = (.env.local 中无此行,跳过)")
        return
    }
    $stripped = $info.Stripped
    if ([string]::IsNullOrEmpty($stripped) -or $stripped.StartsWith('【人工填写:') -or $stripped.StartsWith('TBD')) {
        Write-Host "  GITLAB_TOKEN = $stripped  [待人工填写:GitLab Profile → Account → Private token]"
    } else {
        $masked = if ($stripped.Length -le 8) {
            ('*' * $stripped.Length)
        } else {
            $stripped.Substring(0, 4) + ('*' * ($stripped.Length - 8)) + $stripped.Substring($stripped.Length - 4)
        }
        Write-Host "  GITLAB_TOKEN = $masked  [已填入,长度 $($stripped.Length)]"
    }
}

$script:modified = $false

Write-Host "derive-gitlab.ps1: 从 origin ($url) 派生 GitLab 凭据:"
Update-Field -Key 'GITLAB_API_URL' -NewVal $apiUrl

# ---- GITLAB_PROJECT_ID:经 GitLab API 解析数字 ID ----
$tokenInfo = Get-FieldStripped -Key 'GITLAB_TOKEN'
$tokenVal = if ($tokenInfo.Found) { $tokenInfo.Stripped } else { '' }

$tokenUsable = $true
if ([string]::IsNullOrEmpty($tokenVal) -or $tokenVal.StartsWith('【人工填写:') -or $tokenVal.StartsWith('TBD')) {
    Write-Host "  GITLAB_PROJECT_ID = TBD  [token 未填,跳过 API 解析;填完 token 后重跑此脚本]"
    $tokenUsable = $false
}

if ($tokenUsable) {
    # PS 5.1 默认 TLS1.0/1.1,自建 GitLab 通常需要 TLS1.2
    try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}
    $headers = @{ 'PRIVATE-TOKEN' = $tokenVal }

    $userOk = $false
    try {
        $userResp = Invoke-WebRequest -Uri "$apiUrl/user" -Headers $headers -UseBasicParsing -ErrorAction Stop
        if ([int]$userResp.StatusCode -eq 200) { $userOk = $true }
        else { Write-Host "  GITLAB_PROJECT_ID = TBD  [token 验证失败 HTTP $([int]$userResp.StatusCode),留 TBD 待人工确认]" }
    } catch {
        $code = 0
        if ($_.Exception.Response) { try { $code = [int]$_.Exception.Response.StatusCode } catch {} }
        if ($code -eq 0) { $code = '000' }
        Write-Host "  GITLAB_PROJECT_ID = TBD  [token 验证失败 HTTP $code,留 TBD 待人工确认]"
    }

    if ($userOk) {
        try {
            $searchUri = "$apiUrl/projects?search=$([uri]::EscapeDataString($repoName))&simple=true&per_page=50"
            $projects = Invoke-RestMethod -Uri $searchUri -Headers $headers -UseBasicParsing -ErrorAction Stop
            $match = $projects | Where-Object { $_.path_with_namespace -eq $pathRaw } | Select-Object -First 1
            if ($match) {
                Update-Field -Key 'GITLAB_PROJECT_ID' -NewVal "$($match.id)"
            } else {
                Write-Host "  GITLAB_PROJECT_ID = TBD  [API 未匹配 path_with_namespace=$pathRaw,请到 GitLab 项目设置页查数字 ID 后填入]"
            }
        } catch {
            Write-Host "  GITLAB_PROJECT_ID = TBD  [API 调用失败:$($_.Exception.Message)]"
        }
    }
}

Report-Token

# ---- 回写 .env.local(保持 EOL / BOM 状态)----
if ($script:modified) {
    $newText = [string]::Join($eol, $lines.ToArray())
    if ($hadTrailingNewline) { $newText += $eol }
    $encoding = New-Object System.Text.UTF8Encoding($hasBom)
    [System.IO.File]::WriteAllText($EnvFile, $newText, $encoding)
}