feat(git-control): add Git repository and branch access control features
- Introduced new scripts for managing Gitea repository permissions, including branch protection, user access, and organization-wide settings. - Added documentation for the git-control skill, detailing API usage and capabilities. - Included .gitignore to prevent committing sensitive configuration files. - Created references for Gitea API endpoints related to branch protection and user permissions.
This commit is contained in:
@@ -4,23 +4,26 @@
|
|||||||
|
|
||||||
## 故障场景表
|
## 故障场景表
|
||||||
|
|
||||||
| 场景编号 | 故障场景描述 | 节点 A 状态(MySQL/Keepalived) | 节点 B 状态(MySQL/Keepalived) | 节点 C 状态(Keepalived) | 各节点有效优先级 | VIP 最终归属 | 关键说明 |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
| 场景编号 | 故障场景描述 | 节点 A 状态(MySQL/Keepalived) | 节点 B 状态(MySQL/Keepalived) | 节点 C 状态(Keepalived) | 各节点有效优先级 | VIP 最终归属 | 关键说明 |
|
||||||
| 1 | 初始正常状态 | 正常/正常 | 正常/正常 | 正常 | A=110、B=100、C=40 | Node A | A 优先级最高,成为 Master |
|
| ---- | ---------------------------- | ------------------------- | ------------------------- | ------------------- | ---------------- | --------- | ------------------------------ |
|
||||||
| 2 | A 的 MySQL 停机,Keepalived 正常 | 故障/正常 | 正常/正常 | 正常 | A=60、B=100、C=40 | Node B | A 扣权重后,B 优先级更高接管 VIP |
|
| 1 | 初始正常状态 | 正常/正常 | 正常/正常 | 正常 | A=110、B=100、C=40 | Node A | A 优先级最高,成为 Master |
|
||||||
| 3 | 场景 2 后,A 的 MySQL 恢复 | 恢复/正常 | 正常/正常 | 正常 | A=110、B=100、C=40 | Node A | A 优先级恢复,30 秒后抢占 VIP |
|
| 2 | A 的 MySQL 停机,Keepalived 正常 | 故障/正常 | 正常/正常 | 正常 | A=60、B=100、C=40 | Node B | A 扣权重后,B 优先级更高接管 VIP |
|
||||||
| 4 | A 的 Keepalived 停机 | 正常/故障(离线) | 正常/正常 | 正常 | A=离线、B=100、C=40 | Node B | A 无选举资格,B 接管 |
|
| 3 | 场景 2 后,A 的 MySQL 恢复 | 恢复/正常 | 正常/正常 | 正常 | A=110、B=100、C=40 | Node A | A 优先级恢复,30 秒后抢占 VIP |
|
||||||
| 5 | B 的 MySQL 停机 | 正常/正常 | 故障/正常 | 正常 | A=110、B=50、C=40 | Node A | B 扣权重后,A 仍为最高 |
|
| 4 | A 的 Keepalived 停机 | 正常/故障(离线) | 正常/正常 | 正常 | A=离线、B=100、C=40 | Node B | A 无选举资格,B 接管 |
|
||||||
| 6 | B 的 Keepalived 停机 | 正常/正常 | 正常/故障(离线) | 正常 | A=110、B=离线、C=40 | Node A | B 无选举资格,A 保持 Master |
|
| 5 | B 的 MySQL 停机 | 正常/正常 | 故障/正常 | 正常 | A=110、B=50、C=40 | Node A | B 扣权重后,A 仍为最高 |
|
||||||
| 7 | A、B MySQL 均停机 | 故障/正常 | 故障/正常 | 正常 | A=60、B=50、C=40 | Node A | 无可用 MySQL,但 Keepalived 仍按优先级选举 |
|
| 6 | B 的 Keepalived 停机 | 正常/正常 | 正常/故障(离线) | 正常 | A=110、B=离线、C=40 | Node A | B 无选举资格,A 保持 Master |
|
||||||
| 8 | A MySQL 停机 + B Keepalived 停机 | 故障/正常 | 无意义/故障(离线) | 正常 | A=60、B=离线、C=40 | Node A | B 离线,A 优先级高于 C(但 MySQL 不可用) |
|
| 7 | A、B MySQL 均停机 | 故障/正常 | 故障/正常 | 正常 | A=60、B=50、C=40 | Node A | 无可用 MySQL,但 Keepalived 仍按优先级选举 |
|
||||||
| 9 | A Keepalived 停机 + B MySQL 停机 | 正常/故障(离线) | 故障/正常 | 正常 | A=离线、B=50、C=40 | Node B | A 离线,B 优先级高于 C(但 MySQL 不可用) |
|
| 8 | A MySQL 停机 + B Keepalived 停机 | 故障/正常 | 无意义/故障(离线) | 正常 | A=60、B=离线、C=40 | Node A | B 离线,A 优先级高于 C(但 MySQL 不可用) |
|
||||||
| 10 | A、B Keepalived 均停机 | 正常/故障(离线) | 正常/故障(离线) | 正常 | A=离线、B=离线、C=40 | 无节点绑定 VIP | C 无 VIP 配置,仅参与选举不持有 VIP |
|
| 9 | A Keepalived 停机 + B MySQL 停机 | 正常/故障(离线) | 故障/正常 | 正常 | A=离线、B=50、C=40 | Node B | A 离线,B 优先级高于 C(但 MySQL 不可用) |
|
||||||
| 11 | A MySQL+Keepalived 均停机 | 故障/故障(离线) | 正常/正常 | 正常 | A=离线、B=100、C=40 | Node B | A 完全离线,B 正常接管 |
|
| 10 | A、B Keepalived 均停机 | 正常/故障(离线) | 正常/故障(离线) | 正常 | A=离线、B=离线、C=40 | 无节点绑定 VIP | C 无 VIP 配置,仅参与选举不持有 VIP |
|
||||||
| 12 | B MySQL+Keepalived 均停机 | 正常/正常 | 故障/故障(离线) | 正常 | A=110、B=离线、C=40 | Node A | B 完全离线,A 保持 Master |
|
| 11 | A MySQL+Keepalived 均停机 | 故障/故障(离线) | 正常/正常 | 正常 | A=离线、B=100、C=40 | Node B | A 完全离线,B 正常接管 |
|
||||||
| 13 | 场景 7 后,A MySQL 恢复 | 恢复/正常 | 故障/正常 | 正常 | A=110、B=50、C=40 | Node A | A 优先级恢复最高,接管 VIP |
|
| 12 | B MySQL+Keepalived 均停机 | 正常/正常 | 故障/故障(离线) | 正常 | A=110、B=离线、C=40 | Node A | B 完全离线,A 保持 Master |
|
||||||
| 14 | 场景 7 后,B MySQL 恢复 | 故障/正常 | 恢复/正常 | 正常 | A=60、B=100、C=40 | Node B | B 优先级高于 A,接管 VIP |
|
| 13 | 场景 7 后,A MySQL 恢复 | 恢复/正常 | 故障/正常 | 正常 | A=110、B=50、C=40 | Node A | A 优先级恢复最高,接管 VIP |
|
||||||
| 15 | C Keepalived 停机 | 正常/正常 | 正常/正常 | 故障(离线) | A=110、B=100、C=离线 | Node A | C 仅为仲裁,离线不影响 A/B 选举 |
|
| 14 | 场景 7 后,B MySQL 恢复 | 故障/正常 | 恢复/正常 | 正常 | A=60、B=100、C=40 | Node B | B 优先级高于 A,接管 VIP |
|
||||||
| 16 | A、B MySQL 均停机 + C 停机 | 故障/正常 | 故障/正常 | 故障(离线) | A=60、B=50、C=离线 | Node A | C 离线不影响 A/B 选举 |
|
| 15 | C Keepalived 停机 | 正常/正常 | 正常/正常 | 故障(离线) | A=110、B=100、C=离线 | Node A | C 仅为仲裁,离线不影响 A/B 选举 |
|
||||||
| 17 | 所有节点 Keepalived 均停机 | 正常/故障(离线) | 正常/故障(离线) | 故障(离线) | 全离线 | 无节点绑定 VIP | 无 Keepalived 参与选举,VIP 失联 |
|
| 16 | A、B MySQL 均停机 + C 停机 | 故障/正常 | 故障/正常 | 故障(离线) | A=60、B=50、C=离线 | Node A | C 离线不影响 A/B 选举 |
|
||||||
| 18 | A 恢复但复制异常(check_preempt 失败) | 恢复/正常 | 正常/正常 | 正常 | A=90、B=100、C=40 | Node B | 副库端口正常但 A 本地复制异常,A 降权不抢占 |
|
| 17 | 所有节点 Keepalived 均停机 | 正常/故障(离线) | 正常/故障(离线) | 故障(离线) | 全离线 | 无节点绑定 VIP | 无 Keepalived 参与选举,VIP 失联 |
|
||||||
|
| 18 | A 恢复但复制异常(check_preempt 失败) | 恢复/正常 | 正常/正常 | 正常 | A=90、B=100、C=40 | Node B | 副库端口正常但 A 本地复制异常,A 降权不抢占 |
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Do not commit token - security
|
||||||
|
config.local.json
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"source":"local","sourceType":"local","installedAt":"2026-02-27T00:00:00.000Z"}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
---
|
||||||
|
name: git-control
|
||||||
|
description: Gitea repository and branch access control. Use when managing Git permissions: enable/disable branch protection (push, merge, read), user read/write permissions on repos, organization-wide repo access, listing orgs/repos, or bulk permission changes. Supports Gitea API (e.g. afe.git:3000).
|
||||||
|
---
|
||||||
|
|
||||||
|
# Git Control (Gitea)
|
||||||
|
|
||||||
|
Control Gitea repository and branch permissions via API. Requires Gitea API Token with admin/repo permissions.
|
||||||
|
|
||||||
|
**API Reference**: See `references/gitea_api.md` for endpoints.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Gitea API Token**: Settings → Applications → Generate New Token (repo, admin for org-level)
|
||||||
|
- **Base URL**: Default `http://afe.git:3000` (override with `-GiteaBaseUrl`)
|
||||||
|
|
||||||
|
**Local config**: Scripts auto-load from `config.local.json` in the skill root when ApiToken is not provided. Format:
|
||||||
|
```json
|
||||||
|
{"ApiToken": "your-token", "GiteaBaseUrl": "http://afe.git:3000"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### 1. Branch Protection (Enable/Disable)
|
||||||
|
|
||||||
|
Control who can push or merge to a branch.
|
||||||
|
|
||||||
|
| Action | Script | Effect |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| Lock branch (read-only) | `branch_protect.ps1 -Action lock` | No push, no merge |
|
||||||
|
| Unlock branch | `branch_protect.ps1 -Action unlock` | Remove protection |
|
||||||
|
| Restrict merge only | `branch_protect.ps1 -Action merge-only` | No direct push, PR merge allowed |
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\branch_protect.ps1 -ApiToken "..." -Owner G3SF -Repo g3fo-trade -BranchName uat -Action lock
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. List Orgs & Repos
|
||||||
|
|
||||||
|
| Action | Script |
|
||||||
|
|--------|--------|
|
||||||
|
| List all organizations | `list_orgs_repos.ps1 -Action list-orgs` |
|
||||||
|
| List repos in org | `list_orgs_repos.ps1 -Action list-repos -Org G3SF` |
|
||||||
|
| List all repos (admin) | `list_orgs_repos.ps1 -Action list-all-repos` |
|
||||||
|
|
||||||
|
### 3. User Repo Permissions
|
||||||
|
|
||||||
|
Add, remove, or change a user's permission on a repo.
|
||||||
|
|
||||||
|
| Action | Effect |
|
||||||
|
|--------|--------|
|
||||||
|
| Grant read | User can view only |
|
||||||
|
| Grant write | User can push, create PRs |
|
||||||
|
| Grant admin | User can manage settings |
|
||||||
|
| Revoke | Remove collaborator |
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\user_permission.ps1 -ApiToken "..." -Owner G3SF -Repo g3fo-trade -Username john -Action read
|
||||||
|
.\scripts\user_permission.ps1 -ApiToken "..." -Owner G3SF -Repo g3fo-trade -Username john -Action revoke
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Org-Wide Repo Protection
|
||||||
|
|
||||||
|
Apply branch protection to all repos under an organization.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\org_repos_protect.ps1 -ApiToken "..." -Org G3SF -BranchName uat -Action lock
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Org-Wide User Access
|
||||||
|
|
||||||
|
Revoke or grant a user's collaborator access across all org repos.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\org_user_access.ps1 -ApiToken "..." -Org G3SF -Username john -Action revoke
|
||||||
|
.\scripts\org_user_access.ps1 -ApiToken "..." -Org G3SF -Username john -Action read
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Org repos often use team permissions. For team-based access, use Gitea API team endpoints (see `references/gitea_api.md`).
|
||||||
|
|
||||||
|
## Common Control Functions
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| **Lock UAT for release** | Lock `uat` branch on specified repos before release freeze |
|
||||||
|
| **Unlock for merge** | Temporarily allow merge (add user to merge allowlist) |
|
||||||
|
| **Audit user access** | List repos a user can access (collaborators + org teams) |
|
||||||
|
| **Bulk branch lock** | Lock same branch across multiple repos |
|
||||||
|
| **Read-only maintenance** | Set repo to read-only during maintenance |
|
||||||
|
| **New member onboarding** | Grant read to org repos for new team member |
|
||||||
|
| **Offboarding** | Revoke user from all org repos |
|
||||||
|
|
||||||
|
## Script Parameters (Common)
|
||||||
|
|
||||||
|
- `-ApiToken` or `$env:GITEA_TOKEN`
|
||||||
|
- `-GiteaBaseUrl` (default: http://afe.git:3000)
|
||||||
|
- `-Owner` / `-Org` (organization name)
|
||||||
|
- `-Repo` (repository name)
|
||||||
|
- `-BranchName` (branch pattern, e.g. uat, main)
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- 403: Insufficient permissions (need admin for org ops)
|
||||||
|
- 404: Repo/org/user not found
|
||||||
|
- 422: Validation error (check API compatibility with Gitea version)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Gitea API Reference for Git Control
|
||||||
|
|
||||||
|
Base: `{GiteaBaseUrl}/api/v1`
|
||||||
|
Auth: `Authorization: token {ApiToken}`
|
||||||
|
|
||||||
|
## Branch Protection
|
||||||
|
|
||||||
|
| Action | Method | Path |
|
||||||
|
|--------|--------|------|
|
||||||
|
| List | GET | `/repos/{owner}/{repo}/branch_protections` |
|
||||||
|
| Create | POST | `/repos/{owner}/{repo}/branch_protections` |
|
||||||
|
| Get | GET | `/repos/{owner}/{repo}/branch_protections/{name}` |
|
||||||
|
| Edit | PATCH | `/repos/{owner}/{repo}/branch_protections/{name}` |
|
||||||
|
| Delete | DELETE | `/repos/{owner}/{repo}/branch_protections/{name}` |
|
||||||
|
|
||||||
|
**CreateBranchProtectionOption** (POST body):
|
||||||
|
- `branch_name`: pattern (e.g. `uat`, `main`)
|
||||||
|
- `enable_push`: false = no direct push
|
||||||
|
- `enable_merge_whitelist`: true + empty lists = no one can merge
|
||||||
|
- `merge_whitelist_usernames`: []
|
||||||
|
- `merge_whitelist_teams`: []
|
||||||
|
|
||||||
|
## Organizations & Repositories
|
||||||
|
|
||||||
|
| Action | Method | Path | Note |
|
||||||
|
|--------|--------|------|------|
|
||||||
|
| List all orgs | GET | `/admin/orgs` | Admin only |
|
||||||
|
| List org repos | GET | `/orgs/{org}/repos` | Pagination: page, limit |
|
||||||
|
| List user repos | GET | `/user/repos` | Current user |
|
||||||
|
| Search repos | GET | `/repos/search` | q, page, limit |
|
||||||
|
|
||||||
|
## Collaborators (User Repo Permissions)
|
||||||
|
|
||||||
|
| Action | Method | Path |
|
||||||
|
|--------|--------|------|
|
||||||
|
| List | GET | `/repos/{owner}/{repo}/collaborators` |
|
||||||
|
| Add/Update | PUT | `/repos/{owner}/{repo}/collaborators/{username}` |
|
||||||
|
| Remove | DELETE | `/repos/{owner}/{repo}/collaborators/{username}` |
|
||||||
|
| Check | GET | `/repos/{owner}/{repo}/collaborators/{username}` |
|
||||||
|
|
||||||
|
**PUT body**: `{"permission": "read"|"write"|"admin"}`
|
||||||
|
|
||||||
|
## Organization Teams (Org-level Permissions)
|
||||||
|
|
||||||
|
| Action | Method | Path |
|
||||||
|
|--------|--------|------|
|
||||||
|
| List org teams | GET | `/orgs/{org}/teams` |
|
||||||
|
| List team repos | GET | `/teams/{id}/repos` |
|
||||||
|
| Add repo to team | PUT | `/teams/{id}/repos/{org}/{repo}` |
|
||||||
|
| Remove repo from team | DELETE | `/teams/{id}/repos/{org}/{repo}` |
|
||||||
|
| List team members | GET | `/teams/{id}/members` |
|
||||||
|
| Add member | PUT | `/teams/{id}/members/{username}` |
|
||||||
|
| Remove member | DELETE | `/teams/{id}/members/{username}` |
|
||||||
|
|
||||||
|
Team permission: `permission` = "none"|"read"|"write"|"admin"|"owner"
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Branch protection: lock, unlock, or merge-only
|
||||||
|
# lock = no push, no merge | unlock = remove protection | merge-only = no direct push, PR merge allowed
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ApiToken = $env:GITEA_TOKEN,
|
||||||
|
[string]$GiteaBaseUrl = "http://afe.git:3000",
|
||||||
|
[string]$Owner,
|
||||||
|
[string]$Repo,
|
||||||
|
[string]$BranchName,
|
||||||
|
[ValidateSet("lock", "unlock", "merge-only")]
|
||||||
|
[string]$Action = "lock"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ConfigPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\config.local.json"
|
||||||
|
if (-not $ApiToken -and (Test-Path $ConfigPath)) {
|
||||||
|
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||||
|
$ApiToken = $cfg.ApiToken
|
||||||
|
if ($cfg.GiteaBaseUrl) { $GiteaBaseUrl = $cfg.GiteaBaseUrl }
|
||||||
|
}
|
||||||
|
if (-not $ApiToken) { Write-Host "Error: ApiToken required"; exit 1 }
|
||||||
|
if (-not $Owner -or -not $Repo -or -not $BranchName) { Write-Host "Error: Owner, Repo, BranchName required"; exit 1 }
|
||||||
|
|
||||||
|
$ApiBase = "$GiteaBaseUrl/api/v1"
|
||||||
|
$ReposPath = "repos/$Owner/$Repo"
|
||||||
|
$Headers = @{ "Authorization" = "token $ApiToken"; "Content-Type" = "application/json" }
|
||||||
|
|
||||||
|
# Get existing protections
|
||||||
|
$Existing = @()
|
||||||
|
try {
|
||||||
|
$r = Invoke-RestMethod -Uri "$ApiBase/$ReposPath/branch_protections" -Headers $Headers -Method Get
|
||||||
|
$Existing = if ($r -is [array]) { $r } else { @($r) }
|
||||||
|
} catch {
|
||||||
|
if ($_.Exception.Response.StatusCode -eq 404) { Write-Host "Repo not found"; exit 1 }
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
|
||||||
|
$Match = $Existing | Where-Object { $_.rule_name -eq $BranchName -or $_.branch_name -eq $BranchName } | Select-Object -First 1
|
||||||
|
$RuleName = if ($Match) { if ($Match.rule_name) { $Match.rule_name } else { $BranchName } } else { $BranchName }
|
||||||
|
|
||||||
|
# Delete existing rule
|
||||||
|
if ($Match) {
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Uri "$ApiBase/$ReposPath/branch_protections/$([uri]::EscapeDataString($RuleName))" -Headers $Headers -Method Delete | Out-Null
|
||||||
|
} catch { Write-Host "Warning: Delete failed: $_" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Action -eq "unlock") {
|
||||||
|
Write-Host "Branch $BranchName unlocked (protection removed)"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create new rule
|
||||||
|
$Body = @{
|
||||||
|
branch_name = $BranchName
|
||||||
|
enable_push = if ($Action -eq "merge-only") { $false } else { $false }
|
||||||
|
enable_push_whitelist = $false
|
||||||
|
enable_merge_whitelist = if ($Action -eq "lock") { $true } else { $false }
|
||||||
|
merge_whitelist_usernames = @()
|
||||||
|
merge_whitelist_teams = @()
|
||||||
|
block_on_rejected_reviews = $false
|
||||||
|
block_on_outdated_branch = $false
|
||||||
|
dismiss_stale_approvals = $false
|
||||||
|
require_signed_commits = $false
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Uri "$ApiBase/$ReposPath/branch_protections" -Headers $Headers -Method Post -Body $Body | Out-Null
|
||||||
|
Write-Host "Branch ${BranchName}: $Action applied"
|
||||||
|
} catch {
|
||||||
|
Write-Host "FAIL: $($_.Exception.Message)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# List organizations and repositories
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ApiToken = $env:GITEA_TOKEN,
|
||||||
|
[string]$GiteaBaseUrl = "http://afe.git:3000",
|
||||||
|
[ValidateSet("list-orgs", "list-repos", "list-all-repos")]
|
||||||
|
[string]$Action = "list-orgs",
|
||||||
|
[string]$Org
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ConfigPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\config.local.json"
|
||||||
|
if (-not $ApiToken -and (Test-Path $ConfigPath)) {
|
||||||
|
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||||
|
$ApiToken = $cfg.ApiToken
|
||||||
|
if ($cfg.GiteaBaseUrl) { $GiteaBaseUrl = $cfg.GiteaBaseUrl }
|
||||||
|
}
|
||||||
|
if (-not $ApiToken) { Write-Host "Error: ApiToken required"; exit 1 }
|
||||||
|
|
||||||
|
$ApiBase = "$GiteaBaseUrl/api/v1"
|
||||||
|
$Headers = @{ "Authorization" = "token $ApiToken"; "Content-Type" = "application/json" }
|
||||||
|
|
||||||
|
if ($Action -eq "list-orgs") {
|
||||||
|
try {
|
||||||
|
$orgs = Invoke-RestMethod -Uri "$ApiBase/admin/orgs?page=1&limit=100" -Headers $Headers -Method Get
|
||||||
|
$orgs | ForEach-Object { Write-Host $_.username }
|
||||||
|
} catch {
|
||||||
|
if ($_.Exception.Response.StatusCode -eq 403) { Write-Host "Admin permission required for list-orgs"; exit 1 }
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Action -eq "list-repos") {
|
||||||
|
if (-not $Org) { Write-Host "Error: -Org required for list-repos"; exit 1 }
|
||||||
|
$repos = @()
|
||||||
|
$page = 1
|
||||||
|
do {
|
||||||
|
$r = Invoke-RestMethod -Uri "$ApiBase/orgs/$Org/repos?page=$page&limit=50" -Headers $Headers -Method Get
|
||||||
|
$arr = if ($r -is [array]) { $r } else { @($r) }
|
||||||
|
$repos += $arr
|
||||||
|
$page++
|
||||||
|
} while ($arr.Count -eq 50)
|
||||||
|
foreach ($repo in $repos) { Write-Output $repo.name }
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Action -eq "list-all-repos") {
|
||||||
|
$orgs = Invoke-RestMethod -Uri "$ApiBase/admin/orgs?page=1&limit=100" -Headers $Headers -Method Get
|
||||||
|
foreach ($o in $orgs) {
|
||||||
|
$r = Invoke-RestMethod -Uri "$ApiBase/orgs/$($o.username)/repos?page=1&limit=100" -Headers $Headers -Method Get
|
||||||
|
$r | ForEach-Object { Write-Host "$($o.username)/$($_.name)" }
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Apply branch protection to all repos in an organization
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ApiToken = $env:GITEA_TOKEN,
|
||||||
|
[string]$GiteaBaseUrl = "http://afe.git:3000",
|
||||||
|
[string]$Org,
|
||||||
|
[string]$BranchName,
|
||||||
|
[ValidateSet("lock", "unlock")]
|
||||||
|
[string]$Action = "lock"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ConfigPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\config.local.json"
|
||||||
|
if (-not $ApiToken -and (Test-Path $ConfigPath)) {
|
||||||
|
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||||
|
$ApiToken = $cfg.ApiToken
|
||||||
|
if ($cfg.GiteaBaseUrl) { $GiteaBaseUrl = $cfg.GiteaBaseUrl }
|
||||||
|
}
|
||||||
|
if (-not $ApiToken) { Write-Host "Error: ApiToken required"; exit 1 }
|
||||||
|
if (-not $Org -or -not $BranchName) { Write-Host "Error: Org, BranchName required"; exit 1 }
|
||||||
|
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$BranchScript = Join-Path $ScriptDir "branch_protect.ps1"
|
||||||
|
|
||||||
|
$repos = @(& "$ScriptDir\list_orgs_repos.ps1" -ApiToken $ApiToken -GiteaBaseUrl $GiteaBaseUrl -Action list-repos -Org $Org)
|
||||||
|
if (-not $repos -or $repos.Count -eq 0) { Write-Host "No repos found in org $Org"; exit 0 }
|
||||||
|
|
||||||
|
$count = 0
|
||||||
|
foreach ($r in $repos) {
|
||||||
|
if (-not $r) { continue }
|
||||||
|
Write-Host "Processing $Org/$r..."
|
||||||
|
& $BranchScript -ApiToken $ApiToken -GiteaBaseUrl $GiteaBaseUrl -Owner $Org -Repo $r -BranchName $BranchName -Action $Action
|
||||||
|
$count++
|
||||||
|
}
|
||||||
|
Write-Host "Done. Processed $count repos."
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Revoke or grant a user's access across all repos in an organization
|
||||||
|
# Uses collaborator API - only affects repos where user is direct collaborator
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ApiToken = $env:GITEA_TOKEN,
|
||||||
|
[string]$GiteaBaseUrl = "http://afe.git:3000",
|
||||||
|
[string]$Org,
|
||||||
|
[string]$Username,
|
||||||
|
[ValidateSet("revoke", "read", "write")]
|
||||||
|
[string]$Action = "revoke"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ConfigPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\config.local.json"
|
||||||
|
if (-not $ApiToken -and (Test-Path $ConfigPath)) {
|
||||||
|
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||||
|
$ApiToken = $cfg.ApiToken
|
||||||
|
if ($cfg.GiteaBaseUrl) { $GiteaBaseUrl = $cfg.GiteaBaseUrl }
|
||||||
|
}
|
||||||
|
if (-not $ApiToken) { Write-Host "Error: ApiToken required"; exit 1 }
|
||||||
|
if (-not $Org -or -not $Username) { Write-Host "Error: Org, Username required"; exit 1 }
|
||||||
|
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$Repos = @(& "$ScriptDir\list_orgs_repos.ps1" -ApiToken $ApiToken -GiteaBaseUrl $GiteaBaseUrl -Action list-repos -Org $Org)
|
||||||
|
$UserScript = Join-Path $ScriptDir "user_permission.ps1"
|
||||||
|
|
||||||
|
$count = 0
|
||||||
|
foreach ($Repo in $Repos) {
|
||||||
|
if (-not $Repo) { continue }
|
||||||
|
Write-Host "Processing $Org/$Repo..."
|
||||||
|
$null = & $UserScript -ApiToken $ApiToken -GiteaBaseUrl $GiteaBaseUrl -Owner $Org -Repo $Repo -Username $Username -Action $Action 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { $count++ }
|
||||||
|
}
|
||||||
|
Write-Host "Done. Updated $count repos where $Username was collaborator."
|
||||||
|
Write-Host "Note: Org repos may use team permissions. Check org teams for full control."
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Add, update, or revoke user permission on a repository
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ApiToken = $env:GITEA_TOKEN,
|
||||||
|
[string]$GiteaBaseUrl = "http://afe.git:3000",
|
||||||
|
[string]$Owner,
|
||||||
|
[string]$Repo,
|
||||||
|
[string]$Username,
|
||||||
|
[ValidateSet("read", "write", "admin", "revoke")]
|
||||||
|
[string]$Action = "read"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ConfigPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\config.local.json"
|
||||||
|
if (-not $ApiToken -and (Test-Path $ConfigPath)) {
|
||||||
|
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||||
|
$ApiToken = $cfg.ApiToken
|
||||||
|
if ($cfg.GiteaBaseUrl) { $GiteaBaseUrl = $cfg.GiteaBaseUrl }
|
||||||
|
}
|
||||||
|
if (-not $ApiToken) { Write-Host "Error: ApiToken required"; exit 1 }
|
||||||
|
if (-not $Owner -or -not $Repo -or -not $Username) { Write-Host "Error: Owner, Repo, Username required"; exit 1 }
|
||||||
|
|
||||||
|
$ApiBase = "$GiteaBaseUrl/api/v1"
|
||||||
|
$ReposPath = "repos/$Owner/$Repo"
|
||||||
|
$Headers = @{ "Authorization" = "token $ApiToken"; "Content-Type" = "application/json" }
|
||||||
|
|
||||||
|
if ($Action -eq "revoke") {
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Uri "$ApiBase/$ReposPath/collaborators/$Username" -Headers $Headers -Method Delete
|
||||||
|
Write-Host "Revoked $Username from $Owner/$Repo"
|
||||||
|
} catch {
|
||||||
|
if ($_.Exception.Response.StatusCode -eq 404) { Write-Host "User not a collaborator or repo not found" }
|
||||||
|
else { throw }
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$Body = @{ permission = $Action } | ConvertTo-Json
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Uri "$ApiBase/$ReposPath/collaborators/$Username" -Headers $Headers -Method Put -Body $Body
|
||||||
|
Write-Host "Set $Username to $Action on $Owner/$Repo"
|
||||||
|
} catch {
|
||||||
|
Write-Host "FAIL: $($_.Exception.Message)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user