Agrega gitea_skill: skill para operar Gitea self-hosted (scripts API BOM-safe + push headless sin persistir token, SKILL/TOOLS/api-index). Actualiza CLAUDE.md, README.md, .env.example.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get or search a Gitea repository.
|
||||
|
||||
.DESCRIPTION
|
||||
Three modes:
|
||||
-Name <repo> -> GET /repos/{owner}/{name} (default owner = auth user)
|
||||
-Search <term> -> GET /repos/search?q=<term>
|
||||
-List -> GET /repos/search (all visible to the token)
|
||||
|
||||
.PARAMETER Owner
|
||||
Owner (user or org). Defaults to the authenticated user.
|
||||
|
||||
.PARAMETER Name
|
||||
Exact repository name to look up.
|
||||
|
||||
.PARAMETER Search
|
||||
Free-text search term.
|
||||
|
||||
.PARAMETER List
|
||||
Switch: list all visible repos.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GiteaRepo.ps1 -Name Proxmox-Coolify-Manager
|
||||
.\Get-GiteaRepo.ps1 -Search manager
|
||||
.\Get-GiteaRepo.ps1 -List
|
||||
#>
|
||||
param(
|
||||
[string]$Owner,
|
||||
[Parameter(ParameterSetName = "ByName")]
|
||||
[string]$Name,
|
||||
[Parameter(ParameterSetName = "BySearch")]
|
||||
[string]$Search,
|
||||
[Parameter(ParameterSetName = "ListAll")]
|
||||
[switch]$List
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$api = Join-Path $here "Invoke-GiteaApi.ps1"
|
||||
|
||||
function Resolve-GiteaOwner {
|
||||
param([string]$OwnerArg)
|
||||
if ($OwnerArg) { return $OwnerArg }
|
||||
$me = & $api -Path "/user"
|
||||
return $me.login
|
||||
}
|
||||
|
||||
switch ($PSCmdlet.ParameterSetName) {
|
||||
"ByName" {
|
||||
if (-not $Name) { throw "Provide -Name." }
|
||||
$o = Resolve-GiteaOwner -OwnerArg $Owner
|
||||
try {
|
||||
$r = & $api -Path "/repos/$o/$Name"
|
||||
[PSCustomObject]@{
|
||||
full_name = $r.full_name
|
||||
private = $r.private
|
||||
default_branch = $r.default_branch
|
||||
clone_url = $r.clone_url
|
||||
html_url = $r.html_url
|
||||
ssh_url = $r.ssh_url
|
||||
updated_at = $r.updated_at
|
||||
} | Format-List
|
||||
} catch {
|
||||
if ($_.Exception.Message -match "404") {
|
||||
Write-Host "Repository not found: $o/$Name" -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
throw
|
||||
}
|
||||
}
|
||||
"BySearch" {
|
||||
$term = [uri]::EscapeDataString($Search)
|
||||
$r = & $api -Path "/repos/search?q=$term&limit=50"
|
||||
$r.data | Select-Object full_name, private, default_branch, updated_at | Format-Table -AutoSize
|
||||
}
|
||||
"ListAll" {
|
||||
$r = & $api -Path "/repos/search?limit=50"
|
||||
$r.data | Select-Object full_name, private, default_branch, updated_at | Format-Table -AutoSize
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Low-level Gitea REST API wrapper (curl-based, BOM-safe).
|
||||
|
||||
.DESCRIPTION
|
||||
Mirrors Invoke-GitHubApi.ps1's design: uses curl.exe so JSON bodies are
|
||||
written via --data-binary @file (no PowerShell Set-Content BOM, which
|
||||
Gitea rejects with "invalid character '\u00ef' looking for beginning of
|
||||
value"). Reads GITEA_URL + GITEA_TOKEN from the environment.
|
||||
|
||||
.PARAMETER Method
|
||||
HTTP verb. Default GET.
|
||||
|
||||
.PARAMETER Path
|
||||
Path under /api/v1 (with or without leading slash). Query params allowed.
|
||||
|
||||
.PARAMETER BodyJson
|
||||
JSON body for write verbs. Validated as JSON before sending.
|
||||
|
||||
.PARAMETER Raw
|
||||
Return a pscustomobject with Status, Headers, Body instead of parsed JSON.
|
||||
|
||||
.EXAMPLE
|
||||
.\Invoke-GiteaApi.ps1 -Path "/version"
|
||||
.\Invoke-GiteaApi.ps1 -Path "/user"
|
||||
.\Invoke-GiteaApi.ps1 -Method POST -Path "/user/repos" -BodyJson $json
|
||||
#>
|
||||
param(
|
||||
[ValidateSet("GET", "POST", "PATCH", "PUT", "DELETE")]
|
||||
[string]$Method = "GET",
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
|
||||
[string]$BodyJson,
|
||||
|
||||
[switch]$Raw
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($env:GITEA_TOKEN)) {
|
||||
throw "Set GITEA_TOKEN before calling the Gitea API. Store it in .env.local.ps1 (gitignored)."
|
||||
}
|
||||
if (-not (Get-Command curl.exe -ErrorAction SilentlyContinue)) {
|
||||
throw "curl.exe not found in PATH."
|
||||
}
|
||||
|
||||
$base = if ($env:GITEA_URL) { $env:GITEA_URL.TrimEnd("/") } else { "https://gitea-hjwh0svsoo9p5w5kj2j6b1bd.urieljareth.org" }
|
||||
$base = "$base/api/v1"
|
||||
$cleanPath = $Path.TrimStart("/")
|
||||
$uri = "$base/$cleanPath"
|
||||
|
||||
# Gitea accepts "token <T>" (canonical) and "Bearer <T>" (since 1.16+).
|
||||
$curlArgs = @(
|
||||
"-sS", "-i",
|
||||
"-X", $Method.ToUpper(),
|
||||
"-H", "Authorization: token $env:GITEA_TOKEN",
|
||||
"-H", "Accept: application/json",
|
||||
"-H", "User-Agent: gitea-skill"
|
||||
)
|
||||
$tmpBody = $null
|
||||
if ($PSBoundParameters.ContainsKey("BodyJson")) {
|
||||
try { $null = $BodyJson | ConvertFrom-Json } catch { throw "BodyJson is not valid JSON: $($_.Exception.Message)" }
|
||||
$tmpBody = [System.IO.Path]::GetTempFileName()
|
||||
[System.IO.File]::WriteAllText($tmpBody, $BodyJson, (New-Object System.Text.UTF8Encoding($false)))
|
||||
$curlArgs += @("--data-binary", "@$tmpBody", "-H", "Content-Type: application/json")
|
||||
}
|
||||
|
||||
try {
|
||||
$output = & curl.exe @curlArgs $uri
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) { throw "Gitea API request failed with exit code $exitCode." }
|
||||
|
||||
$rawStr = ($output | Out-String)
|
||||
$parts = $rawStr -split "(?:`r`n`r`n|`n`n)", 2
|
||||
$headersBlock = if ($parts.Count -ge 1) { $parts[0] } else { "" }
|
||||
$bodyBlock = if ($parts.Count -ge 2) { $parts[1] } else { "{}" }
|
||||
|
||||
$statusLine = ($headersBlock -split "`n" | Select-Object -First 1).Trim()
|
||||
if ($statusLine -notmatch " 20[04-9] ") {
|
||||
$snippet = $bodyBlock.Trim()
|
||||
if ($snippet.Length -gt 500) { $snippet = $snippet.Substring(0, 500) + "..." }
|
||||
throw "Gitea API HTTP error: $statusLine`nURI: $uri`nBody: $snippet"
|
||||
}
|
||||
|
||||
if ($Raw) {
|
||||
[pscustomobject]@{ Status = $statusLine; Headers = $headersBlock; Body = $bodyBlock }
|
||||
} else {
|
||||
try { $bodyBlock | ConvertFrom-Json }
|
||||
catch { $bodyBlock }
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($tmpBody -and (Test-Path -LiteralPath $tmpBody)) {
|
||||
Remove-Item -LiteralPath $tmpBody -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Create a Gitea repository (idempotent).
|
||||
|
||||
.DESCRIPTION
|
||||
Mirrors deploy_skill/scripts/New-GitHubRepo.ps1. If the repo already exists,
|
||||
prints its URL and returns the existing object instead of failing.
|
||||
|
||||
.PARAMETER Name
|
||||
Repository name (required).
|
||||
|
||||
.PARAMETER Description
|
||||
Short description.
|
||||
|
||||
.PARAMETER Private
|
||||
Switch. If set, creates a private repo. Default public.
|
||||
|
||||
.PARAMETER Owner
|
||||
Optional. User (default) or org to create under.
|
||||
|
||||
.PARAMETER NoAutoInit
|
||||
Skip the Gitea-side README init (use when you will push an existing local
|
||||
repo with its own history).
|
||||
|
||||
.EXAMPLE
|
||||
.\New-GiteaRepo.ps1 -Name my-app -Description "demo" -Private
|
||||
.\New-GiteaRepo.ps1 -Name my-app -NoAutoInit
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidatePattern('^[A-Za-z0-9._-]+$')]
|
||||
[string]$Name,
|
||||
|
||||
[string]$Description = "",
|
||||
|
||||
[switch]$Private,
|
||||
|
||||
[string]$Owner,
|
||||
|
||||
[switch]$NoAutoInit
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$api = Join-Path $here "Invoke-GiteaApi.ps1"
|
||||
|
||||
$me = & $api -Path "/user"
|
||||
$effectiveOwner = if ($Owner) { $Owner } else { $me.login }
|
||||
Write-Host "Authenticated Gitea user: $($me.login)" -ForegroundColor Cyan
|
||||
|
||||
$existing = $null
|
||||
try {
|
||||
$existing = & $api -Path "/repos/$effectiveOwner/$Name"
|
||||
} catch {
|
||||
if ($_.Exception.Message -notmatch "404") { throw }
|
||||
$existing = $null
|
||||
}
|
||||
if ($existing) {
|
||||
Write-Host "Repository already exists: $($existing.html_url)" -ForegroundColor Yellow
|
||||
[PSCustomObject]@{
|
||||
full_name = $existing.full_name
|
||||
html_url = $existing.html_url
|
||||
clone_url = $existing.clone_url
|
||||
ssh_url = $existing.ssh_url
|
||||
private = $existing.private
|
||||
default_branch = $existing.default_branch
|
||||
} | Format-List
|
||||
return $existing
|
||||
}
|
||||
|
||||
$body = [ordered]@{
|
||||
name = $Name
|
||||
description = $Description
|
||||
private = [bool]$Private
|
||||
auto_init = (-not $NoAutoInit)
|
||||
has_issues = $true
|
||||
has_wiki = $false
|
||||
default_branch = "main"
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
$endpoint = if ($Owner -and $Owner -ne $me.login) {
|
||||
"/orgs/$Owner/repos"
|
||||
} else {
|
||||
"/user/repos"
|
||||
}
|
||||
|
||||
Write-Host "Creating repository at: $endpoint" -ForegroundColor Cyan
|
||||
$created = & $api -Method POST -Path $endpoint -BodyJson $body
|
||||
|
||||
[PSCustomObject]@{
|
||||
full_name = $created.full_name
|
||||
html_url = $created.html_url
|
||||
clone_url = $created.clone_url
|
||||
ssh_url = $created.ssh_url
|
||||
private = $created.private
|
||||
default_branch = $created.default_branch
|
||||
} | Format-List
|
||||
|
||||
$created
|
||||
@@ -0,0 +1,140 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Wire a local git repo to Gitea and push the current branch headlessly.
|
||||
|
||||
.DESCRIPTION
|
||||
1. Ensures the target Gitea repo exists (creates it if -CreateIfMissing).
|
||||
2. Adds (or updates) a git remote pointing at Gitea. Default remote name
|
||||
is 'gitea' so it sits alongside a GitHub 'origin' without conflict.
|
||||
3. Pushes the current branch with a one-shot Authorization header so the
|
||||
Gitea token is NOT persisted into .git/config or wincred.
|
||||
|
||||
The token is injected via `git -c http.extraHeader=...` for the single
|
||||
push invocation only. credential.helper is blanked for that push so the
|
||||
helper does not prompt and does not cache the header.
|
||||
|
||||
.PARAMETER AppPath
|
||||
Path to the local git working copy. Defaults to current directory.
|
||||
|
||||
.PARAMETER Name
|
||||
Repository name on Gitea. Defaults to the basename of AppPath.
|
||||
|
||||
.PARAMETER Owner
|
||||
Owner (user or org). Defaults to the authenticated Gitea user.
|
||||
|
||||
.PARAMETER RemoteName
|
||||
Remote name to add/update. Default 'gitea'.
|
||||
|
||||
.PARAMETER Branch
|
||||
Branch to push. Defaults to the current HEAD of AppPath.
|
||||
|
||||
.PARAMETER CreateIfMissing
|
||||
Create the Gitea repo if it does not exist. If omitted and the repo is
|
||||
missing, the script aborts with a clear message.
|
||||
|
||||
.PARAMETER Private
|
||||
Only used when -CreateIfMissing creates a new repo.
|
||||
|
||||
.PARAMETER Force
|
||||
Skip the interactive confirmation before pushing.
|
||||
|
||||
.EXAMPLE
|
||||
.\Sync-GiteaRemote.ps1 -AppPath .\my-app -CreateIfMissing
|
||||
.\Sync-GiteaRemote.ps1 -Name Proxmox-Coolify-Manager -RemoteName origin -Force
|
||||
#>
|
||||
param(
|
||||
[string]$AppPath = (Get-Location).Path,
|
||||
[string]$Name,
|
||||
[string]$Owner,
|
||||
[string]$RemoteName = "gitea",
|
||||
[string]$Branch,
|
||||
[switch]$CreateIfMissing,
|
||||
[switch]$Private,
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$api = Join-Path $here "Invoke-GiteaApi.ps1"
|
||||
$newRepo = Join-Path $here "New-GiteaRepo.ps1"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($env:GITEA_TOKEN)) {
|
||||
throw "Set GITEA_TOKEN (and GITEA_URL) before running this. Load .\.env.local.ps1."
|
||||
}
|
||||
|
||||
$repoPath = (Resolve-Path -LiteralPath $AppPath).ProviderPath
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $repoPath ".git"))) {
|
||||
throw "Not a git repository: $repoPath"
|
||||
}
|
||||
|
||||
$me = & $api -Path "/user"
|
||||
$effectiveOwner = if ($Owner) { $Owner } else { $me.login }
|
||||
$effectiveName = if ($Name) { $Name } else { (Split-Path -Leaf $repoPath) }
|
||||
|
||||
if (-not $Branch) {
|
||||
$Branch = (& git -C $repoPath rev-parse --abbrev-ref HEAD).Trim()
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($Branch) -or $Branch -eq "HEAD") {
|
||||
throw "Could not determine current branch in $repoPath. Pass -Branch explicitly."
|
||||
}
|
||||
|
||||
Write-Host "[*] repo=$effectiveOwner/$effectiveName branch=$Branch remote=$RemoteName" -ForegroundColor Cyan
|
||||
|
||||
$repo = $null
|
||||
try {
|
||||
$repo = & $api -Path "/repos/$effectiveOwner/$effectiveName"
|
||||
} catch {
|
||||
if ($_.Exception.Message -notmatch "404") { throw }
|
||||
$repo = $null
|
||||
}
|
||||
if (-not $repo) {
|
||||
if (-not $CreateIfMissing) {
|
||||
throw "Gitea repo $effectiveOwner/$effectiveName does not exist. Re-run with -CreateIfMissing, or create it first with New-GiteaRepo.ps1."
|
||||
}
|
||||
Write-Host "[*] Creating missing repo $effectiveOwner/$effectiveName ..." -ForegroundColor Cyan
|
||||
& $newRepo -Name $effectiveName -Owner $effectiveOwner -Private:$Private -NoAutoInit | Out-Null
|
||||
$repo = & $api -Path "/repos/$effectiveOwner/$effectiveName"
|
||||
}
|
||||
|
||||
$giteaHost = if ($env:GITEA_URL) { $env:GITEA_URL.TrimEnd("/") } else { "https://gitea-hjwh0svsoo9p5w5kj2j6b1bd.urieljareth.org" }
|
||||
$remoteUrl = "$giteaHost/$effectiveOwner/$effectiveName.git"
|
||||
|
||||
$existingRemote = $null
|
||||
try {
|
||||
$existingRemote = & git -C $repoPath remote get-url $RemoteName 2>$null
|
||||
} catch { $existingRemote = $null }
|
||||
|
||||
if ($existingRemote) {
|
||||
if ($existingRemote.Trim() -ne $remoteUrl) {
|
||||
Write-Host "[*] Updating remote '$RemoteName' URL -> $remoteUrl" -ForegroundColor Yellow
|
||||
& git -C $repoPath remote set-url $RemoteName $remoteUrl
|
||||
} else {
|
||||
Write-Host "[*] Remote '$RemoteName' already -> $remoteUrl" -ForegroundColor DarkGray
|
||||
}
|
||||
} else {
|
||||
Write-Host "[*] Adding remote '$RemoteName' -> $remoteUrl" -ForegroundColor Cyan
|
||||
& git -C $repoPath remote add $RemoteName $remoteUrl
|
||||
}
|
||||
|
||||
if (-not $Force) {
|
||||
$answer = Read-Host "Push '$Branch' to $RemoteName ($effectiveOwner/$effectiveName)? [y/N]"
|
||||
if ($answer -notmatch '^[yY]') {
|
||||
Write-Host "Aborted (no push)." -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[*] Pushing $Branch -> $RemoteName ..." -ForegroundColor Cyan
|
||||
$env:GIT_TERMINAL_PROMPT = "0"
|
||||
& git -C $repoPath `
|
||||
-c "http.extraHeader=Authorization: token $env:GITEA_TOKEN" `
|
||||
-c "credential.helper=" `
|
||||
push -u $RemoteName $Branch
|
||||
$pushExit = $LASTEXITCODE
|
||||
if ($pushExit -ne 0) {
|
||||
throw "git push failed (exit $pushExit). Check the token scopes (needs 'write:repository')."
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[OK] Pushed to $($repo.html_url)" -ForegroundColor Green
|
||||
Write-Host " remote '$RemoteName' -> $remoteUrl"
|
||||
@@ -0,0 +1,68 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Gitea smoke test: config + API auth + token scopes.
|
||||
|
||||
.DESCRIPTION
|
||||
Calls /version, /user (auth), /settings/api, and lists the authenticated
|
||||
user's repos. Exits 1 on any FAIL so it composes into CI / agent startup.
|
||||
|
||||
.EXAMPLE
|
||||
.\Test-GiteaConnection.ps1
|
||||
#>
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$api = Join-Path $here "Invoke-GiteaApi.ps1"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($env:GITEA_TOKEN)) {
|
||||
Write-Host "[FAIL] GITEA_TOKEN not set. Load .\.env.local.ps1 first." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($env:GITEA_URL)) {
|
||||
Write-Host "[WARN] GITEA_URL not set; using default https://gitea-hjwh0svsoo9p5w5kj2j6b1bd.urieljareth.org" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$failures = 0
|
||||
|
||||
# 1) Version (anonymous, but proves reachability)
|
||||
try {
|
||||
$v = & $api -Path "/version"
|
||||
Write-Host "[PASS] /version -> Gitea $($v.version)" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "[FAIL] /version: $($_.Exception.Message)" -ForegroundColor Red
|
||||
$failures++
|
||||
}
|
||||
|
||||
# 2) Authenticated user (proves token validity)
|
||||
try {
|
||||
$u = & $api -Path "/user"
|
||||
Write-Host "[PASS] /user -> login='$($u.login)' is_admin=$($u.is_admin)" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "[FAIL] /user (token invalid or expired): $($_.Exception.Message)" -ForegroundColor Red
|
||||
$failures++
|
||||
}
|
||||
|
||||
# 3) Token scopes (Gitea 1.20+: /users/me/tokens only lists tokens created via API; for app tokens, /user suffices)
|
||||
try {
|
||||
$settings = & $api -Path "/settings/api"
|
||||
Write-Host "[PASS] /settings/api -> max_response_items=$($settings.max_response_items)" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "[WARN] /settings/api unavailable on this version: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# 4) List repos (proves read scope on /repos/search)
|
||||
try {
|
||||
$repos = & $api -Path "/repos/search?limit=50"
|
||||
$count = @($repos.data).Count
|
||||
Write-Host "[PASS] /repos/search -> $count repos visible" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "[FAIL] /repos/search: $($_.Exception.Message)" -ForegroundColor Red
|
||||
$failures++
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
if ($failures -gt 0) {
|
||||
Write-Host "[RESULT] $failures FAIL(s). Gitea skill NOT ready." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[RESULT] OK. Gitea skill ready." -ForegroundColor Green
|
||||
Reference in New Issue
Block a user