Add API.md (3200+ lines) with complete REST API documentation covering authentication, repository management, issues, pull requests, organizations, package registry, Actions, and Vault APIs. Includes code examples and error handling. Add GUIDE.md with user-focused documentation for getting started, repository operations, collaboration workflows, and CI/CD setup. Implement documentation tab in repository view with automatic detection and rendering of API.md and GUIDE.md files alongside README. Add locale strings and template for doc tab navigation.
64 KiB
GitCaddy API Reference
Complete technical reference for developers integrating with or using GitCaddy, a self-hosted Git repository management platform.
Table of Contents
- Introduction
- Authentication
- REST API Endpoints
- Git Protocol
- WebSocket APIs
- Frontend JavaScript APIs
- Error Codes
- Rate Limiting
- Webhooks
- Code Examples
Introduction
GitCaddy provides comprehensive REST APIs for repository management, collaboration tools, CI/CD workflows, package registry, and enterprise features. All API endpoints are accessible at:
https://your-gitcaddy-instance.com/api/v1/
Base URL Format:
https://{instance}/api/v1/{endpoint}
Content Type:
All requests and responses use application/json unless otherwise specified.
API Versioning:
Current stable version: v1
Authentication
GitCaddy supports multiple authentication methods for API access.
Personal Access Tokens
Personal access tokens (PATs) provide programmatic access to the GitCaddy API.
Creating a Token:
- Navigate to User Settings → Applications → Manage Access Tokens
- Click "Generate New Token"
- Set token name and scopes
- Copy the generated token (shown only once)
Using Tokens:
Include the token in the Authorization header:
Authorization: token YOUR_ACCESS_TOKEN
Example Request:
curl -H "Authorization: token abc123def456" \
https://gitcaddy.example.com/api/v1/user
Token Scopes:
| Scope | Description |
|---|---|
repo |
Full access to repositories |
repo:status |
Read-only access to repository status |
public_repo |
Access to public repositories only |
admin:org |
Full organization administration |
write:org |
Write access to organizations |
read:org |
Read-only organization access |
admin:public_key |
Manage public SSH keys |
admin:repo_hook |
Manage repository webhooks |
admin:org_hook |
Manage organization webhooks |
notification |
Access notifications |
user |
Full user profile access |
read:user |
Read-only user profile access |
user:email |
Access user email addresses |
delete_repo |
Delete repositories |
package |
Access package registry |
admin:gpg_key |
Manage GPG keys |
admin:application |
Manage OAuth applications |
OAuth2 Authentication
GitCaddy supports OAuth2 for third-party application integration.
OAuth2 Flow:
-
Register Application:
- User Settings → Applications → OAuth2 Applications
- Create new OAuth2 application
- Note
client_idandclient_secret
-
Authorization Request:
GET /login/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&state={state}
- Token Exchange:
POST /login/oauth/access_token
Content-Type: application/json
{
"client_id": "your_client_id",
"client_secret": "your_client_secret",
"code": "authorization_code",
"grant_type": "authorization_code",
"redirect_uri": "https://your-app.com/callback"
}
Response:
{
"access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a",
"token_type": "bearer",
"scope": "repo,user"
}
- Using Access Token:
Authorization: Bearer gho_16C7e42F292c6912E7710c838347Ae178B4a
Supported OAuth2 Providers:
GitCaddy can authenticate users via external OAuth2 providers:
- GitHub
- GitLab
- Bitbucket
- Microsoft Azure AD
- Custom OpenID Connect providers
WebAuthn (Passkeys)
GitCaddy supports WebAuthn for passwordless authentication and two-factor authentication.
Registration API:
// Client-side registration
const response = await fetch('/user/settings/security/webauthn/register', {
method: 'GET',
credentials: 'include'
});
const options = await response.json();
// Create credential
const credential = await navigator.credentials.create({
publicKey: options
});
// Send credential to server
await fetch('/user/settings/security/webauthn/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'My Security Key',
credential: credential
})
});
Authentication API:
// Get authentication challenge
const response = await fetch('/user/webauthn/assertion', {
method: 'GET'
});
const options = await response.json();
// Get credential
const assertion = await navigator.credentials.get({
publicKey: options
});
// Verify assertion
await fetch('/user/webauthn/assertion', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(assertion)
});
REST API Endpoints
Repository APIs
List User Repositories
GET /api/v1/user/repos
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page |
integer | No | Page number (default: 1) |
limit |
integer | No | Page size (default: 10, max: 50) |
type |
string | No | Filter by type: owner, collaborator, member |
Response:
[
{
"id": 1,
"owner": {
"id": 1,
"login": "username",
"full_name": "User Name",
"avatar_url": "https://gitcaddy.example.com/avatars/1"
},
"name": "my-repo",
"full_name": "username/my-repo",
"description": "Repository description",
"private": false,
"fork": false,
"parent": null,
"empty": false,
"mirror": false,
"size": 1024,
"html_url": "https://gitcaddy.example.com/username/my-repo",
"ssh_url": "git@gitcaddy.example.com:username/my-repo.git",
"clone_url": "https://gitcaddy.example.com/username/my-repo.git",
"website": "",
"stars_count": 5,
"forks_count": 2,
"watchers_count": 3,
"open_issues_count": 1,
"default_branch": "main",
"archived": false,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-15T12:30:00Z",
"permissions": {
"admin": true,
"push": true,
"pull": true
}
}
]
Create Repository
POST /api/v1/user/repos
Request Body:
{
"name": "new-repo",
"description": "My new repository",
"private": false,
"auto_init": true,
"gitignores": "Go",
"license": "MIT",
"readme": "Default",
"default_branch": "main"
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Repository name |
description |
string | No | Repository description |
private |
boolean | No | Create as private (default: false) |
auto_init |
boolean | No | Initialize with README (default: false) |
gitignores |
string | No | .gitignore template name |
license |
string | No | License template (MIT, Apache-2.0, GPL-3.0, etc.) |
readme |
string | No | README template |
default_branch |
string | No | Default branch name (default: main) |
trust_model |
string | No | Trust model: default, collaborator, committer, collaboratorcommitter |
Response: 201 Created
{
"id": 2,
"name": "new-repo",
"full_name": "username/new-repo",
"private": false,
"html_url": "https://gitcaddy.example.com/username/new-repo",
"clone_url": "https://gitcaddy.example.com/username/new-repo.git",
"created_at": "2024-01-20T10:00:00Z"
}
Get Repository
GET /api/v1/repos/{owner}/{repo}
Response:
{
"id": 1,
"owner": {
"id": 1,
"login": "username",
"full_name": "User Name"
},
"name": "my-repo",
"full_name": "username/my-repo",
"description": "Repository description",
"private": false,
"default_branch": "main",
"permissions": {
"admin": true,
"push": true,
"pull": true
}
}
Delete Repository
DELETE /api/v1/repos/{owner}/{repo}
Response: 204 No Content
List Repository Branches
GET /api/v1/repos/{owner}/{repo}/branches
Response:
[
{
"name": "main",
"commit": {
"id": "abc123def456",
"message": "Initial commit",
"url": "https://gitcaddy.example.com/username/my-repo/commit/abc123def456"
},
"protected": true,
"user_can_push": true,
"user_can_merge": true
}
]
Create Branch
POST /api/v1/repos/{owner}/{repo}/branches
Request Body:
{
"new_branch_name": "feature-branch",
"old_branch_name": "main"
}
Get Branch Protection
GET /api/v1/repos/{owner}/{repo}/branch_protections/{branch}
Response:
{
"branch_name": "main",
"enable_push": false,
"enable_push_whitelist": true,
"push_whitelist_usernames": ["admin"],
"push_whitelist_teams": ["core-team"],
"push_whitelist_deploy_keys": false,
"enable_merge_whitelist": true,
"merge_whitelist_usernames": ["maintainer"],
"merge_whitelist_teams": ["reviewers"],
"enable_status_check": true,
"status_check_contexts": ["ci/tests", "ci/lint"],
"required_approvals": 2,
"enable_approvals_whitelist": false,
"block_on_rejected_reviews": true,
"dismiss_stale_approvals": true,
"require_signed_commits": true,
"protected_file_patterns": "*.lock",
"unprotected_file_patterns": "docs/*"
}
List Repository Tags
GET /api/v1/repos/{owner}/{repo}/tags
Response:
[
{
"name": "v1.0.0",
"commit": {
"sha": "abc123def456",
"url": "https://gitcaddy.example.com/username/my-repo/commit/abc123def456"
},
"zipball_url": "https://gitcaddy.example.com/username/my-repo/archive/v1.0.0.zip",
"tarball_url": "https://gitcaddy.example.com/username/my-repo/archive/v1.0.0.tar.gz"
}
]
Get File Contents
GET /api/v1/repos/{owner}/{repo}/contents/{filepath}?ref={branch}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
ref |
string | No | Branch, tag, or commit SHA (default: default branch) |
Response:
{
"name": "README.md",
"path": "README.md",
"sha": "abc123",
"type": "file",
"size": 1024,
"encoding": "base64",
"content": "IyBNeVJlcG8KClRoaXMgaXMgYSBSRUFETUUgZmlsZS4=",
"target": null,
"url": "https://gitcaddy.example.com/api/v1/repos/username/my-repo/contents/README.md",
"html_url": "https://gitcaddy.example.com/username/my-repo/src/branch/main/README.md",
"git_url": "https://gitcaddy.example.com/api/v1/repos/username/my-repo/git/blobs/abc123",
"download_url": "https://gitcaddy.example.com/username/my-repo/raw/branch/main/README.md"
}
Create/Update File
POST /api/v1/repos/{owner}/{repo}/contents/{filepath}
Request Body:
{
"content": "base64_encoded_content",
"message": "Create README.md",
"branch": "main",
"sha": "abc123def456",
"author": {
"name": "Author Name",
"email": "author@example.com"
},
"committer": {
"name": "Committer Name",
"email": "committer@example.com"
},
"dates": {
"author": "2024-01-20T10:00:00Z",
"committer": "2024-01-20T10:00:00Z"
},
"signoff": false,
"new_branch": "feature-branch"
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
content |
string | Yes | Base64-encoded file content |
message |
string | Yes | Commit message |
branch |
string | No | Branch name (default: default branch) |
sha |
string | No | Blob SHA for update (required when updating) |
new_branch |
string | No | Create new branch for commit |
Delete File
DELETE /api/v1/repos/{owner}/{repo}/contents/{filepath}
Request Body:
{
"message": "Delete file",
"sha": "abc123def456",
"branch": "main"
}
Fork Repository
POST /api/v1/repos/{owner}/{repo}/forks
Request Body:
{
"organization": "my-org",
"name": "forked-repo"
}
List Forks
GET /api/v1/repos/{owner}/{repo}/forks
Mirror Repository
POST /api/v1/repos/migrate
Request Body:
{
"clone_addr": "https://github.com/user/repo.git",
"auth_username": "username",
"auth_password": "password_or_token",
"uid": 1,
"repo_name": "mirrored-repo",
"mirror": true,
"private": false,
"description": "Mirrored repository"
}
Issue APIs
List Repository Issues
GET /api/v1/repos/{owner}/{repo}/issues
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
state |
string | No | Filter by state: open, closed, all (default: open) |
labels |
string | No | Comma-separated label IDs or names |
q |
string | No | Search query |
type |
string | No | Filter by type: issues, pulls |
milestones |
string | No | Comma-separated milestone names |
since |
string | No | Only issues updated after this time (ISO 8601) |
before |
string | No | Only issues updated before this time (ISO 8601) |
created_by |
string | No | Filter by creator username |
assigned_by |
string | No | Filter by assignee username |
mentioned_by |
string | No | Filter by mentioned username |
page |
integer | No | Page number |
limit |
integer | No | Page size |
Response:
[
{
"id": 1,
"url": "https://gitcaddy.example.com/api/v1/repos/username/my-repo/issues/1",
"html_url": "https://gitcaddy.example.com/username/my-repo/issues/1",
"number": 1,
"user": {
"id": 1,
"login": "username",
"full_name": "User Name"
},
"title": "Bug: Application crashes on startup",
"body": "Detailed description of the issue...",
"labels": [
{
"id": 1,
"name": "bug",
"color": "#ee0701",
"description": "Something isn't working"
}
],
"milestone": {
"id": 1,
"title": "v1.0",
"description": "First release",
"state": "open",
"due_on": "2024-12-31T23:59:59Z"
},
"assignees": [
{
"id": 2,
"login": "developer"
}
],
"state": "open",
"is_locked": false,
"comments": 3,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-16T14:30:00Z",
"closed_at": null,
"due_date": null,
"pull_request": null
}
]
Create Issue
POST /api/v1/repos/{owner}/{repo}/issues
Request Body:
{
"title": "New issue title",
"body": "Issue description with **markdown** support",
"assignees": ["developer1", "developer2"],
"labels": [1, 2],
"milestone": 1,
"closed": false,
"due_date": "2024-12-31T23:59:59Z",
"ref": "main"
}
Response: 201 Created
Get Issue
GET /api/v1/repos/{owner}/{repo}/issues/{index}
Update Issue
PATCH /api/v1/repos/{owner}/{repo}/issues/{index}
Request Body:
{
"title": "Updated title",
"body": "Updated description",
"state": "closed",
"assignees": ["user1"],
"labels": [1, 3],
"milestone": 2,
"due_date": "2024-12-31T23:59:59Z",
"unset_due_date": false
}
List Issue Comments
GET /api/v1/repos/{owner}/{repo}/issues/{index}/comments
Response:
[
{
"id": 1,
"html_url": "https://gitcaddy.example.com/username/my-repo/issues/1#issuecomment-1",
"pull_request_url": "",
"issue_url": "https://gitcaddy.example.com/api/v1/repos/username/my-repo/issues/1",
"user": {
"id": 1,
"login": "username"
},
"original_author": "",
"original_author_id": 0,
"body": "This is a comment on the issue",
"created_at": "2024-01-15T11:00:00Z",
"updated_at": "2024-01-15T11:00:00Z"
}
]
Create Issue Comment
POST /api/v1/repos/{owner}/{repo}/issues/{index}/comments
Request Body:
{
"body": "Comment text with **markdown** support"
}
Edit Issue Comment
PATCH /api/v1/repos/{owner}/{repo}/issues/comments/{id}
Delete Issue Comment
DELETE /api/v1/repos/{owner}/{repo}/issues/comments/{id}
List Issue Labels
GET /api/v1/repos/{owner}/{repo}/labels
Response:
[
{
"id": 1,
"name": "bug",
"color": "#ee0701",
"description": "Something isn't working",
"url": "https://gitcaddy.example.com/api/v1/repos/username/my-repo/labels/1"
}
]
Create Label
POST /api/v1/repos/{owner}/{repo}/labels
Request Body:
{
"name": "enhancement",
"color": "#a2eeef",
"description": "New feature or request"
}
Track Time on Issue
POST /api/v1/repos/{owner}/{repo}/issues/{index}/times
Request Body:
{
"time": 3600,
"created": "2024-01-15T10:00:00Z",
"user_name": "username"
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
time |
integer | Yes | Time in seconds |
created |
string | No | Timestamp (ISO 8601) |
user_name |
string | No | Username (admin only) |
List Tracked Times
GET /api/v1/repos/{owner}/{repo}/issues/{index}/times
Pull Request APIs
List Pull Requests
GET /api/v1/repos/{owner}/{repo}/pulls
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
state |
string | No | Filter by state: open, closed, all |
sort |
string | No | Sort by: oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority |
milestone |
integer | No | Filter by milestone ID |
labels |
string | No | Comma-separated label IDs |
page |
integer | No | Page number |
limit |
integer | No | Page size |
Response:
[
{
"id": 1,
"url": "https://gitcaddy.example.com/api/v1/repos/username/my-repo/pulls/1",
"number": 1,
"user": {
"id": 1,
"login": "contributor"
},
"title": "Add new feature",
"body": "This PR adds a new feature...",
"labels": [],
"milestone": null,
"assignees": [],
"state": "open",
"is_locked": false,
"comments": 2,
"html_url": "https://gitcaddy.example.com/username/my-repo/pulls/1",
"diff_url": "https://gitcaddy.example.com/username/my-repo/pulls/1.diff",
"patch_url": "https://gitcaddy.example.com/username/my-repo/pulls/1.patch",
"mergeable": true,
"merged": false,
"merged_at": null,
"merge_commit_sha": null,
"merged_by": null,
"base": {
"label": "main",
"ref": "main",
"sha": "abc123",
"repo_id": 1,
"repo": {
"id": 1,
"name": "my-repo",
"full_name": "username/my-repo"
}
},
"head": {
"label": "contributor:feature-branch",
"ref": "feature-branch",
"sha": "def456",
"repo_id": 2,
"repo": {
"id": 2,
"name": "my-repo",
"full_name": "contributor/my-repo"
}
},
"merge_base": "abc123",
"due_date": null,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-16T14:30:00Z",
"closed_at": null
}
]
Create Pull Request
POST /api/v1/repos/{owner}/{repo}/pulls
Request Body:
{
"title": "Add new feature",
"body": "Detailed description of changes",
"head": "feature-branch",
"base": "main",
"assignees": ["reviewer1"],
"labels": [1],
"milestone": 1,
"due_date": "2024-12-31T23:59:59Z"
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | PR title |
body |
string | No | PR description |
head |
string | Yes | Branch containing changes |
base |
string | Yes | Branch to merge into |
assignees |
array | No | Array of usernames |
labels |
array | No | Array of label IDs |
milestone |
integer | No | Milestone ID |
Get Pull Request
GET /api/v1/repos/{owner}/{repo}/pulls/{index}
Update Pull Request
PATCH /api/v1/repos/{owner}/{repo}/pulls/{index}
Request Body:
{
"title": "Updated title",
"body": "Updated description",
"base": "main",
"assignees": ["reviewer1", "reviewer2"],
"labels": [1, 2],
"milestone": 1,
"state": "closed",
"due_date": "2024-12-31T23:59:59Z"
}
Merge Pull Request
POST /api/v1/repos/{owner}/{repo}/pulls/{index}/merge
Request Body:
{
"Do": "merge",
"MergeMessageField": "Merge pull request #1",
"MergeTitleField": "Add new feature",
"delete_branch_after_merge": true,
"force_merge": false,
"head_commit_id": "def456",
"merge_when_checks_succeed": false
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
Do |
string | Yes | Merge method: merge, rebase, rebase-merge, squash, manually-merged |
MergeMessageField |
string | No | Merge commit message |
MergeTitleField |
string | No | Merge commit title |
delete_branch_after_merge |
boolean | No | Delete head branch after merge |
force_merge |
boolean | No | Force merge even if checks fail |
head_commit_id |
string | No | Expected head commit SHA |
List PR Reviews
GET /api/v1/repos/{owner}/{repo}/pulls/{index}/reviews
Response:
[
{
"id": 1,
"user": {
"id": 2,
"login": "reviewer"
},
"body": "Looks good to me!",
"commit_id": "def456",
"state": "APPROVED",
"html_url": "https://gitcaddy.example.com/username/my-repo/pulls/1#pullrequestreview-1",
"submitted_at": "2024-01-16T10:00:00Z"
}
]
Create PR Review
POST /api/v1/repos/{owner}/{repo}/pulls/{index}/reviews
Request Body:
{
"body": "Review comment",
"event": "APPROVED",
"comments": [
{
"path": "src/main.go",
"body": "Consider refactoring this function",
"old_position": 0,
"new_position": 42
}
]
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
body |
string | No | Overall review comment |
event |
string | Yes | Review state: APPROVED, REQUEST_CHANGES, COMMENT |
comments |
array | No | Array of line comments |
Submit PR Review
POST /api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{id}
Dismiss PR Review
POST /api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals
Request Body:
{
"message": "Outdated review"
}
Request AI Code Review
POST /api/v1/repos/{owner}/{repo}/pulls/{index}/ai-review
Request Body:
{
"provider": "openai",
"model": "gpt-4"
}
Response:
{
"review_id": 42,
"status": "pending",
"message": "AI review requested. Results will be posted as comments."
}
Organization APIs
List Organizations
GET /api/v1/orgs
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page |
integer | No | Page number |
limit |
integer | No | Page size |
Get Organization
GET /api/v1/orgs/{org}
Response:
{
"id": 1,
"username": "my-org",
"full_name": "My Organization",
"avatar_url": "https://gitcaddy.example.com/avatars/1",
"description": "Organization description",
"website": "https://example.com",
"location": "San Francisco, CA",
"visibility": "public",
"repo_admin_change_team_access": false,
"username": "my-org"
}
Create Organization
POST /api/v1/orgs
Request Body:
{
"username": "new-org",
"full_name": "New Organization",
"description": "Organization description",
"website": "https://example.com",
"location": "San Francisco, CA",
"visibility": "public",
"repo_admin_change_team_access": false
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
username |
string | Yes | Organization username |
full_name |
string | No | Display name |
description |
string | No | Organization description |
website |
string | No | Website URL |
location |
string | No | Location |
visibility |
string | No | Visibility: public, limited, private |
Edit Organization
PATCH /api/v1/orgs/{org}
Delete Organization
DELETE /api/v1/orgs/{org}
List Organization Teams
GET /api/v1/orgs/{org}/teams
Response:
[
{
"id": 1,
"name": "core-team",
"description": "Core development team",
"organization": {
"id": 1,
"username": "my-org",
"full_name": "My Organization"
},
"permission": "admin",
"can_create_org_repo": true,
"includes_all_repositories": false,
"units": [
"repo.code",
"repo.issues",
"repo.pulls",
"repo.releases",
"repo.wiki"
]
}
]
Create Team
POST /api/v1/orgs/{org}/teams
Request Body:
{
"name": "new-team",
"description": "Team description",
"permission": "write",
"can_create_org_repo": false,
"includes_all_repositories": false,
"units": [
"repo.code",
"repo.issues",
"repo.pulls"
]
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Team name |
description |
string | No | Team description |
permission |
string | No | Permission level: read, write, admin |
units |
array | No | Repository unit permissions |
List Team Members
GET /api/v1/teams/{id}/members
Add Team Member
PUT /api/v1/teams/{id}/members/{username}
Remove Team Member
DELETE /api/v1/teams/{id}/members/{username}
List Team Repositories
GET /api/v1/teams/{id}/repos
Add Team Repository
PUT /api/v1/teams/{id}/repos/{org}/{repo}
User APIs
Get Authenticated User
GET /api/v1/user
Response:
{
"id": 1,
"login": "username",
"full_name": "User Name",
"email": "user@example.com",
"avatar_url": "https://gitcaddy.example.com/avatars/1",
"language": "en-US",
"is_admin": false,
"last_login": "2024-01-20T10:00:00Z",
"created": "2023-01-01T00:00:00Z",
"restricted": false,
"active": true,
"prohibit_login": false,
"location": "San Francisco",
"website": "https://example.com",
"description": "User bio",
"visibility": "public",
"followers_count": 10,
"following_count": 5,
"starred_repos_count": 20
}
Get User
GET /api/v1/users/{username}
List User Emails
GET /api/v1/user/emails
Response:
[
{
"email": "user@example.com",
"verified": true,
"primary": true
},
{
"email": "alternate@example.com",
"verified": false,
"primary": false
}
]
Add Email
POST /api/v1/user/emails
Request Body:
{
"emails": ["new@example.com"]
}
Delete Email
DELETE /api/v1/user/emails
Request Body:
{
"emails": ["old@example.com"]
}
List SSH Keys
GET /api/v1/user/keys
Response:
[
{
"id": 1,
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...",
"url": "https://gitcaddy.example.com/api/v1/user/keys/1",
"title": "Work Laptop",
"fingerprint": "SHA256:abc123def456",
"created_at": "2024-01-01T00:00:00Z",
"read_only": false
}
]
Add SSH Key
POST /api/v1/user/keys
Request Body:
{
"title": "My Key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...",
"read_only": false
}
Delete SSH Key
DELETE /api/v1/user/keys/{id}
List GPG Keys
GET /api/v1/user/gpg_keys
Response:
[
{
"id": 1,
"primary_key_id": "",
"key_id": "ABC123DEF456",
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
"emails": [
{
"email": "user@example.com",
"verified": true
}
],
"subkeys": [],
"can_sign": true,
"can_encrypt_comms": true,
"can_encrypt_storage": true,
"can_certify": true,
"created_at": "2024-01-01T00:00:00Z",
"expires_at": "2025-01-01T00:00:00Z"
}
]
Add GPG Key
POST /api/v1/user/gpg_keys
Request Body:
{
"armored_public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..."
}
List Starred Repositories
GET /api/v1/user/starred
Star Repository
PUT /api/v1/user/starred/{owner}/{repo}
Unstar Repository
DELETE /api/v1/user/starred/{owner}/{repo}
List Watched Repositories
GET /api/v1/user/subscriptions
Watch Repository
PUT /api/v1/repos/{owner}/{repo}/subscription
Unwatch Repository
DELETE /api/v1/repos/{owner}/{repo}/subscription
Package Registry APIs
GitCaddy supports multiple package formats with dedicated API endpoints.
List Packages
GET /api/v1/packages/{owner}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
type |
string | No | Package type: npm, maven, docker, pypi, alpine, etc. |
q |
string | No | Search query |
page |
integer | No | Page number |
limit |
integer | No | Page size |
Response:
[
{
"id": 1,
"name": "my-package",
"version": "1.0.0",
"type": "npm",
"owner": {
"id": 1,
"login": "username"
},
"repository": {
"id": 1,
"name": "my-repo",
"full_name": "username/my-repo"
},
"creator": {
"id": 1,
"login": "username"
},
"html_url": "https://gitcaddy.example.com/username/-/packages/npm/my-package/1.0.0",
"created_at": "2024-01-15T10:00:00Z"
}
]
Get Package
GET /api/v1/packages/{owner}/{type}/{name}/{version}
Delete Package
DELETE /api/v1/packages/{owner}/{type}/{name}/{version}
NPM Package API
Configure npm registry:
npm config set registry https://gitcaddy.example.com/api/packages/{owner}/npm/
npm config set //gitcaddy.example.com/api/packages/{owner}/npm/:_authToken {token}
Publish package:
npm publish
Install package:
npm install @{owner}/{package}
Docker Registry API
Login:
docker login gitcaddy.example.com
Tag and push:
docker tag myimage gitcaddy.example.com/{owner}/{image}:{tag}
docker push gitcaddy.example.com/{owner}/{image}:{tag}
Pull:
docker pull gitcaddy.example.com/{owner}/{image}:{tag}
Maven Package API
Configure in pom.xml:
<repositories>
<repository>
<id>gitcaddy</id>
<url>https://gitcaddy.example.com/api/packages/{owner}/maven</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>gitcaddy</id>
<url>https://gitcaddy.example.com/api/packages/{owner}/maven</url>
</repository>
</distributionManagement>
Configure authentication in ~/.m2/settings.xml:
<servers>
<server>
<id>gitcaddy</id>
<username>{username}</username>
<password>{token}</password>
</server>
</servers>
PyPI Package API
Configure pip:
pip config set global.index-url https://gitcaddy.example.com/api/packages/{owner}/pypi/simple
Upload with twine:
twine upload --repository-url https://gitcaddy.example.com/api/packages/{owner}/pypi dist/*
Actions APIs
List Workflow Runs
GET /api/v1/repos/{owner}/{repo}/actions/runs
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
status |
string | No | Filter by status: success, failure, waiting, running |
event |
string | No | Filter by event: push, pull_request, schedule |
actor |
string | No | Filter by actor username |
branch |
string | No | Filter by branch |
page |
integer | No | Page number |
limit |
integer | No | Page size |
Response:
{
"total_count": 10,
"workflow_runs": [
{
"id": 1,
"name": "CI",
"head_branch": "main",
"head_sha": "abc123def456",
"run_number": 42,
"event": "push",
"status": "completed",
"conclusion": "success",
"workflow_id": 1,
"url": "https://gitcaddy.example.com/api/v1/repos/username/my-repo/actions/runs/1",
"html_url": "https://gitcaddy.example.com/username/my-repo/actions/runs/1",
"created_at": "2024-01-20T10:00:00Z",
"updated_at": "2024-01-20T10:05:00Z",
"run_started_at": "2024-01-20T10:00:30Z"
}
]
}
Get Workflow Run
GET /api/v1/repos/{owner}/{repo}/actions/runs/{run_id}
Cancel Workflow Run
POST /api/v1/repos/{owner}/{repo}/actions/runs/{run_id}/cancel
Rerun Workflow
POST /api/v1/repos/{owner}/{repo}/actions/runs/{run_id}/rerun
List Workflow Jobs
GET /api/v1/repos/{owner}/{repo}/actions/runs/{run_id}/jobs
Response:
{
"total_count": 3,
"jobs": [
{
"id": 1,
"run_id": 1,
"name": "build",
"status": "completed",
"conclusion": "success",
"started_at": "2024-01-20T10:00:30Z",
"completed_at": "2024-01-20T10:03:00Z",
"url": "https://gitcaddy.example.com/api/v1/repos/username/my-repo/actions/jobs/1",
"html_url": "https://gitcaddy.example.com/username/my-repo/actions/runs/1/jobs/1"
}
]
}
Get Job Logs
GET /api/v1/repos/{owner}/{repo}/actions/jobs/{job_id}/logs
Response: Plain text log output
List Repository Secrets
GET /api/v1/repos/{owner}/{repo}/actions/secrets
Response:
{
"total_count": 2,
"secrets": [
{
"name": "API_KEY",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z"
}
]
}
Create/Update Secret
PUT /api/v1/repos/{owner}/{repo}/actions/secrets/{secret_name}
Request Body:
{
"value": "secret_value"
}
Delete Secret
DELETE /api/v1/repos/{owner}/{repo}/actions/secrets/{secret_name}
List Runners
GET /api/v1/repos/{owner}/{repo}/actions/runners
Response:
{
"total_count": 1,
"runners": [
{
"id": 1,
"name": "runner-1",
"os": "linux",
"status": "online",
"busy": false,
"labels": [
{
"id": 1,
"name": "ubuntu-latest",
"type": "read-only"
}
]
}
]
}
Register Runner
POST /api/v1/repos/{owner}/{repo}/actions/runners/registration-token
Response:
{
"token": "RUNNER_REGISTRATION_TOKEN",
"expires_at": "2024-01-20T11:00:00Z"
}
Vault APIs
Enterprise-grade secrets management with encryption and audit logging.
List Vault Secrets
GET /api/v1/repos/{owner}/{repo}/vault/secrets
Response:
{
"total_count": 5,
"secrets": [
{
"id": "secret-id-123",
"name": "database_password",
"description": "Production database password",
"version": 3,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-15T10:00:00Z",
"created_by": {
"id": 1,
"login": "admin"
},
"last_accessed_at": "2024-01-20T09:30:00Z",
"access_count": 42
}
]
}
Create Vault Secret
POST /api/v1/repos/{owner}/{repo}/vault/secrets
Request Body:
{
"name": "api_key",
"description": "External API key",
"value": "secret_value_encrypted",
"metadata": {
"environment": "production",
"service": "payment-gateway"
}
}
Response: 201 Created
{
"id": "secret-id-456",
"name": "api_key",
"version": 1,
"created_at": "2024-01-20T10:00:00Z"
}
Get Vault Secret
GET /api/v1/repos/{owner}/{repo}/vault/secrets/{secret_id}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
version |
integer | No | Specific version (default: latest) |
Response:
{
"id": "secret-id-456",
"name": "api_key",
"description": "External API key",
"value": "decrypted_secret_value",
"version": 1,
"created_at": "2024-01-20T10:00:00Z",
"metadata": {
"environment": "production",
"service": "payment-gateway"
}
}
Update Vault Secret
PUT /api/v1/repos/{owner}/{repo}/vault/secrets/{secret_id}
Request Body:
{
"value": "new_secret_value",
"description": "Updated description"
}
Response:
{
"id": "secret-id-456",
"version": 2,
"updated_at": "2024-01-20T11:00:00Z"
}
Delete Vault Secret
DELETE /api/v1/repos/{owner}/{repo}/vault/secrets/{secret_id}
Response: 204 No Content
List Secret Versions
GET /api/v1/repos/{owner}/{repo}/vault/secrets/{secret_id}/versions
Response:
{
"total_count": 3,
"versions": [
{
"version": 3,
"created_at": "2024-01-20T10:00:00Z",
"created_by": {
"id": 1,
"login": "admin"
}
},
{
"version": 2,
"created_at": "2024-01-15T10:00:00Z",
"created_by": {
"id": 1,
"login": "admin"
}
}
]
}
Get Vault Audit Log
GET /api/v1/repos/{owner}/{repo}/vault/audit
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
secret_id |
string | No | Filter by secret ID |
action |
string | No | Filter by action: create, read, update, delete |
user |
string | No | Filter by username |
since |
string | No | Start date (ISO 8601) |
before |
string | No | End date (ISO 8601) |
page |
integer | No | Page number |
limit |
integer | No | Page size |
Response:
{
"total_count": 100,
"entries": [
{
"id": "audit-log-123",
"secret_id": "secret-id-456",
"secret_name": "api_key",
"action": "read",
"user": {
"id": 2,
"login": "developer"
},
"ip_address": "192.168.1.100",
"user_agent": "GitCaddy-Actions/1.0",
"timestamp": "2024-01-20T10:30:00Z",
"success": true
}
]
}
Generate CI/CD Token
POST /api/v1/repos/{owner}/{repo}/vault/tokens
Request Body:
{
"name": "ci-token",
"secret_ids": ["secret-id-456", "secret-id-789"],
"expires_at": "2024-12-31T23:59:59Z",
"read_only": true
}
Response:
{
"token": "vault_token_abc123def456",
"expires_at": "2024-12-31T23:59:59Z"
}
Git Protocol
GitCaddy supports standard Git protocols for repository operations.
HTTP(S) Protocol
Clone:
git clone https://gitcaddy.example.com/username/repo.git
Authentication:
Use personal access token as password:
git clone https://username:token@gitcaddy.example.com/username/repo.git
Or configure credential helper:
git config --global credential.helper store
git clone https://gitcaddy.example.com/username/repo.git
# Enter username and token when prompted
SSH Protocol
Clone:
git clone git@gitcaddy.example.com:username/repo.git
Setup:
- Generate SSH key:
ssh-keygen -t ed25519 -C "your_email@example.com"
-
Add public key to GitCaddy:
- User Settings → SSH/GPG Keys → Add Key
- Paste contents of
~/.ssh/id_ed25519.pub
-
Configure SSH:
# ~/.ssh/config
Host gitcaddy.example.com
User git
IdentityFile ~/.ssh/id_ed25519
Git LFS
Enable LFS for repository:
git lfs install
git lfs track "*.psd"
git add .gitattributes
git commit -m "Enable LFS for PSD files"
Configuration:
GitCaddy automatically handles LFS objects when pushing/pulling.
LFS API Endpoints:
POST /api/v1/repos/{owner}/{repo}/lfs/objects/batch
Request Body:
{
"operation": "upload",
"transfers": ["basic"],
"objects": [
{
"oid": "abc123def456...",
"size": 1048576
}
]
}
WebSocket APIs
GitCaddy uses WebSocket connections for real-time features.
EventSource (Server-Sent Events)
Time Tracking Updates:
const eventSource = new EventSource('/user/events');
eventSource.addEventListener('timetrack', (event) => {
const data = JSON.parse(event.data);
console.log('Time tracking update:', data);
// Update stopwatch UI
});
eventSource.addEventListener('error', (error) => {
console.error('EventSource error:', error);
});
Notification Updates:
const notificationSource = new EventSource('/api/v1/notifications/events');
notificationSource.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
console.log('New notification:', notification);
// Display notification toast
});
SharedWorker
Shared stopwatch across tabs:
// Initialize shared worker for time tracking
const worker = new SharedWorker('/assets/js/stopwatch-worker.js');
worker.port.start();
worker.port.postMessage({
action: 'start',
issueId: 123
});
worker.port.onmessage = (event) => {
const { action, elapsed } = event.data;
if (action === 'tick') {
updateStopwatchDisplay(elapsed);
}
};
Frontend JavaScript APIs
GitCaddy provides modular JavaScript APIs for frontend functionality.
Core Utilities
DOM Manipulation
import {$, $$} from './modules/utils/dom.js';
// Select single element
const button = $('#submit-button');
// Select multiple elements
const listItems = $$('.list-item');
// Event delegation
$(document).on('click', '.dynamic-button', (event) => {
console.log('Button clicked:', event.target);
});
HTML Escaping
import {htmlEscape} from './modules/utils/html.js';
const userInput = '<script>alert("xss")</script>';
const safe = htmlEscape(userInput);
// Result: <script>alert("xss")</script>
Date/Time Utilities
import {parseISODateTime, formatDatetime} from './modules/utils/time.js';
const date = parseISODateTime('2024-01-20T10:00:00Z');
const formatted = formatDatetime(date, 'short'); // "Jan 20, 2024"
Color Utilities
import {useLightTextOnBackground} from './modules/utils/color.js';
const bgColor = '#ff0000';
const useLight = useLightTextOnBackground(bgColor);
// Returns true if light text should be used on this background
Glob Pattern Matching
import {matchGlobPattern} from './modules/utils/glob.js';
const pattern = '*.js';
const filename = 'script.js';
const matches = matchGlobPattern(pattern, filename); // true
DOM Manipulation
Creating Elements
import {createElementFromHTML} from './modules/utils/dom.js';
const html = '<div class="alert">Message</div>';
const element = createElementFromHTML(html);
document.body.appendChild(element);
Toggle Classes
import {toggleClass} from './modules/utils/dom.js';
const element = $('#my-element');
toggleClass(element, 'active', true); // Add class
toggleClass(element, 'hidden', false); // Remove class
Web Components
Absolute Date Component
<absolute-date date="2024-01-20T10:00:00Z" format="short"></absolute-date>
JavaScript API:
const dateElement = document.querySelector('absolute-date');
dateElement.setAttribute('date', '2024-01-21T12:00:00Z');
Origin URL Component
<origin-url data-url="https://example.com/page"></origin-url>
Displays sanitized origin URL with proper formatting.
Overflow Menu Component
<overflow-menu>
<button class="item">Action 1</button>
<button class="item">Action 2</button>
<button class="item">Action 3</button>
</overflow-menu>
Automatically creates overflow menu for items that don't fit.
Vue Components
File Tree Component
import {createApp} from 'vue';
import FileTreeView from './modules/features/repo-file-tree.js';
const app = createApp({
components: {
FileTreeView
},
data() {
return {
files: [
{name: 'src', type: 'dir', children: [...]},
{name: 'README.md', type: 'file'}
]
};
}
});
app.mount('#file-tree');
Template:
<file-tree-view :files="files" @file-click="handleFileClick"></file-tree-view>
Context Popup Component
import {createContextPopup} from './modules/features/context-popup.js';
const popup = createContextPopup({
items: [
{label: 'Edit', action: () => console.log('Edit')},
{label: 'Delete', action: () => console.log('Delete')}
],
position: {x: event.clientX, y: event.clientY}
});
popup.show();
Tooltip System
import {createTippy} from './modules/features/tippy.js';
// Simple tooltip
createTippy('#my-element', {
content: 'Tooltip text'
});
// HTML content
createTippy('.help-icon', {
content: '<strong>Help:</strong> This is help text',
allowHTML: true
});
// Interactive tooltip
createTippy('.interactive', {
content: document.querySelector('#tooltip-content'),
interactive: true,
trigger: 'click'
});
Toast Notifications
import {showToast} from './modules/features/toast.js';
// Success toast
showToast('Operation completed successfully', 'success');
// Error toast
showToast('An error occurred', 'error');
// Warning toast
showToast('Please review your changes', 'warning');
// Info toast
showToast('New version available', 'info');
// Custom duration
showToast('Auto-closing in 5 seconds', 'info', 5000);
Markdown Rendering
import {renderMarkdown} from './modules/features/markup.js';
const markdown = '# Heading\n\nParagraph with **bold** text.';
const html = await renderMarkdown(markdown);
document.querySelector('#content').innerHTML = html;
Features:
- GitHub-flavored markdown
- Syntax highlighting
- Math rendering (KaTeX)
- Mermaid diagrams
- Task lists
- Emoji support
Mermaid Diagrams
import {renderMermaid} from './modules/features/mermaid.js';
const diagramCode = `
graph TD
A[Start] --> B[Process]
B --> C[End]
`;
await renderMermaid('#diagram-container', diagramCode);
SortableJS Integration
import Sortable from 'sortablejs';
const projectBoard = document.querySelector('#project-board');
Sortable.create(projectBoard, {
animation: 150,
handle: '.card-handle',
onEnd: (event) => {
const cardId = event.item.dataset.cardId;
const newPosition = event.newIndex;
// Update card position via API
fetch(`/api/v1/projects/cards/${cardId}/move`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({position: newPosition})
});
}
});
Error Codes
GitCaddy uses standard HTTP status codes with detailed error responses.
HTTP Status Codes
| Code | Status | Description |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource created successfully |
| 204 | No Content | Request succeeded with no response body |
| 400 | Bad Request | Invalid request parameters |
| 401 | Unauthorized | Authentication required |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource not found |
| 409 | Conflict | Resource conflict (e.g., duplicate name) |
| 422 | Unprocessable Entity | Validation error |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error |
| 503 | Service Unavailable | Service temporarily unavailable |
Error Response Format
{
"message": "Human-readable error message",
"errors": [
{
"field": "email",
"message": "Email is required"
}
],
"url": "https://docs.gitcaddy.example.com/api/errors/validation"
}
Common Error Codes
Authentication Errors:
{
"message": "Unauthorized: Invalid or expired token",
"code": "INVALID_TOKEN"
}
Validation Errors:
{
"message": "Validation failed",
"errors": [
{
"field": "name",
"message": "Repository name already exists"
},
{
"field": "description",
"message": "Description must be less than 500 characters"
}
]
}
Permission Errors:
{
"message": "Forbidden: You do not have permission to perform this action",
"required_permission": "admin"
}
Rate Limit Errors:
{
"message": "API rate limit exceeded",
"limit": 5000,
"remaining": 0,
"reset": "2024-01-20T11:00:00Z"
}
Resource Not Found:
{
"message": "Repository not found",
"resource": "repository",
"id": "username/nonexistent-repo"
}
Rate Limiting
GitCaddy implements rate limiting to prevent abuse.
Rate Limit Headers
All API responses include rate limit information:
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1705750800
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests per hour |
X-RateLimit-Remaining |
Remaining requests in current window |
X-RateLimit-Reset |
Unix timestamp when limit resets |
Rate Limits by Authentication
| Authentication | Requests/Hour |
|---|---|
| Unauthenticated | 60 |
| Authenticated (PAT) | 5,000 |
| OAuth Application | 5,000 |
| Enterprise License | 15,000 |
Handling Rate Limits
async function makeAPIRequest(url) {
const response = await fetch(url, {
headers: {
'Authorization': 'token YOUR_TOKEN'
}
});
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
if (response.status === 429) {
const resetDate = new Date(reset * 1000);
const waitTime = resetDate - new Date();
console.log(`Rate limited. Retry after ${waitTime}ms`);
await new Promise(resolve => setTimeout(resolve, waitTime));
return makeAPIRequest(url); // Retry
}
if (remaining < 100) {
console.warn(`Low rate limit: ${remaining} requests remaining`);
}
return response.json();
}
Webhooks
GitCaddy can send HTTP POST requests to external URLs when events occur.
Creating Webhooks
Via API:
POST /api/v1/repos/{owner}/{repo}/hooks
Request Body:
{
"type": "gitea",
"config": {
"url": "https://example.com/webhook",
"content_type": "json",
"secret": "webhook_secret"
},
"events": [
"push",
"pull_request",
"issues",
"issue_comment",
"release"
],
"active": true
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | Webhook type: gitea, gogs, slack, discord |
config.url |
string | Yes | Webhook URL |
config.content_type |
string | No | Content type: json, form |
config.secret |
string | No | Secret for signature verification |
events |
array | Yes | Events to trigger webhook |
active |
boolean | No | Enable webhook (default: true) |
Webhook Events
| Event | Description |
|---|---|
push |
Git push to repository |
create |
Branch or tag created |
delete |
Branch or tag deleted |
fork |
Repository forked |
issues |
Issue opened, closed, edited |
issue_comment |
Comment on issue |
pull_request |
PR opened, closed, merged, edited |
pull_request_review |
PR review submitted |
pull_request_review_comment |
Comment on PR review |
release |
Release published |
repository |
Repository created, deleted |
wiki |
Wiki page created, edited |
Webhook Payload
Push Event:
{
"ref": "refs/heads/main",
"before": "abc123def456",
"after": "def456ghi789",
"compare_url": "https://gitcaddy.example.com/username/repo/compare/abc123...def456",
"commits": [
{
"id": "def456ghi789",
"message": "Fix bug in authentication",
"url": "https://gitcaddy.example.com/username/repo/commit/def456ghi789",
"author": {
"name": "Developer",
"email": "dev@example.com",
"username": "developer"
},
"committer": {
"name": "Developer",
"email": "dev@example.com",
"username": "developer"
},
"timestamp": "2024-01-20T10:00:00Z",
"added": ["new-file.js"],
"removed": [],
"modified": ["existing-file.js"]
}
],
"repository": {
"id": 1,
"name": "repo",
"full_name": "username/repo",
"html_url": "https://gitcaddy.example.com/username/repo",
"private": false,
"owner": {
"id": 1,
"login": "username",
"full_name": "User Name"
}
},
"pusher": {
"id": 1,
"login": "username",
"full_name": "User Name"
},
"sender": {
"id": 1,
"login": "username",
"full_name": "User Name"
}
}
Pull Request Event:
{
"action": "opened",
"number": 1,
"pull_request": {
"id": 1,
"number": 1,
"user": {
"id": 2,
"login": "contributor"
},
"title": "Add new feature",
"body": "Description of changes",
"state": "open",
"html_url": "https://gitcaddy.example.com/username/repo/pulls/1",
"diff_url": "https://gitcaddy.example.com/username/repo/pulls/1.diff",
"patch_url": "https://gitcaddy.example.com/username/repo/pulls/1.patch",
"merged": false,
"mergeable": true,
"base": {
"label": "main",
"ref": "main",
"sha": "abc123"
},
"head": {
"label": "contributor:feature",
"ref": "feature",
"sha": "def456"
},
"created_at": "2024-01-20T10:00:00Z",
"updated_at": "2024-01-20T10:00:00Z"
},
"repository": {
"id": 1,
"name": "repo",
"full_name": "username/repo"
},
"sender": {
"id": 2,
"login": "contributor"
}
}
Webhook Security
Signature Verification:
GitCaddy signs webhook payloads with HMAC-SHA256 using the configured secret.
X-Gitea-Signature: sha256=abc123def456...
Verification Example (Node.js):
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const calculatedSignature = 'sha256=' + hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(calculatedSignature)
);
}
// Express middleware
app.post('/webhook', (req, res) => {
const signature = req.headers['x-gitea-signature'];
const payload = JSON.stringify(req.body);
if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook
res.status(200).send('OK');
});
Testing Webhooks
Manual Test:
curl -X POST https://gitcaddy.example.com/api/v1/repos/username/repo/hooks/1/test \
-H "Authorization: token YOUR_TOKEN"
View Webhook Deliveries:
GET /api/v1/repos/{owner}/{repo}/hooks/{id}/deliveries
Response:
[
{
"id": 1,
"uuid": "abc-123-def-456",
"url": "https://example.com/webhook",
"request": {
"headers": {
"Content-Type": "application/json",
"X-Gitea-Event": "push",
"X-Gitea-Signature": "sha256=..."
},
"body": "{...}"
},
"response": {
"status": 200,
"headers": {},
"body": "OK"
},
"delivered_at": "2024-01-20T10:00:00Z",
"duration": 0.123
}
]
Code Examples
Complete Repository Workflow
// Create repository
async function createRepository(name, description) {
const response = await fetch('https://gitcaddy.example.com/api/v1/user/repos', {
method: 'POST',
headers: {
'Authorization': 'token YOUR_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
description: description,
private: false,
auto_init: true,
license: 'MIT'
})
});
return response.json();
}
// Add file to repository
async function addFile(owner, repo, path, content, message) {
const encodedContent = btoa(content);
const response = await fetch(
`https://gitcaddy.example.com/api/v1/repos/${owner}/${repo}/contents/${path}`,
{
method: 'POST',
headers: {
'Authorization': 'token YOUR_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: encodedContent,
message: message,
branch: 'main'
})
}
);
return response.json();
}
// Create pull request
async function createPullRequest(owner, repo, title, head, base) {
const response = await fetch(
`https://gitcaddy.example.com/api/v1/repos/${owner}/${repo}/pulls`,
{
method: 'POST',
headers: {
'Authorization': 'token YOUR_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: title,
head: head,
base: base,
body: 'Please review these changes'
})
}
);
return response.json();
}
// Usage
(async () => {
const repo = await createRepository('my-project', 'My awesome project');
console.log('Repository created:', repo.html_url);
const file = await addFile(
'username',
'my-project',
'src/index.js',
'console.log("Hello World");',
'Add index.js'
);
console.log('File added:', file.commit.sha);
const pr = await createPullRequest(
'username',
'my-project',
'Add feature',
'feature-branch',
'main'
);
console.log('Pull request created:', pr.html_url);
})();
Issue Management
import requests
class GitCaddyClient:
def __init__(self, base_url, token):
self.base_url = base_url
self.headers = {
'Authorization': f'token {token}',
'Content-Type': 'application/json'
}
def create_issue(self, owner, repo, title, body, labels=None):
url = f'{self.base_url}/api/v1/repos/{owner}/{repo}/issues'
data = {
'title': title,
'body': body
}
if labels:
data['labels'] = labels
response = requests.post(url, json=data, headers=self.headers)
return response.json()
def add_comment(self, owner, repo, issue_number, comment):
url = f'{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments'
data = {'body': comment}
response = requests.post(url, json=data, headers=self.headers)
return response.json()
def close_issue(self, owner, repo, issue_number):
url = f'{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}'
data = {'state': 'closed'}
response = requests.patch(url, json=data, headers=self.headers)
return response.json()
def track_time(self, owner, repo, issue_number, seconds):
url = f'{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/times'
data = {'time': seconds}
response = requests.post(url, json=data, headers=self.headers)
return response.json()
# Usage
client = GitCaddyClient('https://gitcaddy.example.com', 'YOUR_TOKEN')
# Create issue
issue = client.create_issue(
'username',
'my-repo',
'Bug: Login not working',
'Users cannot log in with correct credentials',
labels=[1, 2] # bug, high-priority
)
print(f'Issue created: #{issue["number"]}')
# Add comment
comment = client.add_comment(
'username',
'my-repo',
issue['number'],
'Investigating the issue...'
)
# Track time (2 hours)
time_entry = client.track_time('username', 'my-repo', issue['number'], 7200)
print(f'Tracked {time_entry["time"]}s')
# Close issue
closed = client.close_issue('username', 'my-repo', issue['number'])
print(f'Issue closed: {closed["state"]}')
CI/CD Integration
# .gitcaddy/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Get secrets from Vault
id: vault
uses: actions/vault-secrets@v1
with:
secrets: |
database_password
api_key
- name: Deploy
if: github.ref == 'refs/heads/main'
run: |
echo "Deploying with API key: ${{ steps.vault.outputs.api_key }}"
npm run deploy
env:
DATABASE_PASSWORD: ${{ steps.vault.outputs.database_password }}
API_KEY: ${{ steps.vault.outputs.api_key }}
Webhook Handler
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
)
type PushEvent struct {
Ref string `json:"ref"`
Before string `json:"before"`
After string `json:"after"`
Repository struct {
Name string `json:"name"`
FullName string `json:"full_name"`
} `json:"repository"`
Pusher struct {
Login string `json:"login"`
} `json:"pusher"`
Commits []struct {
ID string `json:"id"`
Message string `json:"message"`
Author struct {
Name string `json:"name"`
Email string `json:"email"`
} `json:"author"`
} `json:"commits"`
}
func verifySignature(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expectedMAC := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expectedMAC))
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
payload, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
signature := r.Header.Get("X-Gitea-Signature")
if !verifySignature(payload, signature, "webhook_secret") {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
event := r.Header.Get("X-Gitea-Event")
switch event {
case "push":
var pushEvent PushEvent
if err := json.Unmarshal(payload, &pushEvent); err != nil {
http.Error(w, "Error parsing JSON", http.StatusBadRequest)
return
}
log.Printf("Push to %s by %s", pushEvent.Repository.FullName, pushEvent.Pusher.Login)
log.Printf("Commits: %d", len(pushEvent.Commits))
for _, commit := range pushEvent.Commits {
log.Printf(" - %s: %s", commit.ID[:7], commit.Message)
}
case "pull_request":
log.Printf("Pull request event received")
default:
log.Printf("Unknown event: %s", event)
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Webhook processed")
}
func main() {
http.HandleFunc("/webhook", handleWebhook)
log.Println("Webhook server listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Package Publishing
#!/bin/bash
# publish-npm.sh - Publish npm package to GitCaddy registry
GITCADDY_URL="https://gitcaddy.example.com"
OWNER="username"
TOKEN="YOUR_TOKEN"
# Configure npm
npm config set registry "${GITCADDY_URL}/api/packages/${OWNER}/npm/"
npm config set "//${GITCADDY_URL#https://}/api/packages/${OWNER}/npm/:_authToken" "${TOKEN}"
# Update version
npm version patch
# Publish
npm publish
echo "Package published to GitCaddy registry"
# Dockerfile - Build and push Docker image to GitCaddy registry
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# Build and push:
# docker build -t gitcaddy.example.com/username/myapp:latest .
# docker login gitcaddy.example.com
# docker push gitcaddy.example.com/username/myapp:latest
For additional support and documentation, visit the GitCaddy Documentation or join the community forum.