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:
urieljareth
2026-07-19 09:35:52 -06:00
parent f587a8baa2
commit 3b7209dcc1
11 changed files with 823 additions and 0 deletions
+10
View File
@@ -16,3 +16,13 @@ COOLIFY_TOKEN=REPLACE_WITH_COOLIFY_TOKEN
# Generate at https://github.com/settings/tokens (classic 'repo' scope, or # Generate at https://github.com/settings/tokens (classic 'repo' scope, or
# fine-grained with Contents:Read+Write and Metadata:Read). # fine-grained with Contents:Read+Write and Metadata:Read).
GITHUB_TOKEN=REPLACE_WITH_GITHUB_PAT GITHUB_TOKEN=REPLACE_WITH_GITHUB_PAT
# Used by gitea_skill (Invoke-GiteaApi.ps1, New-GiteaRepo.ps1, Sync-GiteaRemote.ps1).
# Self-hosted Gitea behind the Cloudflare tunnel. Generate a token at:
# <GITEA_URL>/user/settings/applications
# Scopes needed: read:repository, write:repository, read:user (and write:admin
# only if you manage other users). git push uses a one-shot http.extraHeader
# injected by Sync-GiteaRemote.ps1 — the token is NOT persisted to .git/config.
GITEA_URL=https://gitea-hjwh0svsoo9p5w5kj2j6b1bd.urieljareth.org
GITEA_USER=urieljareth
GITEA_TOKEN=REPLACE_WITH_GITEA_TOKEN
+7
View File
@@ -61,6 +61,12 @@ Invoke-ProxmoxApi -Path "/version"
# Cloudflare API (requires CLOUDFLARE_API_TOKEN) — full tunnel/DNS control # Cloudflare API (requires CLOUDFLARE_API_TOKEN) — full tunnel/DNS control
.\scripts\Invoke-CloudflareApi.ps1 -Path "/user/tokens/verify" .\scripts\Invoke-CloudflareApi.ps1 -Path "/user/tokens/verify"
# Gitea API (requires GITEA_URL + GITEA_TOKEN) — self-hosted git hosting
.\gitea_skill\scripts\Test-GiteaConnection.ps1
.\gitea_skill\scripts\Get-GiteaRepo.ps1 -List
.\gitea_skill\scripts\Invoke-GiteaApi.ps1 -Path "/user"
.\gitea_skill\scripts\Sync-GiteaRemote.ps1 -AppPath .\my-app -CreateIfMissing -Force
``` ```
For the Coolify API reference: don't read the whole tree. Search For the Coolify API reference: don't read the whole tree. Search
@@ -118,4 +124,5 @@ root cause of "database connection" failures here — check with
- [coolify_skill/SKILL.md](coolify_skill/SKILL.md) + [coolify_skill/TOOLS.md](coolify_skill/TOOLS.md) — Coolify agent operating skill and API reference index. - [coolify_skill/SKILL.md](coolify_skill/SKILL.md) + [coolify_skill/TOOLS.md](coolify_skill/TOOLS.md) — Coolify agent operating skill and API reference index.
- [docs/AGENTS-coolify-apps.md](docs/AGENTS-coolify-apps.md) — compatibility guide for agents **developing** an app to deploy on this Coolify instance (networking, ports, domains/TLS, env, volumes, healthchecks) + pre-deploy checklist. Source this when building a new app, not when operating an existing one. - [docs/AGENTS-coolify-apps.md](docs/AGENTS-coolify-apps.md) — compatibility guide for agents **developing** an app to deploy on this Coolify instance (networking, ports, domains/TLS, env, volumes, healthchecks) + pre-deploy checklist. Source this when building a new app, not when operating an existing one.
- [deploy_skill/SKILL.md](deploy_skill/SKILL.md) + [deploy_skill/TOOLS.md](deploy_skill/TOOLS.md) — deploy pipeline skill (scaffold → push → Coolify API → verify → rollback). - [deploy_skill/SKILL.md](deploy_skill/SKILL.md) + [deploy_skill/TOOLS.md](deploy_skill/TOOLS.md) — deploy pipeline skill (scaffold → push → Coolify API → verify → rollback).
- [gitea_skill/SKILL.md](gitea_skill/SKILL.md) + [gitea_skill/TOOLS.md](gitea_skill/TOOLS.md) — self-hosted Gitea skill: list/create/mirror repos, wire a `gitea` remote, push headlessly via one-shot `http.extraHeader` (token never persisted). Distinct from deploy_skill (which targets Coolify); this manages the git hosting layer.
- `PROXMOX/`, `proxmox-agent/`, `proxmox-skill/` are **legacy pointer folders** — they only redirect to the live docs above. Don't add content there. - `PROXMOX/`, `proxmox-agent/`, `proxmox-skill/` are **legacy pointer folders** — they only redirect to the live docs above. Don't add content there.
+4
View File
@@ -29,6 +29,10 @@ Verificado el 2026-05-30 desde esta maquina:
punta (scaffold Dockerfile/compose → checklist → repo GitHub → push → alta punta (scaffold Dockerfile/compose → checklist → repo GitHub → push → alta
via API → deploy → verificacion → rollback). Necesita `GITHUB_TOKEN` y via API → deploy → verificacion → rollback). Necesita `GITHUB_TOKEN` y
`COOLIFY_TOKEN` en `.env.local.ps1`. `COOLIFY_TOKEN` en `.env.local.ps1`.
- `gitea_skill/`: skill para operar la instancia Gitea self-hosted (crear/listar/
buscar repos, migrar desde GitHub, wire de un remoto `gitea` y push headless
con token inyectado por invocacion — sin persistirlo en `.git/config`).
Necesita `GITEA_URL`, `GITEA_USER`, `GITEA_TOKEN` en `.env.local.ps1`.
- `docs/proxmox-inventory.md`: inventario verificado y notas de arquitectura. - `docs/proxmox-inventory.md`: inventario verificado y notas de arquitectura.
- `docs/runbooks/`: procedimientos concretos para conexion, diagnostico y Coolify. - `docs/runbooks/`: procedimientos concretos para conexion, diagnostico y Coolify.
- `docs/runbooks/nextcloud.md`: recuperacion y fix HTTPS para Nextcloud. - `docs/runbooks/nextcloud.md`: recuperacion y fix HTTPS para Nextcloud.
+125
View File
@@ -0,0 +1,125 @@
---
name: gitea-agent
description: Operate the local self-hosted Gitea instance (gitea-...urieljareth.org) for this Proxmox and Coolify Manager project. Use when Codex needs to create/list/search/mirror Gitea repositories, push a local project to Gitea headlessly (token in extraHeader, no wincred), inspect branches/releases/hooks, or wire a `gitea` remote alongside a GitHub `origin`. Distinct from coolify-deploy (which ships to Coolify) — this skill only manages the git hosting layer on Gitea.
---
# Gitea Agent
Use this project-local skill for Gitea work in this repository. The Gitea
instance lives behind the same Cloudflare tunnel as Coolify and is reachable
only via its public URL.
## Startup routine
1. Load private values: `. .\.env.local.ps1` (provides `GITEA_URL`, `GITEA_USER`,
`GITEA_TOKEN`).
2. Run the smoke test: `.\gitea_skill\scripts\Test-GiteaConnection.ps1`.
3. Only after it passes, perform the requested operation.
## Local context (verified 2026-07-19)
- Gitea URL: `https://gitea-hjwh0svsoo9p5w5kj2j6b1bd.urieljareth.org`
- Gitea version: `1.26.2`
- Authenticated user: `urieljareth` (admin)
- Auth header: `Authorization: token <GITEA_TOKEN>` (canonical Gitea form;
`Bearer` also works on 1.16+).
- The Gitea instance runs as a Coolify container inside LXC `102` on the
Proxmox host `192.168.0.200`. Container-level work goes through
`pct exec 102 -- docker ...` (see `coolify_skill`); this skill speaks only
to the public REST API.
- Secrets live only in `.env.local.ps1` (gitignored). Never write tokens into
Markdown, scripts, or `.git/config`.
## When to use
- "Push this project to Gitea" / "crea un repo en Gitea y sube esto"
- "List my Gitea repos" / "¿qué repos tengo en Gitea?"
- "Mirror this GitHub repo to Gitea" (`/repos/migrate`)
- "Set up a `gitea` remote alongside GitHub `origin`"
- "Create a release / tag on Gitea"
## When NOT to use
- Deploying an app to run on the server → use `deploy_skill/` (Coolify).
- Operating an already-deployed container → use `coolify_skill/`.
- Proxmox host / LXC maintenance → use `agent/`.
## Safety policy
- **Read-only by default**: prefer `Get-GiteaRepo.ps1` and GET endpoints before
any write.
- **Confirm before mutating**: repo creation, visibility changes, branch
protection, deletes, releases, and any `git push` require explicit
confirmation unless `-Force` is supplied.
- **Never write secrets into the repo.** The token is injected per-invocation
via `http.extraHeader` for pushes; it is NOT baked into remote URLs and NOT
stored in wincred by these scripts.
- **No force-push to main** unless the operator explicitly asks.
- **State the rollback path** before destructive actions (delete repo, force
push): once a Gitea repo is deleted the data is gone (no Coolify-side
recycle bin).
## Tooling
See [`TOOLS.md`](TOOLS.md) for the full command reference. Core scripts:
- `scripts/Invoke-GiteaApi.ps1` — low-level REST wrapper (curl-based, BOM-safe).
- `scripts/Test-GiteaConnection.ps1` — smoke test (version + auth + repos).
- `scripts/Get-GiteaRepo.ps1` — list / search / get a repo.
- `scripts/New-GiteaRepo.ps1` — idempotent repo creation.
- `scripts/Sync-GiteaRemote.ps1` — wire a `gitea` remote + push headlessly.
## API reference
See [`references/api-index.md`](references/api-index.md) for the verified
endpoint cheatsheet. Full Swagger at `$GITEA_URL/api/swagger`.
## Common workflows
### Push an existing local repo to Gitea (one shot)
```powershell
. .\.env.local.ps1
.\gitea_skill\scripts\Sync-GiteaRemote.ps1 `
-AppPath .\my-app `
-Name my-app `
-CreateIfMissing -Force
```
This creates `urieljareth/my-app` if missing, adds a `gitea` remote (so it
coexists with a GitHub `origin`), and pushes the current branch with a
one-shot auth header (token not persisted).
### Mirror a GitHub repo into Gitea
```powershell
. .\.env.local.ps1
$body = @{
clone_addr = "https://github.com/urieljarethbusiness-cpu/my-app.git"
repo_owner = "urieljareth"
repo_name = "my-app"
service = "github"
mirror = $true
private = $false
wiki = $false
issues = $false
pull_requests = $false
releases = $true
} | ConvertTo-Json -Compress
.\gitea_skill\scripts\Invoke-GiteaApi.ps1 -Method POST -Path "/repos/migrate" -BodyJson $body
```
### Add a Gitea webhook so Coolify auto-pulls on push
```powershell
$body = @{
type = "gitea"
active = $true
events = @("push")
config = @{ url = "https://coolify.urieljareth.org/webhooks/source.manual_webhook_secret_gitea"; content_type = "json" }
} | ConvertTo-Json -Depth 5 -Compress
.\gitea_skill\scripts\Invoke-GiteaApi.ps1 -Method POST `
-Path "/repos/urieljareth/my-app/hooks" -BodyJson $body
```
(Replace the `manual_webhook_secret_*` segment with the real webhook URL from
the Coolify app's "Webhooks" section.)
+125
View File
@@ -0,0 +1,125 @@
# Gitea — Tooling
All commands assume PowerShell from the project root. Load env first:
```powershell
. .\.env.local.ps1
```
Required env vars (in `.env.local.ps1`, gitignored):
- `GITEA_URL` — base URL, e.g. `https://gitea-hjwh0svsoo9p5w5kj2j6b1bd.urieljareth.org`.
- `GITEA_USER` — auth user (informational; `GITEA_TOKEN` encodes the identity).
- `GITEA_TOKEN` — app token with `read:repository`, `write:repository`,
`read:user` scopes (generate at `$GITEA_URL/user/settings/applications`).
## Smoke test
```powershell
.\gitea_skill\scripts\Test-GiteaConnection.ps1
```
Calls `/version`, `/user`, `/settings/api`, `/repos/search`. Exits 1 on any
FAIL — gate operational work on this.
## Read-only
```powershell
# Who am I?
.\gitea_skill\scripts\Invoke-GiteaApi.ps1 -Path "/user"
# List visible repos
.\gitea_skill\scripts\Get-GiteaRepo.ps1 -List
# Get one repo (owner defaults to auth user)
.\gitea_skill\scripts\Get-GiteaRepo.ps1 -Name Proxmox-Coolify-Manager
.\gitea_skill\scripts\Get-GiteaRepo.ps1 -Owner urieljareth -Name MP-Manager
# Search
.\gitea_skill\scripts\Get-GiteaRepo.ps1 -Search manager
# Branches + tags
.\gitea_skill\scripts\Invoke-GiteaApi.ps1 -Path "/repos/urieljareth/MP-Manager/branches"
.\gitea_skill\scripts\Invoke-GiteaApi.ps1 -Path "/repos/urieljareth/MP-Manager/tags"
```
## Create a repo (idempotent)
```powershell
.\gitea_skill\scripts\New-GiteaRepo.ps1 -Name my-app -Description "demo" [-Private] [-NoAutoInit]
```
If the repo already exists, prints its URL and returns the existing object
(no failure). `-NoAutoInit` skips the Gitea-side README so you can push an
existing local history without conflicts.
## Push a local repo to Gitea (headless, token NOT persisted)
```powershell
.\gitea_skill\scripts\Sync-GiteaRemote.ps1 `
-AppPath .\my-app `
-Name my-app `
-RemoteName gitea `
-CreateIfMissing [-Private] [-Force]
```
What it does:
1. Ensures `urieljareth/<Name>` exists (creates if `-CreateIfMissing`).
2. Adds (or updates) the `gitea` remote with the **clean** HTTPS URL — **no
token in `.git/config`**.
3. Pushes the current branch with a **one-shot** `http.extraHeader=Authorization:
token <T>` and a blanked `credential.helper`. The token never reaches disk.
`-RemoteName gitea` (default) coexists with a GitHub `origin`. To replace an
existing `origin` that already points at Gitea (as in this manager repo),
pass `-RemoteName origin`.
### If you prefer wincred for interactive pushes (one-time)
```powershell
cmdkey /generic:"git:$env:GITEA_URL" /user:"$env:GITEA_USER" /pass:"$env:GITEA_TOKEN"
```
After that, plain `git push` works. The skill scripts do NOT require this —
they work headlessly out of the box.
## Low-level API
```powershell
# GET
.\gitea_skill\scripts\Invoke-GiteaApi.ps1 -Path "/repos/urieljareth/MP-Manager/releases"
# POST with body
$body = @{ name = "v1.0.0"; tag_name = "v1.0.0" } | ConvertTo-Json -Compress
.\gitea_skill\scripts\Invoke-GiteaApi.ps1 -Method POST `
-Path "/repos/urieljareth/MP-Manager/releases" -BodyJson $body
# Raw (status + headers + body string)
.\gitea_skill\scripts\Invoke-GiteaApi.ps1 -Path "/user" -Raw
```
`Invoke-GiteaApi.ps1` uses `curl.exe --data-binary @<tmpfile>` (UTF-8 no BOM)
so Gitea's JSON parser never trips on a BOM — same pattern as
`Invoke-GitHubApi.ps1`.
## Commands that always require confirmation
- Creating a repo (`New-GiteaRepo.ps1` warns if it already exists).
- Any `git push` (`Sync-GiteaRemote.ps1` asks unless `-Force`).
- Any Gitea `POST`/`PATCH`/`DELETE` (repo, release, hook, branch protection).
- Mirror/migrate operations.
- Deleting a repo (no Coolify-side recycle bin — data is gone).
`-Force` skips the interactive prompts. Use only after manually reviewing the plan.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `GITEA_TOKEN not set` | `.env.local.ps1` not loaded | `. .\.env.local.ps1` |
| `401`/`403` on writes | Token lacks `write:repository` | Regenerate token with the right scope at `$GITEA_URL/user/settings/applications` |
| `invalid character '\u00ef'` | Body sent with a BOM | Already handled by `Invoke-GiteaApi.ps1` (uses `[IO.File]::WriteAllText` no-BOM); do not switch to `Set-Content -Encoding utf8` |
| `git push` prompts for credentials | The one-shot header was overridden by a stored helper | The script blanks `credential.helper` for the push; if you configured wincred manually, that's fine too. If a different helper intercepts, run from a clean shell. |
| `404` creating repo under an org | Org does not exist or token lacks access | Create the org first at `$GITEA_URL/org/create` |
| Cloudflare 5xx on Gitea URL | Tunnel routing or the Gitea container is down | `coolify_skill/scripts/Get-CoolifyDockerStatus.ps1 -Filter gitea`; tunnel is dashboard-managed, see `docs/runbooks/cloudflare-tunnel.md` |
+66
View File
@@ -0,0 +1,66 @@
# Gitea API quick reference (v1.26.x)
Base URL: `$GITEA_URL/api/v1` (default `https://gitea-hjwh0svsoo9p5w5kj2j6b1bd.urieljareth.org/api/v1`).
Auth header: `Authorization: token <GITEA_TOKEN>` (also accepts `Bearer` on 1.16+).
Docs (Swagger): `$GITEA_URL/api/swagger` (browsable in a browser).
## Verified 2026-07-19 on this instance
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/version` | GET | Server version (anonymous). Returns `{"version":"1.26.2"}`. |
| `/user` | GET | Authenticated user. Proves token. |
| `/settings/api` | GET | Pagination/limits config. |
| `/repos/search?limit=50` | GET | All repos visible to the token. |
| `/repos/{owner}/{name}` | GET | Single repo (404 if missing or no access). |
| `/user/repos` | POST | Create repo under auth user. |
| `/orgs/{org}/repos` | POST | Create repo under an org. |
| `/repos/{owner}/{name}` | PATCH | Update description, visibility, default branch. |
| `/repos/{owner}/{name}` | DELETE | Delete repo. |
| `/repos/{owner}/{name}/branches` | GET | List branches. |
| `/repos/{owner}/{name}/branch protections` | POST | Branch protection rules. |
| `/repos/{owner}/{name}/releases` | GET/POST | List / create releases. |
| `/repos/{owner}/{name}/tags` | GET | List tags. |
| `/repos/{owner}/{name}/hooks` | GET/POST | Webhooks (e.g. Coolify pull). |
| `/repos/{owner}/{name}/mirror-sync` | POST | Sync a mirror repo. |
| `/repos/migrate` | POST | Migrate/mirror from GitHub/GitLab/etc. |
| `/users/{username}/tokens` | POST | Create a new app token (needs username+password or admin). |
| `/orgs` | GET | List orgs. |
| `/admin/users` | GET | List users (admin only). |
## Token scopes (Gitea 1.20+ fine-grained)
Tokens carry scopes. The current skill token (`GITEA_TOKEN` in `.env.local.ps1`)
needs at minimum: `read:repository`, `write:repository`, `read:user`. Create at
`$GITEA_URL/user/settings/applications` (or via API `/users/{user}/tokens` with
basic auth). Verify scopes with:
```powershell
.\gitea_skill\scripts\Test-GiteaConnection.ps1
```
## Auth header convention
Gitea's canonical header is `Authorization: token <T>` (note: literal `token`,
not `Bearer`). This differs from GitHub/Coolify. `Invoke-GiteaApi.ps1` uses the
canonical form. `Bearer` also works on 1.16+ but stick to `token` for max
compat.
## Git push with token (headless, no wincred)
The token is NOT persisted into `.git/config` by `Sync-GiteaRemote.ps1`. It is
sent via a one-shot `http.extraHeader`:
```powershell
git -c "http.extraHeader=Authorization: token $env:GITEA_TOKEN" `
-c "credential.helper=" `
push -u gitea main
```
If you prefer wincred (so plain `git push` works afterwards), seed it once:
```powershell
cmdkey /generic:"git:$env:GITEA_URL" /user:"$env:GITEA_USER" /pass:"$env:GITEA_TOKEN"
```
But note: the skill scripts do not require this. They work headlessly.
+81
View File
@@ -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
}
}
+98
View File
@@ -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
}
}
+99
View File
@@ -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
+140
View File
@@ -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