2
0

147 Commits

Author SHA1 Message Date
04bdf641c7 ci(pages): optimize vault dependency download in build workflow
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m41s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m44s
Build and Release / Lint (push) Successful in 10m57s
Build and Release / Build Binary (linux/arm64) (push) Failing after 4m53s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 5m15s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m44s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m52s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 8m14s
Replace GOPRIVATE with GOSUMDB=off and download only the specific gitcaddy-vault module instead of all dependencies. This speeds up the build process and avoids unnecessary downloads.
2026-04-26 08:23:22 -04:00
fbf5e968d9 fix(pages): prioritize social preview route over static routes
Some checks failed
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m13s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 7m29s
Build and Release / Lint (push) Successful in 8m33s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 2m33s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 2m34s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 2m52s
Build and Release / Build Binary (linux/arm64) (push) Failing after 5m21s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 9h4m18s
Move social preview image handling before static route checking to prevent catch-all patterns like /*.png from shadowing the auto-generated OG card endpoint.

Also bumps gitcaddy-vault from v1.0.60 to v1.0.61.
2026-04-26 07:51:12 -04:00
5f10865ab7 feat(mirror): add push mirror reset and repo history reset
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m16s
Build and Release / Unit Tests (push) Successful in 11m1s
Build and Release / Lint (push) Successful in 11m29s
Build and Release / Build Binary (linux/arm64) (push) Failing after 8s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 5m5s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m58s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 8m44s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 18m6s
Add functionality to reset push mirrors to a specific commit with force-push and pause auto-sync. Includes individual mirror reset, bulk reset for all mirrors, and full repository history reset. All operations include safety confirmations and update locale files for UI strings.
2026-04-25 05:10:52 -04:00
8774100d25 feat(licenses): add BUSL-1.1, CC-BY-NC-4.0, and CC-BY-ND-4.0
All checks were successful
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m45s
Build and Release / Unit Tests (push) Successful in 5m27s
Build and Release / Lint (push) Successful in 7m49s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Add Business Source License 1.1 and Creative Commons Attribution-NonCommercial 4.0 and Attribution-NoDerivatives 4.0 licenses to the available license templates.
2026-04-25 04:27:06 -04:00
de55903c71 feat(licenses): add support for license bundles
Add license bundle functionality to support licenses that require multiple files (e.g., Community Specification License 1.0 with CONTRIBUTING.md, GOVERNANCE.md, LICENSE.md, and NOTICES.md). Includes the CSL-1.0 bundle and ACL-1.0 license, with corresponding UI updates and translations.
2026-04-25 04:19:58 -04:00
416278c747 fix(pages): allow cross-promotion to private repos with pages
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m54s
Build and Release / Unit Tests (push) Successful in 5m29s
Build and Release / Lint (push) Successful in 7m53s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m43s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 5m3s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m41s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m26s
Build and Release / Build Binary (linux/arm64) (push) Failing after 15m15s
Remove the private repo filter from cross-promotion targets. Landing pages are publicly accessible regardless of the repository's private flag, so private repos with pages enabled should be allowed as cross-promotion targets.
2026-04-24 23:26:16 -04:00
58cf5dd410 fix(pages): strip AI placeholder values from translations
Filter out placeholder strings like "<UNKNOWN>", "<EMPTY>", and "N/A" that AI sometimes returns for empty fields. Only include non-empty fields in the translatable content to prevent placeholders from being generated and saved in translation overlays.
2026-04-24 23:24:01 -04:00
0ab62c2b95 feat(pages): add customizable headlines for value props and features
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m3s
Build and Release / Unit Tests (push) Successful in 11m1s
Build and Release / Lint (push) Successful in 11m18s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m1s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 5m17s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m7s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m36s
Build and Release / Build Binary (linux/arm64) (push) Successful in 10m23s
Add headline and subheadline fields to value propositions and features sections in landing page configuration. This allows users to customize the large section headings independently from the small section labels.
2026-04-24 22:20:38 -04:00
97b534f66b build(ci): replace go mod tidy with go mod download
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m6s
Build and Release / Unit Tests (push) Successful in 11m6s
Build and Release / Lint (push) Successful in 11m18s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m3s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h6m29s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m26s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m45s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 11m23s
Use go mod download instead of go mod tidy in CI workflows to avoid unnecessary modifications to go.mod and go.sum files during builds
2026-04-24 02:44:29 -04:00
eb969b8f82 build(deps): update go toolchain from 1.25.5 to 1.25.9
Some checks failed
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m14s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 6m32s
Build and Release / Lint (push) Successful in 6m44s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m57s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been cancelled
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
2026-04-24 02:27:54 -04:00
b310e8ed18 build(deps): exclude gitea SDK v0.24.0 to pin to v0.22.0
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m4s
Build and Release / Unit Tests (push) Successful in 10m39s
Build and Release / Lint (push) Successful in 11m10s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m38s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m1s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m4s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m53s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m58s
Add exclusion for gitea SDK v0.24.0 in addition to v0.24.1 since the entire v0.24.x series requires go 1.26
2026-04-24 01:46:14 -04:00
e8286de3be fix(deps): exclude gitea SDK v0.24.1 requiring Go 1.26
Some checks failed
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m45s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 6m13s
Build and Release / Lint (push) Successful in 6m24s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m38s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m59s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m46s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m49s
Build and Release / Build Binary (linux/arm64) (push) Failing after 18m45s
Add exclude directive for code.gitea.io/sdk/gitea v0.24.1 to prevent automatic upgrade to version requiring Go 1.26 while project remains on Go 1.25.
2026-04-24 00:54:06 -04:00
4648a021c5 Update build.yml
Some checks failed
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m21s
Build and Release / Unit Tests (push) Successful in 5m10s
Build and Release / Lint (push) Successful in 7m40s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m4s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m40s
2026-04-23 18:08:20 -04:00
1a335e741b fix(ci): download modules before bindata generation
Some checks failed
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m12s
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 10m25s
Build and Release / Lint (push) Successful in 11m51s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 3m10s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m28s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m44s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m26s
Build and Release / Build Binary (linux/arm64) (push) Successful in 9m13s
Add explicit go mod download before make generate to ensure all dependencies (including updated vault) are available for bindata generation.
2026-04-23 17:04:06 -04:00
998337e80d fix(ci): revert to go mod tidy with error suppression
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m50s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m26s
Build and Release / Lint (push) Successful in 10m32s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 3m5s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m55s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m46s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Revert back to go mod tidy but add || true to prevent build failures from tidy errors after vault dependency update.
2026-04-23 16:45:42 -04:00
009566a3ce fix(ci): use go mod download instead of tidy for vault
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m23s
Build and Release / Unit Tests (push) Successful in 10m43s
Build and Release / Lint (push) Successful in 11m47s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 2m26s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 3m44s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 2m17s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 9h4m5s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Replace go mod tidy with go mod download after updating vault dependency to avoid modifying go.sum unnecessarily and speed up the build.
2026-04-23 16:26:12 -04:00
2ca88587d4 chore(ci): upgrade to Go 1.26.2 and remove verbose flag
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m51s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m9s
Build and Release / Lint (push) Successful in 10m43s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 2m9s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m36s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m54s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m22s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Update Go version to 1.26.2 and remove -v flag from go mod tidy commands to reduce log noise.
2026-04-23 16:04:47 -04:00
05f8df8a9e Update build.yml
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m9s
Build and Release / Unit Tests (push) Successful in 11m12s
Build and Release / Lint (push) Successful in 11m36s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m47s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m25s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m55s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m27s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
2026-04-23 15:18:10 -04:00
44b6c62093 refactor(ci): move GOPROXY/GOPRIVATE to inline exports
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m20s
Build and Release / Unit Tests (push) Successful in 10m30s
Build and Release / Lint (push) Successful in 11m44s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 2m6s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m42s
Build and Release / Build Binary (linux/arm64) (push) Failing after 11m27s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 11m32s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 11m37s
Move environment variable overrides from step-level env to inline exports within run scripts for better visibility and consistency across Unix/Windows steps.
2026-04-23 11:33:41 -04:00
4557edfe98 fix(ci): use GOPROXY=direct for vault dependency updates
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m45s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 10m4s
Build and Release / Lint (push) Successful in 10m51s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 2m16s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m7s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h6m58s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m55s
Build and Release / Build Binary (linux/arm64) (push) Successful in 11m44s
Replace git URL rewriting with GOPROXY=direct and GOPRIVATE="" to fetch vault dependency directly from source without proxy, avoiding authentication issues.
2026-04-23 10:55:41 -04:00
3b8cfc0c03 fix(ci): add git auth for vault dependency updates
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m50s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m18s
Build and Release / Lint (push) Successful in 10m30s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m36s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m36s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m11s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Configure git URL rewriting with RELEASE_TOKEN for vault dependency steps to ensure authenticated access to private repository when updating go.mod.
2026-04-23 10:33:09 -04:00
55568524a7 fix(ci): auto-append .0 to Go version for download URL
Some checks failed
Build and Release / Unit Tests (push) Successful in 3m52s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m28s
Build and Release / Lint (push) Successful in 10m43s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m41s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m5s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 11m32s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m21s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Add logic to append .0 patch version when GO_VERSION is in major.minor format (e.g., 1.26 → 1.26.0) since go.dev download URLs require full semver.

Also remove redundant GOPROXY=direct and GOPRIVATE="" overrides in vault dependency steps since these are already set globally.
2026-04-23 10:01:36 -04:00
62f4a3ce37 fix(ci): use Go 1.26 instead of 1.26.2
Some checks failed
Build and Release / Unit Tests (push) Successful in 4m3s
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m38s
Build and Release / Lint (push) Successful in 10m45s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 3m26s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m53s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h9m9s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m56s
Build and Release / Build Binary (linux/arm64) (push) Failing after 16s
Use major.minor version format to automatically pick up latest patch releases.
2026-04-23 02:28:43 -04:00
3c8405a3b2 chore(ci): upgrade Go version to 1.26.2 2026-04-23 02:27:10 -04:00
e105e047a4 feat(mirror): add option to exclude hidden files from push mirrors
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m50s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m30s
Build and Release / Lint (push) Successful in 10m36s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m44s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h7m26s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m9s
Build and Release / Build Binary (linux/arm64) (push) Failing after 17m34s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m34s
Add ExcludeHiddenFiles option to push mirrors to filter out dotfiles and other hidden paths:
- Add exclude_hidden_files column to push_mirror table
- Implement tree filtering using git plumbing (ls-tree, mktree, commit-tree)
- Add UI toggle in repository mirror settings
- Update locale files for all languages

Migration: v370 adds exclude_hidden_files column with default false.

This allows users to mirror repositories while excluding .git, .env, and other sensitive dotfiles from the pushed copy.
2026-04-22 22:30:17 -04:00
f9f2d45c13 feat(pages): add cross-promote translation support
Adds translation support for cross-promote section in landing pages. Users can now provide localized versions of cross-promote headline, subheadline, and navigation label.

Updates TranslationView with CrossPromoteHeadline, CrossPromoteSubheadline, and NavLabelCrossPromote fields. Updates translation parsing and JSON building to handle cross_promote overlay.

Updates buildTranslatableContent to include cross-promote fields in generated translation template with "Related Offerings" default headline.

Completes cross-promote feature by enabling full i18n support alongside the section configuration added in previous commit.
2026-04-19 21:45:30 -04:00
3a62a5d8c1 feat(pages): add cross-promote section to landing pages
Adds CrossPromoteSectionConfig to landing page configuration, allowing repositories to showcase related projects/products. Section includes customizable headline and subheadline.

Adds LabelCrossPromote to NavigationConfig for translatable section heading. All templates default to "Related Offerings" with template-specific variations (e.g., "Related Offerings" for architecture-deep-dive).

Updates all 9 landing page templates to render cross-promote section when enabled. Adds locale strings for cross-promote UI across all 30 supported languages.

Useful for organizations promoting a product family or related open-source projects from a single landing page.
2026-04-19 21:37:42 -04:00
f2f4367dbe fix(i18n): fix character encoding in locale files
All checks were successful
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 4m1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m28s
Build and Release / Lint (push) Successful in 6m12s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Fixes mojibake (garbled text) in 28 locale files caused by incorrect UTF-8 encoding. Replaces double-encoded characters with proper Unicode representations.

Examples:
- German: "Öffentliche" → "Öffentliche"
- Spanish: "reproducción" → "reproducción"
- French: "dépôts" → "dépôts"
- Czech: "ZaÄíná" → "Začíná"
- Finnish: "että" → "että"

Affects cs-CZ, de-DE, el-GR, es-ES, fa-IR, fi-FI, fr-FR, ga-IE, hi-IN, hu-HU, id-ID, is-IS, it-IT, ja-JP, ko-KR, lv-LV, nl-NL, pl-PL, pt-BR, pt-PT, ro-RO, ru-RU, si-LK, sk-SK, sv-SE, tr-TR, uk-UA, zh-CN.

No functional changes - purely encoding correction for proper display of non-ASCII characters.
2026-04-19 20:53:48 -04:00
7b4e85a473 feat(pages): add customizable section labels for landing pages
Adds ability to customize section headings on repository landing pages. Users can now override default labels for value propositions and features sections.

New fields:
- LabelValueProps: Custom heading for value propositions section (default: "Why choose us")
- LabelFeatures: Custom heading for features section (default: "Capabilities")

Useful for branding consistency or localization when default English headings don't fit the project's tone or language.
2026-04-19 20:31:22 -04:00
d8904e2846 fix(ui): add missing labels to landing page URL inputs
Adds missing label elements to logo URL, favicon URL, and hero image URL input fields in landing page settings. Improves form accessibility and visual consistency with other form fields.
2026-04-19 18:29:36 -04:00
4fabef6a65 feat(api): add organization management API v2 and MCP tools
All checks were successful
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 3m59s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 8m32s
Build and Release / Lint (push) Successful in 9m40s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Adds comprehensive v2 API endpoints for organization management:
- GET /api/v2/user/orgs - list user's organizations
- GET /api/v2/orgs/{org}/overview - org overview with pinned repos, members, stats, profile, recent activity
- GET /api/v2/orgs/{org}/repos - list org repositories with grouping support
- GET /api/v2/orgs/{org}/profile/readme - get org profile README
- PATCH /api/v2/orgs/{org} - update org metadata (requires owner/admin)
- PUT /api/v2/orgs/{org}/profile/readme - update profile README
- POST /api/v2/orgs/{org}/pinned - pin repository
- DELETE /api/v2/orgs/{org}/pinned/{repo} - unpin repository

Adds 8 MCP tools for AI assistant access: list_orgs, get_org_overview, update_org, list_org_repos, get_org_profile_readme, update_org_profile_readme, pin_org_repo, unpin_org_repo.

Introduces OrgOverviewV2 and OrgRecentActivity structs. Adds org/profile service for README and pinning operations.
2026-04-19 17:32:06 -04:00
f26bd3e273 fix(explore): prevent group splitting across pages in org explorer
When organization grouping is enabled, modifies sort order to group by group_header first before applying user-selected ordering. This prevents organizations in the same group from being split across pagination boundaries.

Adds CASE expression to sort ungrouped orgs (null/empty group_header) last, then groups alphabetically, then applies the requested orderBy within each group.
2026-04-19 17:16:14 -04:00
c5daac3366 feat(mcp): add stats, value props, and CTA tools for landing pages
All checks were successful
Build and Release / Lint (push) Successful in 5m21s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m21s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m32s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 12m1s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m55s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h7m31s
Build and Release / Unit Tests (push) Successful in 14m12s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 12m51s
Implements three new MCP tools for landing page management: update_landing_stats for stat counters, update_landing_value_props for value proposition cards, and update_landing_cta for bottom call-to-action section. Each tool supports structured data with validation and integrates with existing config save flow.
2026-04-05 12:49:19 -04:00
916211004d docs(mcp): add explanation to nolint directive
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m21s
Build and Release / Unit Tests (push) Successful in 15m52s
Build and Release / Lint (push) Successful in 16m53s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m18s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m49s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 8m12s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h7m30s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m19s
Clarifies why unparam is suppressed on toolListLandingTemplates with inline comment explaining interface requirement.
2026-04-05 09:09:01 -04:00
02fdc1a194 chore(mcp): suppress unparam linter warning for toolListLandingTemplates
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m27s
Build and Release / Unit Tests (push) Successful in 5m15s
Build and Release / Lint (push) Failing after 10m56s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Adds nolint:unparam directive to toolListLandingTemplates. Function signature must match tool handler interface even though parameters are unused.
2026-04-05 08:45:31 -04:00
1b0bba09b9 style(mcp): use errors.New for static error messages
Some checks failed
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m19s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Lint (push) Failing after 11m49s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 12m52s
Replaces fmt.Errorf with errors.New for static error message in resolveOwnerRepo. Marks unused context and args parameters with underscore in toolListLandingTemplates.
2026-04-05 04:03:02 -04:00
0c0d1c1493 feat(mcp): add landing page management tools to MCP server
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 3m42s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m25s
Build and Release / Lint (push) Failing after 10m12s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Creates mcp_pages.go with 10 new tools for managing repository landing pages via Claude Code. Supports getting/updating brand, hero, pricing, comparison, features, social proof, SEO, and theme sections. Includes template listing and enable/disable functionality. Integrates with existing pages service and registers tools in handleToolsList and handleToolsCall.
2026-04-05 03:39:54 -04:00
9461599b57 fix(api): populate Repo field in release API responses
All checks were successful
Build and Release / Unit Tests (push) Successful in 7m8s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m33s
Build and Release / Lint (push) Successful in 7m50s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m31s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m39s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m34s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 12m17s
Build and Release / Build Binary (linux/arm64) (push) Successful in 33m20s
Set release.Repo before converting to API format in all v2 release endpoints (CheckAppUpdate, ListReleasesV2, GetReleaseV2, GetLatestReleaseV2). Ensures repository information is available in API responses
2026-04-03 23:57:29 -04:00
414560f470 fix(ci): isolate GOMODCACHE per job to prevent cache conflicts
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m1s
Build and Release / Unit Tests (push) Successful in 22m31s
Build and Release / Lint (push) Successful in 24m45s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m37s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m40s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h7m8s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 13m32s
Build and Release / Build Binary (linux/arm64) (push) Failing after 30m51s
Move GOMODCACHE from global env to job-level env with unique paths per job. Prevents race conditions and cache corruption when jobs run in parallel, especially after the gotextdiff mirror replacement.
2026-03-30 09:47:38 -04:00
b43345986a refactor(pages): remove unused app store fields from advanced settings
Some checks failed
Build and Release / Unit Tests (push) Successful in 6m20s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Lint (push) Successful in 6m31s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 9h1m42s
Build and Release / Integration Tests (PostgreSQL) (push) Has been cancelled
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been cancelled
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
2026-03-30 09:32:58 -04:00
7fbbd26b20 refactor(ui): consolidate pages navigation into main settings navbar
Some checks failed
Build and Release / Unit Tests (push) Successful in 7m33s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m40s
Build and Release / Lint (push) Successful in 7m57s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 9h1m36s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 7m8s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 7m21s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m37s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 20m50s
Remove redundant pages_nav.tmpl and integrate the advanced pages link directly into the main settings navbar. Reduces template duplication while maintaining the same navigation structure.
2026-03-30 03:23:26 -04:00
b26bf4bfe8 fix(ci): replace deleted gotextdiff dependency with mirror
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Failing after 1m41s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m32s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been cancelled
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Lint (push) Has been cancelled
The upstream github.com/hexops/gotextdiff repository was deleted. Replace with internal mirror and add go clean -modcache to all CI dependency installation steps to ensure clean builds with the new module path.
2026-03-30 03:12:19 -04:00
242ebf2dc1 chore(ci): bump Go version to 1.25.5
Some checks failed
Build and Release / Unit Tests (push) Successful in 6m13s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m1s
Build and Release / Lint (push) Successful in 7m20s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 3m20s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 3m19s
Build and Release / Build Binary (linux/arm64) (push) Failing after 2m22s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h10m55s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 13m34s
2026-03-30 02:28:27 -04:00
46f7570d25 feat(pages): add static route configuration for direct file serving
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Failing after 47s
Build and Release / Integration Tests (PostgreSQL) (push) Failing after 1m29s
Build and Release / Lint (push) Failing after 2m16s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Allow repository pages to serve files directly at specific URL paths instead of rendering the landing page. Supports exact paths (/badge.svg) and glob patterns (/schema/*). Add advanced settings UI and API endpoints for managing static routes alongside existing redirects and custom code options.
2026-03-30 02:07:41 -04:00
48aab974fe fix(pages): populate defaults before translation overlay
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m48s
Build and Release / Unit Tests (push) Successful in 8m44s
Build and Release / Lint (push) Successful in 9m14s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m49s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m52s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m56s
Build and Release / Build Binary (linux/arm64) (push) Failing after 17m26s
Add ensureTemplateDefaults function that fills navigation labels and section headlines with template-specific defaults before applying translation overlay. Ensures these fields are present in base config JSON so translations can override them via deep-merge. Fixes issue where translated labels wouldn't apply because base config had empty strings instead of default values. Prevents templates from falling back to hardcoded English text.
2026-03-18 00:05:05 -04:00
965ef8966f feat(pages): add navigation label translation support
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m4s
Build and Release / Unit Tests (push) Successful in 4m38s
Build and Release / Lint (push) Successful in 6m26s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m52s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m52s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m22s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m38s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m46s
Add translation support for navigation section labels (Value Props, Features, Pricing, Blog, Gallery, Compare, etc.). Adds TemplateDefaultLabels function that returns template-specific creative names (e.g., "Systems Analysis" for value props in Architecture Deep Dive). Auto-applies defaults when enabling pages or changing templates. Includes UI fields in languages settings and translation JSON serialization. Enables full localization of section headings.
2026-03-17 23:34:29 -04:00
17028589c8 fix(i18n): use native language names in display map
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been cancelled
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been cancelled
Build and Release / Integration Tests (PostgreSQL) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Lint (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Unit Tests (push) Has been cancelled
Replace English language names with native names for better UX (e.g., "日本語" instead of "Japanese", "Español" instead of "Espanol"). Adds proper diacritics and uses native scripts. Makes language selection more intuitive for non-English speakers.
2026-03-17 21:50:15 -04:00
80096bfbf9 feat(ui): add loading indicators for AI generation actions
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m24s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m26s
Build and Release / Lint (push) Successful in 8m44s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m39s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m53s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m28s
Build and Release / Build Binary (linux/arm64) (push) Failing after 12m10s
Show loading spinner and message when AI content generation or translation is in progress. Disables submit button and hides form to prevent duplicate submissions. Adds Copilot icon to AI buttons. Marks file inputs with data-ays-ignore to prevent "unsaved changes" warnings. Improves UX by providing visual feedback during long-running AI operations.
2026-03-17 21:42:39 -04:00
22844f6437 fix(pages): use navigation labels for section headings and add defaults
Use LabelValueProps and LabelFeatures from navigation config for section headings instead of hardcoded text. Removes hardcoded subheadlines that don't translate well. Adds default headlines for blog ("Latest Posts"), gallery ("Gallery"), and comparison ("How We Compare") sections when not configured. Improves consistency and translation support across all 8 page templates.
2026-03-17 21:34:34 -04:00
737e323fcb perf(pages): parallelize bulk AI translation
Run AI translations for multiple languages concurrently using goroutines and sync.WaitGroup. Protects shared counters with mutex. Significantly reduces total translation time when translating many languages. For example, translating 10 languages now takes ~10 seconds instead of ~100 seconds (assuming 10s per language).
2026-03-17 21:24:05 -04:00
00024298d0 feat(pages): add JSON struct tags to pages config types
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m53s
Build and Release / Unit Tests (push) Successful in 8m48s
Build and Release / Lint (push) Successful in 9m12s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m35s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m50s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m2s
Build and Release / Build Binary (linux/arm64) (push) Failing after 14m53s
Add json struct tags to all LandingConfig types for proper JSON serialization in v2 API. Uses omitempty for primitives and omitzero for structs to exclude empty values from JSON output. Enables clean JSON responses from GET /repos/{owner}/{repo}/pages/config API endpoint.
2026-03-17 20:27:34 -04:00
b27dd0cda8 refactor(pages): use maps.Copy in deepMergeArrays
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 3m22s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m53s
Build and Release / Lint (push) Successful in 9m1s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m53s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 5m14s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m32s
Build and Release / Build Binary (linux/arm64) (push) Failing after 14m38s
Replace manual map copy loop with maps.Copy from Go 1.21 stdlib. Cleaner and potentially more efficient. Pre-allocates merged map with correct capacity.
2026-03-17 11:55:18 -04:00
43e490d933 feat(i18n): add bulk AI translation for all languages
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m21s
Build and Release / Lint (push) Failing after 8m46s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 8m49s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Add "Translate All (AI)" button to pages language settings that translates all configured languages in one operation. Shows success/partial success messages with counts. Adds locale keys for all 29 languages. Also removes trailing newlines from locale files for consistency.
2026-03-17 11:35:20 -04:00
d02f25c0ba fix(i18n): remove BOM from locale files
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m7s
Build and Release / Unit Tests (push) Successful in 4m38s
Build and Release / Lint (push) Successful in 6m35s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m59s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m55s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m19s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m58s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m32s
Remove UTF-8 BOM (byte order mark) from 14 locale JSON files. The BOM character (U+FEFF) at the start of files can cause parsing issues in some JSON parsers and is not needed for UTF-8 files. Affects ko-KR, lv-LV, nl-NL, pl-PL, pt-BR, pt-PT, ru-RU, si-LK, sk-SK, sv-SE, tr-TR, uk-UA, zh-CN, zh-TW.
2026-03-17 09:01:01 -04:00
12341079e1 feat(i18n): add translation keys for new pages sections
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m41s
Build and Release / Unit Tests (push) Successful in 8m55s
Build and Release / Lint (push) Successful in 9m23s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1m4s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 9h0m35s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 50s
Build and Release / Build Binary (linux/arm64) (push) Failing after 32s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 54s
Add locale keys for translating new landing page sections: gallery, comparison, blog, and expanded fields for stats, pricing, testimonials, and footer. Adds section headers and field labels for translation UI. Includes keys for all 29 supported languages. Enables full localization of new pages features added in recent commits.
2026-03-17 03:52:30 -04:00
c5e35e3466 refactor(pages): move app store badges to hero section and add section IDs
Move Google Play and App Store badges from downloads section to hero section for better visibility. Removes redundant display when both downloads and app stores are present. Add id attributes to major sections (hero, stats, downloads, social-proof, cta, etc.) for anchor link navigation. Applies to all 9 page templates. Improves UX for mobile app landing pages.
2026-03-17 03:25:14 -04:00
c0fcf16794 style(api): sort imports in pages API
All checks were successful
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m44s
Build and Release / Unit Tests (push) Successful in 8m45s
Build and Release / Lint (push) Successful in 9m18s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m53s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m46s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m13s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m45s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m38s
2026-03-17 01:27:51 -04:00
c5786aab2b fix(api): use internal json module in pages API
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m48s
Build and Release / Lint (push) Failing after 8m37s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 8m53s
Replace stdlib encoding/json with internal json module for consistency with rest of codebase. Ensures proper handling of edge cases and custom marshaling behavior.
2026-03-17 01:15:01 -04:00
fe5b504b97 feat(api): add v2 API endpoints for landing page configuration
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m44s
Build and Release / Lint (push) Failing after 8m24s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 8m43s
Add comprehensive REST API for managing landing page configuration programmatically. Includes GET /repos/{owner}/{repo}/pages/config to retrieve full config, PUT to replace entire config, PATCH to update specific sections. Supports all config sections: brand, hero, stats, features, pricing, blog, gallery, comparison, etc. Adds UpdatePagesConfigOption and related structs for API payloads. Includes error codes for pages validation. Enables headless/automated landing page management.
2026-03-17 00:52:18 -04:00
a1f477a381 fix(ui): add missing pages settings nav items and cleanup labels
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m53s
Build and Release / Unit Tests (push) Successful in 8m43s
Build and Release / Lint (push) Successful in 9m20s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m30s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m44s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m59s
Build and Release / Build Binary (linux/arm64) (push) Failing after 24m13s
Add Comparison and Languages items to pages settings sidebar navigation. Fix sidebar not staying open when on those pages. Remove redundant labels from URL input fields that appear after "or use a URL" dividers (logo, favicon, hero image). Improves navigation consistency and UI clarity.
2026-03-17 00:14:48 -04:00
4e32224330 refactor(pages): move comparison config to dedicated settings page
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 3m29s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m44s
Build and Release / Lint (push) Successful in 8m50s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m57s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m16s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m13s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Extract comparison table configuration from Content page to new Comparison settings page. Content page now only has enable/disable toggle with link to full config. New page includes dynamic UI for managing groups and features with add/remove buttons. Improves UX by separating complex comparison config from other content settings. Adds navigation tab and route for /settings/pages/comparison.
2026-03-16 23:50:12 -04:00
66ce2b1d70 style(pages): align template constant declarations
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m2s
Build and Release / Unit Tests (push) Successful in 4m37s
Build and Release / Lint (push) Successful in 6m33s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m51s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m30s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 7m21s
Build and Release / Build Binary (linux/arm64) (push) Successful in 9m0s
2026-03-16 23:22:18 -04:00
cdf47b5ecd feat(pages): add comparison table section to landing pages
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m51s
Build and Release / Lint (push) Failing after 8m33s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 8m46s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Add configurable comparison matrix section for feature comparisons. Supports 3 columns (e.g., your product vs competitors, or pricing tiers) with optional highlight column. Features can be organized into groups. Each cell accepts "true" for checkmark, "false" for X, or custom text. Includes settings UI with up to 10 groups and 20 features per group. Renders in all 9 page templates with responsive table styling.
2026-03-16 23:05:10 -04:00
d45354b538 fix(pages): fix navigation anchor links from blog pages
Add LandingURL context variable that resolves to landing page base path. Use it for all navigation anchor links (#features, #pricing, etc.) so they navigate correctly from blog detail/list pages back to landing page sections. For repo-path mode uses /{owner}/{repo}/pages, for custom domains uses /. Fixes broken navigation when viewing blog posts.
2026-03-16 22:37:48 -04:00
f3eba7dd34 feat(pages): add 5 new landing page templates
Add Documentation First, Developer Tool, Visual Showcase, CLI Terminal, and Architecture Deep Dive templates. Brings total templates to 9. Each template has unique design language and target audience: Documentation First for docs-heavy projects, Developer Tool for technical products, Visual Showcase for design/media projects, CLI Terminal for command-line tools, Architecture Deep Dive for technical deep-dives. Updates template display names for clarity.
2026-03-16 22:18:53 -04:00
818c2db411 feat(pages): add blog reading enhancements and gallery lightbox
Some checks failed
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m23s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Lint (push) Successful in 6m8s
Build and Release / Unit Tests (push) Successful in 6m12s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m37s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m0s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binary (linux/arm64) (push) Failing after 13m52s
Add reading progress bar, table of contents sidebar, and back-to-top button for blog post detail pages. TOC auto-generates from h1-h4 headings with smooth scroll navigation and active section highlighting. Sidebar expands on hover from left edge. Add lightbox for gallery images with keyboard navigation (arrows, escape) and captions. Back-to-top button appears after 400px scroll on all pages. All features use inline styles for portability and minimal CSS conflicts.
2026-03-16 21:43:46 -04:00
fa016ab865 perf(pages): stream asset files instead of buffering in memory
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m52s
Build and Release / Unit Tests (push) Successful in 9m0s
Build and Release / Lint (push) Successful in 9m31s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m47s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m7s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 3s
Build and Release / Build Binary (linux/arm64) (push) Failing after 11m19s
Replace full-file buffering with io.Copy streaming in serveRepoFileAsset. Reduces memory usage for large assets (images, videos) by streaming directly from git blob to HTTP response. Adds Content-Length header for better client handling.
2026-03-16 03:19:33 -04:00
dea935bec7 docs(pages): add comments for image decoder imports
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m47s
Build and Release / Unit Tests (push) Successful in 8m55s
Build and Release / Lint (push) Successful in 9m24s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m56s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m7s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
2026-03-16 02:54:36 -04:00
e8b6303971 feat(pages): serve social preview on custom domains
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m1s
Build and Release / Unit Tests (push) Successful in 4m35s
Build and Release / Lint (push) Failing after 5m58s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Add /social-preview.png endpoint for custom domain/subdomain landing pages. Generates social card using same logic as repo social preview but accessible on custom domains. Adds .png extension to social preview URLs for better compatibility with social media crawlers. Updates og:image and twitter:image meta tags to use domain-relative URLs on custom domains. Includes ETag caching with 30min max-age.
2026-03-16 02:46:41 -04:00
c24d329a6a fix(pages): use /pages/assets/ path to avoid conflict with gitea assets
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been cancelled
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Lint (push) Has been cancelled
Build and Release / Integration Tests (PostgreSQL) (push) Has been cancelled
Build and Release / Unit Tests (push) Has been cancelled
Change custom domain asset path from /assets/ to /pages/assets/ to avoid conflict with Gitea's static asset route. Affects gallery images and custom assets on custom domain/subdomain landing pages. Repo-path mode already used the correct /pages/assets/ prefix.
2026-03-16 02:30:04 -04:00
433214fb91 feat(pages): add app store links to downloads section
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m56s
Build and Release / Unit Tests (push) Successful in 8m52s
Build and Release / Lint (push) Successful in 9m21s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m54s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m27s
Build and Release / Build Binary (linux/arm64) (push) Failing after 13m25s
Add GooglePlayID and AppStoreID config fields to display app store download buttons on landing pages. Shows "App Stores" section with Google Play and/or App Store badges when IDs are configured. Useful for mobile apps distributed via stores instead of direct downloads. Includes branded SVG icons for both stores. Applies to all four page templates.
2026-03-16 02:04:33 -04:00
d32bcc4d8c feat(pages): improve SEO meta tags for blog posts and landing pages
Add comprehensive Open Graph and Twitter Card meta tags for blog post detail pages. Use blog post title, subtitle, and featured image when available. Add og:url and twitter:title/description tags. Set twitter:card to summary_large_image when images present, otherwise summary. Add twitter:site support from SEO config. Generates canonical page URL from request context (handles http/https based on TLS).
2026-03-16 01:51:02 -04:00
222c21bf98 feat(pages): serve gallery images via asset endpoint
Add support for serving gallery images through /assets/gallery/ endpoint instead of /raw/ URLs. Maps gallery/ asset paths to .gallery/ folder in repo. Works for both repo-path mode and custom domain mode. Refactors asset serving into shared serveRepoFileAsset helper. Improves URL consistency and allows proper caching headers for gallery images on landing pages.
2026-03-16 01:48:40 -04:00
5e165b97be feat(pages): add option to hide mobile releases from landing page
Add HideMobileReleases config option to hide Android and iOS releases from the downloads section on landing pages. Useful when mobile apps are distributed via app stores rather than direct downloads. Adds checkbox in Content settings and conditional rendering in all four page templates.
2026-03-16 01:22:52 -04:00
c7b5a2af19 feat(socialcard): use display names and custom branding on social cards
Replace GitCaddy logo with custom brand name (from Owner Display Name setting) when set. Use user/org display names instead of usernames in repo full name. Use DisplayTitle instead of repo name when available. Adds drawBrandOrLogo helper to render either custom text or default logo. Improves branding consistency with pages landing page settings.
2026-03-16 01:12:04 -04:00
fca0461ca4 refactor(ui): fix nested forms in pages image upload UI
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m0s
Build and Release / Unit Tests (push) Successful in 4m36s
Build and Release / Lint (push) Successful in 6m23s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m50s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m21s
Build and Release / Build Binary (linux/arm64) (push) Failing after 15m2s
Move upload/delete forms outside main settings form to avoid invalid nested form HTML. Use form attribute on buttons and inputs to associate with hidden forms. Fixes HTML validation errors and potential browser quirks. Also replaces hardcoded border color with CSS variable for theme consistency. Affects brand logo/favicon and hero image upload sections.
2026-03-16 00:31:13 -04:00
044c65e425 feat(releases): add public release downloads setting for limited repos
Add PublicReleaseDownloads repository setting to allow direct release downloads without authentication for Limited visibility repos. Adds migration v369, checkbox in Advanced Settings (only shown for Limited repos with releases enabled), and updates attachment serving logic to check HasPublicReleases. Moves release download routes outside auth requirement when public downloads enabled. Complements existing pages config PublicReleases toggle.
2026-03-16 00:26:53 -04:00
3801e0ba0d feat(pages): group release downloads by platform
Automatically categorize release attachments into Windows, macOS, Linux, Android, iOS, and Other sections based on filename patterns and extensions. Uses heuristics like file extensions (.exe, .dmg, .deb, .apk, .ipa), platform keywords (windows, darwin, linux, android, ios), and common naming conventions. Improves download UX by organizing files into logical platform groups with appropriate icons. Applies to all four page templates.
2026-03-16 00:12:11 -04:00
741ddd5805 fix(pages): use DisplayName instead of Name for blog authors
Replace .Author.Name with .Author.DisplayName in all page templates to show the user's display name instead of username for blog post authors. Affects all four landing page templates (bold-marketing, minimalist-docs, open-source-hero, saas-conversion). Consistent with user display name conventions elsewhere in the application.
2026-03-15 23:57:46 -04:00
5c6b91f6c9 refactor(pages): deduplicate image upload/delete handlers
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m51s
Build and Release / Unit Tests (push) Successful in 8m56s
Build and Release / Lint (push) Successful in 9m26s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m50s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h7m28s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m1s
Extract common image upload and delete logic into pagesUploadImage and pagesDeleteImage helper functions. Eliminates ~90 lines of duplicated code across logo, favicon, and hero image handlers. Uses function parameters for field-specific operations (get/set config fields, locale keys, file prefixes). Makes it easier to add new image upload fields in the future.
2026-03-15 23:20:22 -04:00
573aa49a22 fix(blog): allow public access to blog featured images
Some checks failed
Build and Release / Unit Tests (push) Successful in 3m32s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 8m1s
Build and Release / Lint (push) Failing after 8m13s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Allow public access to blog post featured images even when repository is private, if the post is published and blog is enabled. This supports public landing pages with blog sections that display featured images. Adds IsPublishedBlogFeaturedImage query and isBlogFeaturedImage check in attachment serving. Also removes redundant SafeHTML filter from blog content templates (already HTML-safe).
2026-03-15 23:05:58 -04:00
679810687f feat(pages): add gallery section to landing page templates
Add configurable gallery section that displays images from repository's .gallery folder on landing pages. Includes settings for enabled/disabled, headline, subheadline, max images (default 6), and grid columns (default 3). Reads captions from gallery.json metadata. Implements gallery rendering in all four page templates (bold-marketing, minimalist-docs, open-source-hero, saas-conversion). Integrates with existing gallery management feature.
2026-03-15 22:54:11 -04:00
fc86952bf4 feat(pages): add logo and favicon upload to pages brand settings
Add ability to upload logo and favicon images directly instead of only using URLs. Images stored in repo-avatars storage with hash-based filenames (max 5 MB, JPG/PNG/WebP/GIF). Uploaded images take priority over URL fields. Includes upload/delete endpoints for both logo and favicon, UI with previews, and ResolvedLogoURL/ResolvedFaviconURL helper methods. Updates base_head template to use resolved favicon URL.
2026-03-15 22:45:29 -04:00
79a0c2683e feat(pages): add hero image upload to pages settings
Add ability to upload hero images directly instead of only using URLs. Images are stored in repo-avatars storage with hash-based filenames (max 5 MB, JPG/PNG/WebP/GIF). Uploaded images take priority over URL field. Includes upload and delete endpoints, UI with preview, and ResolvedImageURL helper method. Improves UX by eliminating need for external image hosting.
2026-03-15 22:39:52 -04:00
6007a19bed feat(pages): add support for 11 additional social platforms
Expand social link platform support from 5 to 16 platforms. Adds: Bluesky, Facebook, Instagram, Mastodon, Reddit, RSS, Substack, Threads, TikTok, and Twitch. Updates footer settings UI and all page templates (bold-marketing, minimalist-docs, open-source-hero, saas-conversion) with appropriate Octicon mappings for each platform.
2026-03-15 22:28:18 -04:00
123eb78b54 feat(repo): add owner display name field for repositories
Add OwnerDisplayName field to Repository model to allow custom display name for repository owner (e.g., "John Smith" instead of username). Used in license generation, social cards, and other public-facing contexts. Includes migration v368 to add column, UI in repository settings, and updates to license creation and social card rendering to use display name when available.
2026-03-15 22:19:20 -04:00
56fd71ad6b feat(pages): add option to use media kit social card for OG image
Add checkbox in pages SEO settings to use the repository's Media Kit social preview as the Open Graph image instead of a custom URL. When enabled, shows preview of the social card and uses /social-preview endpoint. Adds og:title, og:description, og:image, and twitter:card meta tags to pages base template. Improves social sharing integration with existing Media Kit feature.
2026-03-15 22:08:31 -04:00
82eddb0b09 feat(ui): add chip-based keyword input for pages SEO
Replace plain text input with interactive chip UI for SEO keywords in repository pages settings. Users can add keywords individually with validation, and remove them by clicking the × button. Backend now trims whitespace from keywords. Improves UX for managing keyword lists.
2026-03-15 22:00:42 -04:00
a2644bb47f fix(pages): preload repository owner in landing page context
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m52s
Build and Release / Unit Tests (push) Successful in 10m54s
Build and Release / Lint (push) Successful in 11m38s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1m36s
Build and Release / Build Binary (linux/arm64) (push) Failing after 1m33s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m39s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 6m23s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 13m45s
Ensure repository owner is loaded before rendering landing pages to prevent template errors when accessing .Repository.Owner.Name. Add error logging if owner loading fails.
2026-03-07 20:40:43 -05:00
cb2791709e refactor(pages): remove inline prompts from A/B test AI calls
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m54s
Build and Release / Unit Tests (push) Successful in 6m17s
Build and Release / Lint (push) Successful in 6m26s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m25s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h3m49s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m57s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m46s
Build and Release / Build Binary (linux/arm64) (push) Failing after 10m31s
Remove inline instruction prompts from experiment generation and analysis. These instructions are now defined in ABTestGeneratePlugin and ABTestAnalyzePlugin, eliminating duplication and improving maintainability.
2026-03-07 16:12:14 -05:00
85ab93145e refactor(pages): move AI prompts to plugin definitions and add JSON extraction
Simplify AI content generation by delegating to plugin system:
- Remove inline prompt instructions (now in LandingPageContentPlugin)
- Add extractJSON helper to handle markdown-wrapped responses
- Hardcode CTA URLs to repo URL instead of relying on AI
- Apply JSON extraction to both content generation and translation
- Reduces code duplication and improves maintainability
2026-03-07 15:52:46 -05:00
fb89a8b55c chore(i18n): add translation key for A/B experiment analysis cron job
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m19s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m26s
Build and Release / Lint (push) Successful in 6m11s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m22s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h3m52s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m2s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 6m3s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Add "admin.dashboard.analyze_page_experiments" translation key across all 29 locales for the landing page A/B testing experiment analysis cron job in the admin dashboard.
2026-03-07 14:28:07 -05:00
81c7b07ca3 chore(i18n): add translations for pages features across 28 locales
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m3s
Build and Release / Lint (push) Successful in 4m17s
Build and Release / Unit Tests (push) Successful in 4m36s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m14s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m3s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m12s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Add localized strings for landing pages features:
- Blog section configuration
- AI content generation
- Multi-language support and translations
- Navigation link controls
- Favicon customization
- Translation management UI

Covers Czech, German, Greek, Spanish, Persian, Finnish, French, Irish, Hindi, Hungarian, Indonesian, Icelandic, Italian, Japanese, Korean, Latvian, Dutch, Polish, Portuguese (BR/PT), Russian, Sinhala, Slovak, Swedish, Turkish, Ukrainian, Chinese (CN/TW)
2026-03-07 13:43:56 -05:00
5788123e00 feat(pages): add multi-language support for landing pages
Implement internationalization system for landing pages:
- Database model for storing language-specific translations
- Language configuration with default and enabled languages
- Language switcher in navigation across all templates
- Translation management UI in settings
- Support for 15 languages including English, Spanish, German, French, Japanese, Chinese
- Auto-detection and manual language selection
- AI-powered translation generation capability
2026-03-07 13:09:46 -05:00
a2edcdabe7 feat(pages): add AI-powered landing page content generator
Enable automatic landing page content generation using AI:
- Generate hero, features, stats, and CTAs from README
- Blog section configuration in settings UI
- Extract repository metadata for AI context
- Merge generated content while preserving existing settings
- User-friendly generation button in settings panel
2026-03-07 12:47:28 -05:00
3a8bdd936c feat(pages): add A/B testing framework for landing pages
Implement comprehensive A/B testing system for landing page optimization:
- Database models for experiments, variants, and events
- AI-powered variant generation and analysis
- Visitor tracking with conversion metrics
- Experiment lifecycle management (draft/active/paused/completed)
- Email notifications for experiment results
- Cron job for automated experiment monitoring
- UI for viewing experiment results and statistics
2026-03-07 12:39:42 -05:00
64b4a9ceed Update pages.go
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m46s
Build and Release / Unit Tests (push) Successful in 6m10s
Build and Release / Lint (push) Successful in 6m20s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m18s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m2s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m41s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 7m10s
Build and Release / Build Binary (linux/arm64) (push) Failing after 11m42s
2026-03-07 11:34:16 -05:00
727ae54f91 feat(pages): add blog post detail and list views to landing pages
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 3m22s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m29s
Build and Release / Lint (push) Failing after 5m48s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Enable blog functionality on custom domains with dedicated views:
- Blog post detail page with markdown rendering
- Paginated blog list view
- Shared context setup for consistent navigation/footer
- Route handling for /blog and /blog/:id paths
- Template updates across all landing page themes
2026-03-07 11:24:56 -05:00
238f0974d8 feat(pages): add blog section support to landing page templates
Add blog section configuration and rendering to landing pages. Includes:
- New BlogSectionConfig with headline, max posts, and excerpt options
- Logo source selection (URL, repo avatar, or org avatar)
- Recent blog posts display with featured images
- Navigation links to blog section
- Absolute repo URLs for custom domain support
2026-03-07 10:34:51 -05:00
b7d8fcc719 fix(i18n): correct sinhala translation encoding
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m16s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m20s
Build and Release / Lint (push) Successful in 5m59s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m10s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m11s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m0s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 6m17s
Build and Release / Build Binary (linux/arm64) (push) Failing after 13m0s
2026-03-06 19:37:14 -05:00
bf59c1cd5f feat(mcp): add list_issues and get_issue tools
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m17s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m32s
Build and Release / Lint (push) Successful in 6m37s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 46s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Implement MCP tools for querying repository issues. list_issues supports pagination and state filtering (open/closed/all), while get_issue retrieves detailed information including body content and comments for a specific issue.
2026-03-06 19:24:59 -05:00
6dff66b5ba chore(i18n): add translations for footer and releases settings
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m53s
Build and Release / Unit Tests (push) Successful in 6m22s
Build and Release / Lint (push) Successful in 6m32s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 9h0m4s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1m52s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 1m42s
Build and Release / Build Binary (linux/arm64) (push) Failing after 1m22s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 49s
Add translations for new footer configuration options (powered by, licenses, API links) and public releases feature across 28 locales.
2026-03-06 19:02:17 -05:00
734dd895bb feat(pages): add public releases option for landing pages
Allow private repositories to enable public release downloads on their landing pages. When enabled, unauthenticated users can download release attachments without accessing the repository. Adds download sections to all landing page templates with styling.
2026-03-06 18:04:35 -05:00
6ceb4f0ad4 fix: formatting
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m59s
Build and Release / Lint (push) Successful in 4m10s
Build and Release / Unit Tests (push) Successful in 4m35s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m13s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m22s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m10s
Build and Release / Build Binary (linux/arm64) (push) Failing after 8m31s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 10m4s
2026-03-06 17:19:31 -05:00
eca22df63a feat(admin): add configurable footer links for licenses and api
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m44s
Build and Release / Lint (push) Failing after 5m50s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 6m4s
Add theme configuration options to control visibility of Licenses and API links in the footer. Also add authentication token to vault version API requests in build workflow to prevent rate limiting issues.
2026-03-06 17:04:32 -05:00
5f2420d353 feat(admin): add configurable footer powered by message
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m47s
Build and Release / Unit Tests (push) Successful in 6m10s
Build and Release / Lint (push) Successful in 6m17s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 37s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 8h59m46s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1m12s
Build and Release / Build Binary (linux/arm64) (push) Failing after 1m19s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Move ShowFooterPoweredBy from static setting to dynamic theme configuration, allowing admins to toggle the "Powered by GitCaddy Server" footer message through the admin panel without restarting the server.
2026-03-06 16:40:41 -05:00
b5db61c34c fix(ci): use release token for checkout to access private repos
All checks were successful
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m51s
Build and Release / Unit Tests (push) Successful in 6m8s
Build and Release / Lint (push) Successful in 6m16s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Add RELEASE_TOKEN to all checkout actions to enable access to private dependencies during CI builds. Fixes authentication issues when cloning vault and other private repositories.
2026-03-04 21:30:59 -05:00
c59a0f746e feat(packages): split package list into public and private sections
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m49s
Build and Release / Unit Tests (push) Successful in 6m13s
Build and Release / Lint (push) Successful in 6m21s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m19s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h3m57s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m10s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 6m18s
Build and Release / Build Binary (linux/arm64) (push) Failing after 9m50s
Separate packages into PublicPackages and PrivatePackages arrays for template rendering. Add CanWritePackages flag to determine if viewer has permission to publish packages (admin, owner, or org team member with write access). Revert anonymous user 404 redirect to show proper error page.
2026-03-04 20:21:14 -05:00
438a41cd78 feat(web): redirect anonymous users to homepage on 404
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 3m21s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m32s
Build and Release / Lint (push) Successful in 6m7s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Improve UX for unauthenticated visitors by redirecting them to the homepage instead of showing a 404 page. Authenticated users still see the standard 404 error page.
2026-03-04 09:34:47 -05:00
8cf6c08841 fix(org): handle anonymous users in repo count filtering
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m58s
Build and Release / Unit Tests (push) Successful in 6m12s
Build and Release / Lint (push) Successful in 6m21s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m23s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h3m51s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 8m1s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 12m57s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Set Private flag based on whether actor is present. When actor is nil (anonymous), Private must be false so SearchRepositoryCondition correctly filters to public repos only. Fixes repo count visibility for unauthenticated users.
2026-03-04 09:13:55 -05:00
112130747c fix(org): filter org repo count by user permissions
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m52s
Build and Release / Unit Tests (push) Successful in 6m14s
Build and Release / Lint (push) Successful in 6m23s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m25s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h6m17s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 7m20s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 9m33s
Build and Release / Build Binary (linux/arm64) (push) Failing after 10m35s
Pass actor/doer to GetOrgOverviewStats to filter repository count based on user access rights. Replace CountRepositories with SearchRepository to respect permission checks. Prevents users from seeing total count of repos they cannot access.
2026-03-04 08:25:15 -05:00
23b6860378 Update build.yml
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m54s
Build and Release / Unit Tests (push) Successful in 6m9s
Build and Release / Lint (push) Successful in 6m28s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m14s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h3m54s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 9m11s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 12m32s
Build and Release / Build Binary (linux/arm64) (push) Successful in 28m38s
2026-03-04 01:55:47 -05:00
aeaea13ab7 Update build.yml
Some checks failed
Build and Release / Unit Tests (push) Failing after 1m48s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m20s
Build and Release / Lint (push) Successful in 4m53s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m48s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been cancelled
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
2026-03-04 01:41:57 -05:00
bd4d53d0f8 chore(ci): revert go version and remove mod tidy steps
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Failing after 31s
Build and Release / Lint (push) Failing after 1m3s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Has been cancelled
Revert to Go 1.25.0 with local toolchain. Remove unnecessary 'go mod tidy' steps from build workflow as dependencies should already be committed.
2026-03-04 01:30:32 -05:00
34a2c8faf0 chore(ci): downgrade go to 1.25.5 and enable auto toolchain
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Failing after 31s
Build and Release / Lint (push) Failing after 1m5s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Failing after 1m7s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Change GOTOOLCHAIN from local to auto to allow automatic Go version upgrades when required by go.mod. Downgrade base version from 1.25.7 to 1.25.5.
2026-03-04 01:17:54 -05:00
79353f4756 chore(ci): update go version to 1.25.7
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Failing after 39s
Build and Release / Lint (push) Failing after 1m37s
Build and Release / Integration Tests (PostgreSQL) (push) Failing after 1m37s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
2026-03-04 01:11:56 -05:00
5bda6de937 refactor(secretscan): use internal json module instead of stdlib
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Failing after 31s
Build and Release / Lint (push) Failing after 55s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Failing after 56s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
2026-03-04 01:03:31 -05:00
dd84db7608 feat(secretscan): support .gitsecrets-ignore file for false positives
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Failing after 50s
Build and Release / Lint (push) Failing after 1m17s
Build and Release / Integration Tests (PostgreSQL) (push) Failing after 1m16s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Parse .gitsecrets-ignore file from commits to filter out known false positives during secret scanning. Matches entries by SHA-256 content hash (truncated to 16 chars) and pattern ID. Update push rejection message to guide users toward GitSecrets addon for managing ignored detections.
2026-03-04 00:58:13 -05:00
14232eec68 fix(actions): ignore label scheme suffix in runner matching
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 6m6s
Build and Release / Unit Tests (push) Successful in 6m27s
Build and Release / Lint (push) Successful in 7m10s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m1s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m48s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m52s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m38s
Build and Release / Build Binary (linux/arm64) (push) Successful in 9m40s
Update runner label matching to strip ":scheme" suffixes (e.g., ":host", ":docker") before comparison. This allows runners with "germany-linux:host" to match jobs with "runs-on: germany-linux" and vice versa.

Previously, exact label match was required, causing runners with scheme-qualified labels to fail matching jobs without schemes.
2026-02-15 12:36:06 -05:00
56cf9d1833 feat(api): implement v2 workflow status and failure endpoints
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m27s
Build and Release / Unit Tests (push) Successful in 7m45s
Build and Release / Lint (push) Successful in 7m58s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m7s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m35s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h6m42s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m49s
Build and Release / Build Binary (linux/arm64) (push) Failing after 2m57s
Adds v2 API endpoints for optimized workflow status queries. Implements /workflows/status to fetch latest run per workflow in single query using MAX(id) grouping. Adds /runs/{id}/failure-log endpoint returning structured failure data with job details, failed steps, log tails (last 200 lines), and workflow YAML content. Reduces client-side API calls and processing overhead.
2026-02-14 11:54:55 -05:00
de7d6a719e feat(plugins): implement protocol version negotiation in server
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 6m51s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m6s
Build and Release / Lint (push) Successful in 7m42s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m9s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m31s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m54s
Build and Release / Build Binary (linux/arm64) (push) Successful in 9m40s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m9s
Complete protocol versioning implementation on the server side, enabling forward-compatible plugin protocol evolution.

Server Changes:
- Send ProtocolVersion = 1 in InitializeRequest
- Store plugin's reported protocol version in ManagedPlugin
- Treat version 0 (pre-versioning plugins) as version 1
- Add SupportsProtocol() method to check before calling version-gated RPCs
- Log plugin's protocol version during initialization

Generated Code:
- Regenerate plugin.pb.go with protocol_version fields
- Add getter methods for new fields

Documentation:
- Add Protocol Versioning section to PLUGINS.md
- Explain version negotiation flow
- Document when plugins need to update vs. when they don't
- Add version history table (currently only v1)
- Update all example code to return protocol_version = 1

Benefits:
- Server can safely add new RPCs in future versions without breaking old plugins
- Plugins can detect newer servers and opt into advanced features
- Zero-value (0) provides backwards compatibility with pre-versioning plugins
- Clear upgrade path documented for plugin developers

This completes the protocol versioning feature started in the previous commit.
2026-02-13 02:18:20 -05:00
e932582f54 docs(plugins): add comprehensive plugin development guide
Add PLUGINS.md with complete documentation for building external GitCaddy plugins using the gRPC-based plugin protocol.

Documentation includes:
- Protocol overview and service definition
- Lifecycle diagram (Initialize → HealthCheck → OnEvent/HandleHTTP → Shutdown)
- Complete message reference for all 6 RPC methods
- Plugin manifest specification (routes, events, permissions, license tiers)
- Health monitoring and auto-restart behavior
- Configuration guide for external vs managed mode
- Transport details (h2c/HTTP2, gRPC wire format)
- Full working examples in Go, C#, and Python
- Debugging tips and common issues

Also updates README.md to reference the plugin guide and removes outdated Chinese translations (zh-cn, zh-tw) that were not being maintained.

This provides plugin developers with everything needed to build and deploy external services that integrate with GitCaddy's plugin framework.
2026-02-13 01:54:25 -05:00
174d18db22 refactor(plugins): migrate external plugin manager to grpc/connect
Replace manual HTTP/JSON RPC implementation with generated gRPC/Connect client, providing type-safe plugin communication.

Code Generation:
- Generate plugin.pb.go and pluginv1connect/plugin.connect.go from plugin.proto
- Add generate-plugin-proto Makefile target
- Delete hand-written types.go (replaced by generated code)

ExternalPluginManager Refactoring:
- Replace httpClient with pluginv1connect.PluginServiceClient
- Use h2c (cleartext HTTP/2) transport for gRPC without TLS
- Replace all manual callRPC/callRPCWithContext calls with typed Connect methods
- Remove JSON serialization/deserialization code
- Simplify error handling with native gRPC status codes

Benefits:
- Type safety: compile-time verification of request/response types
- Protocol compatibility: standard gRPC wire format
- Reduced code: ~100 lines of manual RPC code removed
- Better errors: structured gRPC status codes instead of string parsing
- Matches existing Actions runner pattern (Connect RPC over HTTP/2)

This completes the plugin framework migration to production-grade RPC transport.
2026-02-13 01:45:29 -05:00
f42c6c39f9 feat(ai-service): complete ai production readiness tasks
All checks were successful
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 6m49s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m6s
Build and Release / Lint (push) Successful in 7m15s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Implement critical production readiness features for AI integration: per-request provider config, admin dashboard, workflow inspection, and plugin framework foundation.

Per-Request Provider Config:
- Add ProviderConfig struct to all AI request types
- Update queue to resolve provider/model/API key from cascade (repo > org > system)
- Pass resolved config to AI sidecar on every request
- Fixes multi-tenant issue where all orgs shared sidecar's hardcoded config

Admin AI Dashboard:
- Add /admin/ai page with sidecar health status
- Display global operation stats (total, 24h, success/fail/escalated counts)
- Show operations by tier, top 5 repos, token usage
- Recent operations table with repo, operation, status, duration
- Add GetGlobalOperationStats model method

Workflow Inspection:
- Add InspectWorkflow client method and types
- Implement workflow-inspect queue handler
- Add notifier trigger on workflow file push
- Analyzes YAML for syntax errors, security issues, best practices
- Returns structured issues with line numbers and suggested fixes

Plugin Framework (Phase 5 Foundation):
- Add external plugin config loading from app.ini
- Define ExternalPlugin interface and manager
- Add plugin.proto contract (Initialize, Shutdown, HealthCheck, OnEvent, HandleHTTP)
- Implement health monitoring with auto-restart for managed plugins
- Add event routing to subscribed plugins
- HTTP proxy support for plugin-served routes

This completes Tasks 1-4 from the production readiness plan and establishes the foundation for managed plugin lifecycle.
2026-02-13 01:16:58 -05:00
813e3bcbb4 docs(nuget): expand ai features documentation
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 6m48s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 6m59s
Build and Release / Lint (push) Successful in 7m18s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m17s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m14s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m41s
Build and Release / Build Binary (linux/arm64) (push) Failing after 6m9s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 8h4m30s
Significantly expand the AI features section of the README with comprehensive configuration and usage documentation.

New content includes:
- Detailed explanation of Tier 1 (light operations) vs Tier 2 (agent mode)
- Complete app.ini configuration reference with all AI settings
- Step-by-step setup guide (system → org → repo → runner)
- Configuration cascade explanation (repo > org > system)
- Built-in safety features (bot user, loop prevention, rate limits, escalation)
- Automatic operation triggers table
- Manual API trigger examples
- Operation history and audit log access
- Escalation workflow documentation
- V2 API configuration section

This provides users with everything needed to deploy and configure the AI features, from initial setup through advanced agent mode configuration.
2026-02-12 09:38:12 -05:00
14338d8fd4 refactor(ai): consolidate ai operation types and reduce duplication
All checks were successful
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 7m11s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m21s
Build and Release / Lint (push) Successful in 7m32s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Refactor AI service layer to reduce code duplication and improve consistency.

Changes:
- Rename AIOperationRequest to OperationRequest for consistency
- Extract shared logic for issue-targeted operations (respond, triage) into triggerIssueAIOp helper
- Standardize field alignment in struct definitions
- Remove redundant error handling patterns

This reduces the API operations file by ~40 lines while maintaining identical functionality.
2026-02-12 00:55:52 -05:00
c9f6c4e7d2 feat(ui): add ai settings web interface for repos and orgs
Add comprehensive web UI for configuring AI features at repository and organization levels, completing the activation workflow for AI operations.

Repository AI Settings (repo/settings/ai):
- Enable/disable AI unit for the repository
- Toggle Tier 1 operations (auto-respond, auto-review, auto-triage, workflow inspection)
- Configure Tier 2 agent mode with trigger labels and runtime limits
- Set escalation rules (label, team assignment)
- Override provider/model preferences
- Add custom instructions for different operation types

Organization AI Settings (org/settings/ai):
- Configure org-level AI provider and model
- Set encrypted API key (with masked display)
- Define rate limits (max operations per hour)
- Whitelist allowed operations
- Enable/disable agent mode for org repositories

Both interfaces include proper permission checks, form validation, and cascade resolution display (showing inherited vs. overridden values). Adds navigation entries to settings sidebars and full i18n support.
2026-02-12 00:48:18 -05:00
eb1a5d497e feat(api): add ai operations and settings api endpoints
Add comprehensive API v2 endpoints for managing AI operations and repository-level AI settings.

AI Operations endpoints:
- GET /repos/{owner}/{repo}/ai/operations - List AI operation logs
- GET /repos/{owner}/{repo}/ai/operations/{id} - Get specific operation details
- POST /repos/{owner}/{repo}/ai/operations/trigger - Manually trigger AI operations
- GET /repos/{owner}/{repo}/ai/stats - Get AI usage statistics

AI Settings endpoints:
- GET /repos/{owner}/{repo}/ai/settings - Get repository AI configuration
- PATCH /repos/{owner}/{repo}/ai/settings - Update AI settings (admin only)

Settings include Tier 1 operations (auto-respond, auto-review), Tier 2 agent mode, escalation rules, provider/model preferences, and custom instructions. All endpoints validate AI unit enablement and permissions.
2026-02-12 00:03:04 -05:00
8ad6664b92 feat(ai): add ai service layer with agent and queue system
Implement core AI service infrastructure including agent operations, escalation handling, and asynchronous queue processing.

New services:
- Agent service: Handles Tier 2 AI operations with action runner integration
- Queue service: Asynchronous processing of AI operations with retry logic
- Escalation service: Routes complex issues to staff with configurable rules
- Notifier service: Sends notifications for AI operation results

Additional changes:
- Add GitCaddy AI system user (ID: -3) for bot operations
- Add AIConfig to repository units
- Add AI-specific error codes (rate limiting, service errors, etc.)
- Extend AI client with GenerateIssueResponse method
- Add AISettingsV2 struct for repository-level AI configuration

The queue system enables non-blocking AI operations with proper error handling and rate limiting.
2026-02-12 00:02:49 -05:00
26793bf898 feat(ai): add ai operation logging and org settings models
Add database models and infrastructure for AI operation tracking and organization-level AI configuration.

OperationLog model tracks all AI operations for auditing, including:
- Operation type, tier, and trigger event
- Token usage (input/output)
- Status tracking (pending, success, failed, escalated)
- Performance metrics (duration)
- Rate limiting support via CountRecentOperations

OrgAISettings model stores per-organization AI configuration:
- Provider and model selection
- Encrypted API key storage
- Rate limits (max operations per hour)
- Allowed operations whitelist
- Agent mode permissions

Also adds AI unit type to repository units for enabling/disabling AI features per repo.
2026-02-11 23:46:57 -05:00
7102167351 feat(repo): add public app integration toggle for repositories
All checks were successful
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m21s
Build and Release / Unit Tests (push) Successful in 7m26s
Build and Release / Lint (push) Successful in 7m47s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m12s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h5m1s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m1s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 7m14s
Build and Release / Build Binary (linux/arm64) (push) Successful in 9m17s
Add repository setting to control anonymous access to app integration endpoints (issue submission, update checks). When enabled (default), the desktop app can access these endpoints without authentication. When disabled, vault token authentication is required.

This provides granular control over app integration access, allowing repository owners to enforce full authentication on sensitive repositories while maintaining ease of use for public/limited repos.

Changes include:
- New PublicAppIntegration boolean field on Repository model
- Database migration v365 to add the field (defaults to true)
- Repository settings UI to toggle the feature
- Updated checkVaultTokenForRepo to respect the setting
- Security enhancement: IssueStatusJSONEndpoint now only returns app-submitted issues to anonymous users
2026-02-11 20:32:23 -05:00
dcceabc713 feat(admin): add bulk delete for packages
All checks were successful
Build and Release / Unit Tests (push) Successful in 3m32s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m50s
Build and Release / Lint (push) Successful in 4m55s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m5s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m32s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h4m55s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m23s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m23s
Adds bulk delete functionality to admin packages page, allowing deletion of multiple packages and all their versions at once. Includes confirmation dialog, success/warning flash messages showing count of deleted packages and versions, and proper error handling. Adds new locale strings for delete action, confirmation prompt, and result messages.
2026-02-09 00:22:35 -05:00
9bdd35284f chore(deps): bump gitcaddy-vault from v1.0.59 to v1.0.60
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m13s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m5s
Build and Release / Lint (push) Successful in 5m16s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 2m55s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m6s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m36s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m28s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m48s
2026-02-08 11:17:56 -05:00
33858109d5 chore(deps): bump gitcaddy-vault to v1.0.59
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m17s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m53s
Build and Release / Lint (push) Successful in 5m14s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 2m53s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m24s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m24s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m45s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m53s
2026-02-08 10:44:00 -05:00
b2adcdf969 feat(packages): add bulk visibility management for packages
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m12s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m8s
Build and Release / Lint (push) Successful in 5m19s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m11s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m38s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m9s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m29s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 10m38s
Add ability to bulk set packages as private or public in both admin and repository package views. Includes new bulk action buttons, visibility grouping in repository view, and corresponding backend handlers for processing visibility changes. Admin can manage all packages while repository owners can manage their own packages.
2026-02-07 15:02:16 -05:00
fb2d53ba7a refactor(ui): replace link-action with async methods for job reruns
All checks were successful
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 3m23s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m19s
Build and Release / Lint (push) Successful in 5m22s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Replace link-action class with proper async methods (rerunJob and rerunAll) for handling job and workflow reruns in RepoActionView. This provides better control over the rerun flow and ensures proper page reload after the POST request completes.
2026-02-07 14:22:29 -05:00
ae9cb010ab chore(deps): bump gitcaddy-vault from v1.0.57 to v1.0.58
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m24s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m58s
Build and Release / Lint (push) Successful in 5m11s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 2m59s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h4m22s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m44s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m14s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m14s
2026-02-07 02:17:39 -05:00
d42b629458 Update go.mod
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m36s
Build and Release / Lint (push) Successful in 3m50s
Build and Release / Unit Tests (push) Successful in 4m11s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m1s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m39s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m57s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m25s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m56s
2026-02-06 22:24:59 -05:00
68d4264f9e Update go.mod
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m20s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m49s
Build and Release / Lint (push) Successful in 4m56s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 2m56s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h4m43s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m46s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m32s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m17s
2026-02-06 21:51:21 -05:00
fb8124d8f1 chore(deps): bump gitcaddy-vault from v1.0.54 to v1.0.55
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m7s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m30s
Build and Release / Lint (push) Successful in 5m35s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 2m52s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m39s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m29s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m10s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m2s
2026-02-06 19:22:18 -05:00
a10dbda7ac feat(blog): add view count tracking to blog posts
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m22s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m10s
Build and Release / Lint (push) Successful in 4m54s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m0s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m43s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m39s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 7m46s
Build and Release / Build Binary (linux/arm64) (push) Successful in 9m6s
Adds view_count field to blog_post table with database migration. Implements atomic increment on post views in both standalone and repo blog routes. Displays view count with eye icon in post templates.
2026-02-04 17:11:52 -05:00
f6e54207dc chore(deps): bump gitcaddy-vault from v1.0.52 to v1.0.54
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m11s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m11s
Build and Release / Lint (push) Successful in 5m18s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 2m51s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m36s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 7m2s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 7m20s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m12s
2026-02-04 14:10:50 -05:00
5c9385f4a2 feat(vault): add user-facing warnings for key configuration issues
Adds dedicated error page and warnings for vault encryption key problems including missing configuration, fallback key usage, and decryption failures. Displays context-specific messages to help users understand and fix key configuration issues. Includes detection of crypto errors in vault operations and graceful error handling throughout the UI.
2026-02-04 13:54:54 -05:00
53d4c529a7 perf(blog): limit tag scan to 500 most recent posts
Improve GetExploreTopTags performance by limiting the tag aggregation scan to the 500 most recent published posts instead of scanning all posts. This reduces database load while still providing relevant trending tags.
2026-02-03 17:13:49 -05:00
e19874f4b3 fix(blog): improve blog metadata display formatting
Simplify blog header to show owner/repo format without redundant "Blog" label. Add fallback to CreatedUnix when PublishedUnix is not available for featured posts.
2026-02-03 15:21:39 -05:00
33898561db feat(blog): add subscription-only blog post detection and UI updates
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m20s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m54s
Build and Release / Lint (push) Successful in 5m7s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m1s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h4m41s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 8m37s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 8m40s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m53s
Add HasSubscriptionOnlyBlogPosts function to check if a repository has premium blog content. Update subscribe page to display different messaging when paid blogs are available. Redesign featured blog section with centered layout, larger images, and improved hover effects.
2026-02-03 10:26:28 -05:00
f40550d79b feat(subscriptions): add subscription plan UI and localization
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m23s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m18s
Build and Release / Lint (push) Successful in 5m29s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m3s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m55s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 7m17s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 7m57s
Build and Release / Build Binary (linux/arm64) (push) Failing after 2m42s
Adds locale strings for subscription plan types (monthly/yearly/lifetime), pricing display, payment buttons, and success/empty states. Updates subscribe page title to include repository name for better context.
2026-02-03 10:01:53 -05:00
199 changed files with 91416 additions and 58805 deletions

View File

@@ -16,7 +16,7 @@ env:
GOPRIVATE: git.marketally.com
GONOSUMDB: git.marketally.com
GOTOOLCHAIN: local
GO_VERSION: "1.25.0"
GO_VERSION: "1.25.9"
NODE_VERSION: "22"
jobs:
@@ -24,9 +24,13 @@ jobs:
lint:
name: Lint
runs-on: linux-latest
env:
GOMODCACHE: /tmp/gomod-${{ github.run_id }}-lint
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_TOKEN }}
- name: Clone vault dependency
env:
@@ -40,9 +44,6 @@ jobs:
go-version: ${{ env.GO_VERSION }}
cache: false
- name: Tidy modules
run: go mod tidy
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -52,7 +53,8 @@ jobs:
run: npm install -g pnpm
- name: Install dependencies
run: make deps-frontend deps-backend
run: |
make deps-frontend deps-backend
- name: Run Go linter
run: make lint-go
@@ -65,9 +67,13 @@ jobs:
test-unit:
name: Unit Tests
runs-on: linux-latest
env:
GOMODCACHE: /tmp/gomod-${{ github.run_id }}-unit
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_TOKEN }}
- name: Clone vault dependency
env:
@@ -82,7 +88,8 @@ jobs:
cache: false
- name: Install dependencies
run: go mod download
run: |
go mod download
- name: Run unit tests
run: |
@@ -98,6 +105,8 @@ jobs:
test-pgsql:
name: Integration Tests (PostgreSQL)
runs-on: linux-latest
env:
GOMODCACHE: /tmp/gomod-${{ github.run_id }}-pgsql
services:
pgsql:
image: postgres:15
@@ -115,6 +124,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_TOKEN }}
- name: Clone vault dependency
env:
@@ -128,9 +139,6 @@ jobs:
go-version: ${{ env.GO_VERSION }}
cache: false
- name: Tidy modules
run: go mod tidy
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -140,7 +148,8 @@ jobs:
run: npm install -g pnpm
- name: Install dependencies
run: make deps-frontend deps-backend
run: |
make deps-frontend deps-backend
- name: Build frontend
run: make frontend
@@ -245,7 +254,7 @@ jobs:
if: matrix.goos != 'windows'
id: vault
run: |
VERSION=$(curl -sf "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
VERSION=$(curl -sf -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
echo "version=$VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
@@ -254,7 +263,7 @@ jobs:
id: vault-win
shell: pwsh
run: |
$response = Invoke-RestMethod -Uri "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest"
$response = Invoke-RestMethod -Uri "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" -Headers @{ Authorization = "token ${{ secrets.RELEASE_TOKEN }}" }
$version = $response.tag_name
Write-Host "version=$version"
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
@@ -262,6 +271,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_TOKEN }}
fetch-depth: 0
- name: Configure git line endings (Windows)
@@ -337,8 +347,8 @@ jobs:
- name: Update vault dependency (Unix)
if: matrix.goos != 'windows'
env:
GOPRIVATE: ""
GOPROXY: direct
GOSUMDB: off
run: |
set -x
VAULT_VERSION="${{ steps.vault.outputs.version }}"
@@ -350,14 +360,14 @@ jobs:
sed -i "s|replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault|replace git.marketally.com/gitcaddy/gitcaddy-vault => git.marketally.com/gitcaddy/gitcaddy-vault $VAULT_VERSION|" go.mod
fi
cat go.mod | grep -A2 "gitcaddy-vault" || true
go mod tidy -v 2>&1
go mod download git.marketally.com/gitcaddy/gitcaddy-vault
- name: Update vault dependency (Windows)
if: matrix.goos == 'windows'
shell: pwsh
env:
GOPRIVATE: ""
GOPROXY: direct
GOSUMDB: off
run: |
$vaultVersion = "${{ steps.vault-win.outputs.version }}"
Write-Host "Building with vault $vaultVersion"
@@ -365,7 +375,7 @@ jobs:
$content = $content -replace 'replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault', "replace git.marketally.com/gitcaddy/gitcaddy-vault => git.marketally.com/gitcaddy/gitcaddy-vault $vaultVersion"
Set-Content go.mod $content -NoNewline
Get-Content go.mod | Select-String "gitcaddy/vault" -Context 0,2
go mod tidy
go mod download git.marketally.com/gitcaddy/gitcaddy-vault
- name: Setup Node.js
uses: actions/setup-node@v4
@@ -377,7 +387,8 @@ jobs:
- name: Install dependencies (Unix)
if: matrix.goos != 'windows'
run: make deps-frontend deps-backend
run: |
make deps-frontend deps-backend
- name: Install dependencies (Windows)
if: matrix.goos == 'windows'
@@ -421,7 +432,7 @@ jobs:
TAGS: bindata sqlite sqlite_unlock_notify
run: |
VERSION=$(git describe --tags --always 2>/dev/null | sed "s/-gitcaddy//" || echo "dev")
VAULT_VERSION=$(curl -sf "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4 || echo "dev")
VAULT_VERSION=$(curl -sf -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4 || echo "dev")
LDFLAGS="-X main.Version=${VERSION} -X git.marketally.com/gitcaddy/gitcaddy-vault.Version=${VAULT_VERSION}"
OUTPUT="gitcaddy-server-${VERSION}-${GOOS}-${GOARCH}"
@@ -457,7 +468,7 @@ jobs:
$VERSION = (git describe --tags --always 2>$null) -replace '-gitcaddy',''
if (-not $VERSION) { $VERSION = "dev" }
$VAULT_VERSION = (Invoke-RestMethod -Uri "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" -ErrorAction SilentlyContinue).tag_name
$VAULT_VERSION = (Invoke-RestMethod -Uri "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" -Headers @{ Authorization = "token ${{ secrets.RELEASE_TOKEN }}" } -ErrorAction SilentlyContinue).tag_name
if (-not $VAULT_VERSION) { $VAULT_VERSION = "dev" }
$LDFLAGS = "-X main.Version=$VERSION -X git.marketally.com/gitcaddy/gitcaddy-vault.Version=$VAULT_VERSION"
$OUTPUT = "gitcaddy-server-$VERSION-$env:GOOS-$env:GOARCH.exe"
@@ -584,7 +595,7 @@ jobs:
- name: Get latest vault version
id: vault
run: |
VERSION=$(curl -sf "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
VERSION=$(curl -sf -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
echo "version=$VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
@@ -598,6 +609,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_TOKEN }}
fetch-depth: 0
- name: Configure private repo access
@@ -625,7 +637,7 @@ jobs:
echo "Building with vault $VAULT_VERSION"
sed -i "s|replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault|replace git.marketally.com/gitcaddy/gitcaddy-vault => git.marketally.com/gitcaddy/gitcaddy-vault $VAULT_VERSION|" go.mod
cat go.mod | grep -A2 "gitcaddy-vault" || true
/usr/local/go/bin/go mod tidy -v 2>&1
/usr/local/go/bin/go mod download
- name: Install pnpm
run: npm install -g pnpm
@@ -646,7 +658,7 @@ jobs:
- name: Build binary
run: |
VERSION=$(git describe --tags --always 2>/dev/null | sed "s/-gitcaddy//" || echo "dev")
VAULT_VERSION=$(curl -sf "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4 || echo "dev")
VAULT_VERSION=$(curl -sf -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4 || echo "dev")
LDFLAGS="-X main.Version=${VERSION} -X git.marketally.com/gitcaddy/gitcaddy-vault.Version=${VAULT_VERSION}"
OUTPUT="gitcaddy-server-${VERSION}-linux-arm64"
TAGS="bindata sqlite sqlite_unlock_notify"

View File

@@ -21,6 +21,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_TOKEN }}
- name: Setup Go
uses: actions/setup-go@v5
@@ -64,6 +66,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_TOKEN }}
- name: Setup Go
uses: actions/setup-go@v5
@@ -91,6 +95,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4

View File

@@ -0,0 +1,8 @@
{
"id": "note-1770858253427-5d5d6gaia",
"title": "Issues/Releases",
"content": " Access matrix after changes\n ┌────────────────────────┬────────────┬────────────┬────────────┬─────────────┐\n │ Endpoint │ Public │ Limited │ Private │ Private Org │\n ├────────────────────────┼────────────┼────────────┼────────────┼─────────────┤\n │ POST submit.json │ anonymous │ anonymous │ anonymous │ anonymous │\n ├────────────────────────┼────────────┼────────────┼────────────┼─────────────┤\n │ GET status.json │ anonymous* │ anonymous* │ anonymous* │ anonymous* │\n ├────────────────────────┼────────────┼────────────┼────────────┼─────────────┤\n │ GET external/{id}.json │ anonymous │ anonymous │ anonymous │ anonymous │\n ├────────────────────────┼────────────┼────────────┼────────────┼─────────────┤\n │ GET latest.json │ anonymous │ anonymous │ anonymous │ anonymous │\n └────────────────────────┴────────────┴────────────┴────────────┴─────────────┘\n * Only returns issues where external_source = \"gitcaddy-desktop\"",
"createdAt": 1770858253425,
"updatedAt": 1770858258193,
"tags": []
}

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 gitcaddy
Copyright (c) 2026 GitCaddy
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including

View File

@@ -763,6 +763,12 @@ generate-go: $(TAGS_PREREQ)
@echo "Running go generate..."
@CC= GOOS= GOARCH= CGO_ENABLED=0 $(GO) generate -tags '$(TAGS)' ./...
.PHONY: generate-plugin-proto
generate-plugin-proto:
protoc --go_out=. --go_opt=paths=source_relative \
--connect-go_out=. --connect-go_opt=paths=source_relative \
modules/plugins/pluginv1/plugin.proto
.PHONY: security-check
security-check:
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...

577
PLUGINS.md Normal file
View File

@@ -0,0 +1,577 @@
# GitCaddy Plugin Development Guide
This guide explains how to build external plugins for GitCaddy. Plugins are standalone services that communicate with the server over gRPC (HTTP/2) using a well-defined protocol.
## Table of Contents
- [Overview](#overview)
- [Protocol](#protocol)
- [Service Definition](#service-definition)
- [Lifecycle](#lifecycle)
- [Messages](#messages)
- [Plugin Manifest](#plugin-manifest)
- [Routes](#routes)
- [Events](#events)
- [Permissions](#permissions)
- [Health Monitoring](#health-monitoring)
- [Protocol Versioning](#protocol-versioning)
- [Configuration](#configuration)
- [External Mode](#external-mode)
- [Managed Mode](#managed-mode)
- [Configuration Reference](#configuration-reference)
- [Transport](#transport)
- [Example: Go Plugin](#example-go-plugin)
- [Example: C# Plugin](#example-c-plugin)
- [Example: Python Plugin](#example-python-plugin)
- [Debugging](#debugging)
## Overview
A GitCaddy plugin is any process that implements the `PluginService` gRPC interface. The server connects to the plugin on startup, calls `Initialize` to get its manifest, and then:
- **Health checks** the plugin periodically (default: every 30 seconds)
- **Dispatches events** the plugin has subscribed to (e.g., `license:updated`)
- **Proxies HTTP requests** to routes the plugin has declared
- **Shuts down** the plugin gracefully when the server stops
Plugins can run in two modes:
- **External mode** - The plugin runs independently (Docker, systemd, etc.). The server connects to it.
- **Managed mode** - The server launches the plugin binary and manages its process lifecycle.
## Protocol
The protocol is defined in [`modules/plugins/pluginv1/plugin.proto`](modules/plugins/pluginv1/plugin.proto).
### Service Definition
```protobuf
service PluginService {
rpc Initialize(InitializeRequest) returns (InitializeResponse);
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
rpc GetManifest(GetManifestRequest) returns (PluginManifest);
rpc OnEvent(PluginEvent) returns (EventResponse);
rpc HandleHTTP(HTTPRequest) returns (HTTPResponse);
}
```
All 6 RPCs are unary (request-response). The server is the client; the plugin is the server.
### Lifecycle
```
Server starts
Initialize(server_version, config)
│ Plugin returns: success + PluginManifest
Plugin is ONLINE
├──► HealthCheck() every 30s
│ Plugin returns: healthy, status, details
├──► OnEvent(event_type, payload, repo_id, org_id)
│ Dispatched for subscribed events (fire-and-forget with 30s timeout)
├──► HandleHTTP(method, path, headers, body)
│ Proxied when an incoming request matches a declared route
Server shutting down
Shutdown(reason)
│ Plugin returns: success
Plugin process is sent SIGINT (managed mode only)
```
### Messages
#### InitializeRequest
| Field | Type | Description |
|-------|------|-------------|
| `server_version` | string | The GitCaddy server version (e.g., `"3.0.0"`) |
| `config` | map<string, string> | Server-provided configuration key-value pairs |
| `protocol_version` | int32 | Plugin protocol version the server supports (current: `1`). `0` means pre-versioning. |
#### InitializeResponse
| Field | Type | Description |
|-------|------|-------------|
| `success` | bool | Whether initialization succeeded |
| `error` | string | Error message if `success` is false |
| `manifest` | PluginManifest | The plugin's capability manifest |
| `protocol_version` | int32 | Plugin protocol version the plugin supports (current: `1`). `0` means pre-versioning, treated as `1`. |
#### HealthCheckRequest
Empty message. No fields.
#### HealthCheckResponse
| Field | Type | Description |
|-------|------|-------------|
| `healthy` | bool | Whether the plugin considers itself healthy |
| `status` | string | Human-readable status (e.g., `"operational"`, `"degraded"`) |
| `details` | map<string, string> | Arbitrary key-value details (version, uptime, etc.) |
#### PluginEvent
| Field | Type | Description |
|-------|------|-------------|
| `event_type` | string | The event name (e.g., `"license:updated"`, `"repo:push"`) |
| `payload` | google.protobuf.Struct | Event-specific data as a JSON-like structure |
| `timestamp` | google.protobuf.Timestamp | When the event occurred |
| `repo_id` | int64 | Repository ID (0 if not repo-specific) |
| `org_id` | int64 | Organization ID (0 if not org-specific) |
#### EventResponse
| Field | Type | Description |
|-------|------|-------------|
| `handled` | bool | Whether the plugin handled the event |
| `error` | string | Error message if handling failed |
#### HTTPRequest / HTTPResponse
| Field | Type | Description |
|-------|------|-------------|
| `method` | string | HTTP method (`GET`, `POST`, etc.) |
| `path` | string | Request path (e.g., `/api/v1/health`) |
| `headers` | map<string, string> | HTTP headers |
| `body` | bytes | Request/response body |
| `query_params` | map<string, string> | Query parameters (request only) |
| `status_code` | int32 | HTTP status code (response only) |
## Plugin Manifest
The manifest declares what your plugin does. It is returned during `Initialize` and can be re-fetched via `GetManifest`.
```protobuf
message PluginManifest {
string name = 1;
string version = 2;
string description = 3;
repeated string subscribed_events = 4;
repeated PluginRoute routes = 5;
repeated string required_permissions = 6;
string license_tier = 7;
}
```
| Field | Description | Example |
|-------|-------------|---------|
| `name` | Display name | `"My Analytics Plugin"` |
| `version` | Semver version | `"1.2.0"` |
| `description` | What the plugin does | `"Tracks repository analytics"` |
| `subscribed_events` | Events to receive | `["repo:push", "issue:created"]` |
| `routes` | HTTP routes the plugin handles | See below |
| `required_permissions` | Permissions the plugin needs | `["repo:read", "issue:write"]` |
| `license_tier` | Minimum license tier required | `"standard"`, `"professional"`, `"enterprise"` |
### Routes
Routes declare which HTTP paths your plugin handles. When the server receives a request matching a plugin's route, it proxies the request via `HandleHTTP`.
```protobuf
message PluginRoute {
string method = 1; // "GET", "POST", etc.
string path = 2; // "/api/v1/my-plugin/endpoint"
string description = 3;
}
```
Route matching uses prefix matching: a declared path of `/api/v1/analytics` will match `/api/v1/analytics/events`.
### Events
Subscribe to server events by listing them in `subscribed_events`. Use `"*"` to subscribe to all events.
Available events include:
- `license:updated` - License key changed
- `repo:push` - Code pushed to a repository
- `repo:created` - New repository created
- `issue:created` - New issue opened
- `issue:comment` - Comment added to an issue
- `pull_request:opened` - New pull request opened
- `pull_request:merged` - Pull request merged
Events are dispatched asynchronously (fire-and-forget) with a 30-second timeout per plugin.
### Permissions
The `required_permissions` field declares what server resources your plugin needs access to. The server logs these at startup for admin review.
## Health Monitoring
The server health-checks every registered plugin at a configurable interval (default: 30 seconds).
**Behavior:**
| Consecutive Failures | Action |
|---------------------|--------|
| 1-2 | Warning logged, plugin stays online |
| 3+ | Plugin marked **offline**, error logged |
| 3+ (managed mode) | Automatic restart attempted |
When a previously offline plugin responds to a health check, it is marked **online** and an info log is emitted.
If `HealthCheckResponse.healthy` is `false` (the RPC succeeds but the plugin reports unhealthy), the plugin is marked as **error** status. This allows plugins to report degraded operation (e.g., missing API key, expired license) without being treated as crashed.
**Health check timeout** is configured per-plugin via `HEALTH_TIMEOUT` (default: 5 seconds).
## Protocol Versioning
The plugin protocol uses explicit version negotiation to ensure forward compatibility. Both the server and plugin exchange their supported protocol version during `Initialize`:
1. The server sends `protocol_version = 1` in `InitializeRequest`
2. The plugin returns `protocol_version = 1` in `InitializeResponse`
3. The server stores the plugin's version and checks it before calling any RPCs added in later versions
**What this means for plugin developers:**
- **You don't need to recompile** when the server adds new fields to existing messages. Protobuf handles this automatically — unknown fields are ignored, missing fields use zero-value defaults.
- **You don't need to recompile** when the server adds new event types. Your plugin only receives events it subscribed to.
- **You only need to update** if you want to use features from a newer protocol version (e.g., new RPCs added in protocol v2).
**Version history:**
| Version | RPCs | Notes |
|---------|------|-------|
| 1 | Initialize, Shutdown, HealthCheck, GetManifest, OnEvent, HandleHTTP | Initial release |
**Pre-versioning plugins** (those that don't set `protocol_version` in their response) return `0`, which the server treats as version `1`. This means all existing plugins are compatible without changes.
## Configuration
Plugins are configured in the server's `app.ini`.
### External Mode
The plugin runs independently. The server connects to its gRPC endpoint.
```ini
[plugins]
ENABLED = true
HEALTH_CHECK_INTERVAL = 30s
[plugins.my-plugin]
ENABLED = true
ADDRESS = localhost:9090
HEALTH_TIMEOUT = 5s
SUBSCRIBED_EVENTS = repo:push, issue:created
```
### Managed Mode
The server launches the plugin binary and manages its lifecycle. If the plugin crashes, the server restarts it automatically.
```ini
[plugins.my-plugin]
ENABLED = true
BINARY = /opt/plugins/my-plugin
ARGS = --port 9090 --log-level info
ADDRESS = localhost:9090
HEALTH_TIMEOUT = 5s
```
When `BINARY` is set, the server:
1. Starts the process with the specified arguments
2. Waits 2 seconds for the process to initialize
3. Calls `Initialize` via gRPC
4. Sends `SIGINT` on server shutdown
5. Auto-restarts the process if health checks fail 3 consecutive times
### Configuration Reference
#### `[plugins]` Section
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `ENABLED` | bool | `true` | Master switch for the plugin framework |
| `PATH` | string | `data/plugins` | Directory for plugin data |
| `HEALTH_CHECK_INTERVAL` | duration | `30s` | How often to health-check plugins |
#### `[plugins.<name>]` Section
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `ENABLED` | bool | `true` | Whether this plugin is active |
| `ADDRESS` | string | (required) | gRPC endpoint (e.g., `localhost:9090`) |
| `BINARY` | string | (optional) | Path to plugin binary (enables managed mode) |
| `ARGS` | string | (optional) | Arguments for the binary |
| `HEALTH_TIMEOUT` | duration | `5s` | Timeout for health check RPCs |
| `SUBSCRIBED_EVENTS` | string | (optional) | Comma-separated event names |
A plugin must have either `BINARY` or `ADDRESS` (or both for managed mode). Entries with neither are skipped with a warning.
## Transport
Plugins communicate over **cleartext HTTP/2 (h2c)** by default. The server uses the gRPC wire protocol via [Connect RPC](https://connectrpc.com/).
**Requirements for your plugin's gRPC server:**
- Listen on a TCP port
- Support HTTP/2 (standard for any gRPC server)
- No TLS required for local communication (h2c)
The server constructs its gRPC client with `connect.WithGRPC()`, which uses the standard gRPC binary protocol. This means your plugin can use **any** gRPC server implementation:
| Language | gRPC Library |
|----------|-------------|
| Go | `google.golang.org/grpc` or `connectrpc.com/connect` |
| C# | `Grpc.AspNetCore` |
| Python | `grpcio` |
| Java | `io.grpc` |
| Rust | `tonic` |
| Node.js | `@grpc/grpc-js` |
## Example: Go Plugin
```go
package main
import (
"context"
"log"
"net/http"
"connectrpc.com/connect"
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
"code.gitcaddy.com/server/v3/modules/plugins/pluginv1/pluginv1connect"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
type myPlugin struct {
pluginv1connect.UnimplementedPluginServiceHandler
}
func (p *myPlugin) Initialize(
ctx context.Context,
req *connect.Request[pluginv1.InitializeRequest],
) (*connect.Response[pluginv1.InitializeResponse], error) {
log.Printf("Initialized by server %s (protocol v%d)", req.Msg.ServerVersion, req.Msg.ProtocolVersion)
return connect.NewResponse(&pluginv1.InitializeResponse{
Success: true,
ProtocolVersion: 1,
Manifest: &pluginv1.PluginManifest{
Name: "My Plugin",
Version: "1.0.0",
Description: "Does something useful",
SubscribedEvents: []string{"repo:push"},
Routes: []*pluginv1.PluginRoute{
{Method: "GET", Path: "/api/v1/my-plugin/status", Description: "Plugin status"},
},
},
}), nil
}
func (p *myPlugin) HealthCheck(
ctx context.Context,
req *connect.Request[pluginv1.HealthCheckRequest],
) (*connect.Response[pluginv1.HealthCheckResponse], error) {
return connect.NewResponse(&pluginv1.HealthCheckResponse{
Healthy: true,
Status: "operational",
Details: map[string]string{"version": "1.0.0"},
}), nil
}
func (p *myPlugin) Shutdown(
ctx context.Context,
req *connect.Request[pluginv1.ShutdownRequest],
) (*connect.Response[pluginv1.ShutdownResponse], error) {
log.Printf("Shutdown requested: %s", req.Msg.Reason)
return connect.NewResponse(&pluginv1.ShutdownResponse{Success: true}), nil
}
func (p *myPlugin) OnEvent(
ctx context.Context,
req *connect.Request[pluginv1.PluginEvent],
) (*connect.Response[pluginv1.EventResponse], error) {
log.Printf("Event: %s for repo %d", req.Msg.EventType, req.Msg.RepoId)
return connect.NewResponse(&pluginv1.EventResponse{Handled: true}), nil
}
func main() {
mux := http.NewServeMux()
path, handler := pluginv1connect.NewPluginServiceHandler(&myPlugin{})
mux.Handle(path, handler)
server := &http.Server{
Addr: ":9090",
Handler: h2c.NewHandler(mux, &http2.Server{}),
}
log.Println("Plugin listening on :9090")
log.Fatal(server.ListenAndServe())
}
```
**app.ini:**
```ini
[plugins.my-plugin]
ENABLED = true
ADDRESS = localhost:9090
SUBSCRIBED_EVENTS = repo:push
```
## Example: C# Plugin
```csharp
using Grpc.Core;
// Assumes plugin.proto is included in the .csproj with GrpcServices="Server"
public class MyPlugin : PluginService.PluginServiceBase
{
public override Task<InitializeResponse> Initialize(
InitializeRequest request, ServerCallContext context)
{
var manifest = new PluginManifest
{
Name = "My C# Plugin",
Version = "1.0.0",
Description = "A C# plugin for GitCaddy"
};
manifest.SubscribedEvents.Add("issue:created");
return Task.FromResult(new InitializeResponse
{
Success = true,
ProtocolVersion = 1,
Manifest = manifest
});
}
public override Task<HealthCheckResponse> HealthCheck(
HealthCheckRequest request, ServerCallContext context)
{
return Task.FromResult(new HealthCheckResponse
{
Healthy = true,
Status = "operational"
});
}
public override Task<ShutdownResponse> Shutdown(
ShutdownRequest request, ServerCallContext context)
{
return Task.FromResult(new ShutdownResponse { Success = true });
}
public override Task<EventResponse> OnEvent(
PluginEvent request, ServerCallContext context)
{
Console.WriteLine($"Event: {request.EventType} for repo {request.RepoId}");
return Task.FromResult(new EventResponse { Handled = true });
}
}
```
**Program.cs:**
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(9090, o =>
o.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http2);
});
var app = builder.Build();
app.MapGrpcService<MyPlugin>();
app.Run();
```
## Example: Python Plugin
```python
import grpc
from concurrent import futures
# Generated from plugin.proto using grpcio-tools:
# python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. plugin.proto
import plugin_pb2
import plugin_pb2_grpc
class MyPlugin(plugin_pb2_grpc.PluginServiceServicer):
def Initialize(self, request, context):
manifest = plugin_pb2.PluginManifest(
name="My Python Plugin",
version="1.0.0",
description="A Python plugin for GitCaddy",
subscribed_events=["repo:push"],
)
return plugin_pb2.InitializeResponse(success=True, protocol_version=1, manifest=manifest)
def HealthCheck(self, request, context):
return plugin_pb2.HealthCheckResponse(
healthy=True,
status="operational",
details={"version": "1.0.0"},
)
def Shutdown(self, request, context):
print(f"Shutdown: {request.reason}")
return plugin_pb2.ShutdownResponse(success=True)
def OnEvent(self, request, context):
print(f"Event: {request.event_type} for repo {request.repo_id}")
return plugin_pb2.EventResponse(handled=True)
def HandleHTTP(self, request, context):
return plugin_pb2.HTTPResponse(status_code=501)
def GetManifest(self, request, context):
return plugin_pb2.PluginManifest(
name="My Python Plugin",
version="1.0.0",
)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
plugin_pb2_grpc.add_PluginServiceServicer_to_server(MyPlugin(), server)
server.add_insecure_port("[::]:9090")
server.start()
print("Plugin listening on :9090")
server.wait_for_termination()
if __name__ == "__main__":
serve()
```
## Debugging
**Server logs** show plugin lifecycle events:
```
[I] Loaded external plugin config: my-plugin (managed=false)
[I] External plugin my-plugin is online (managed=false)
[W] Health check failed for plugin my-plugin: connection refused
[E] Plugin my-plugin is now offline after 3 consecutive health check failures
[I] Plugin my-plugin is back online
[I] Shutting down external plugin: my-plugin
```
**Tips:**
- Use `HEALTH_TIMEOUT = 10s` during development to avoid false positives
- Set `HEALTH_CHECK_INTERVAL = 5s` for faster feedback during testing
- Check that your plugin supports cleartext HTTP/2 (h2c) - this is the most common connection issue
- Use `grpcurl` to test your plugin's gRPC service independently:
```bash
# List services
grpcurl -plaintext localhost:9090 list
# Call HealthCheck
grpcurl -plaintext localhost:9090 plugin.v1.PluginService/HealthCheck
# Call Initialize
grpcurl -plaintext -d '{"server_version": "3.0.0"}' \
localhost:9090 plugin.v1.PluginService/Initialize
```

321
README.md
View File

@@ -17,6 +17,11 @@ The AI-native Git platform. Self-hosted, fast, and designed for the age of AI-as
- [Package Registry](#package-registry)
- [Vault System (Pro/Enterprise)](#vault-system-proenterprise)
- [AI-Powered Features](#ai-powered-features)
- [Tier 1 - Light Operations](#ai-powered-features)
- [Tier 2 - Agent Operations](#ai-powered-features)
- [Configuration Cascade](#ai-powered-features)
- [Built-in Safety](#ai-powered-features)
- [Plugin Framework](#plugin-framework)
- [Landing Pages & Public Releases](#landing-pages--public-releases)
- [App Update API (Electron/Squirrel Compatible)](#app-update-api-electronsquirrel-compatible)
- [Release Archive](#release-archive)
@@ -30,6 +35,8 @@ The AI-native Git platform. Self-hosted, fast, and designed for the age of AI-as
- [Configuration](#configuration)
- [Database Setup](#database-setup)
- [AI Features Configuration](#ai-features-configuration)
- [Plugin Framework Configuration](#plugin-framework-configuration)
- [V2 API Configuration](#v2-api-configuration)
- [Authentication Sources](#authentication-sources)
- [Email/SMTP Setup](#emailsmtp-setup)
- [Unsplash Integration](#unsplash-integration)
@@ -42,6 +49,11 @@ The AI-native Git platform. Self-hosted, fast, and designed for the age of AI-as
- [Vault Usage (Pro/Enterprise)](#vault-usage-proenterprise)
- [Landing Pages Configuration](#landing-pages-configuration)
- [AI Features Usage](#ai-features-usage)
- [Automatic Operations](#automatic-operations-event-driven)
- [Manual API Triggers](#manual-api-triggers)
- [Viewing AI Operation History](#viewing-ai-operation-history)
- [Escalation](#escalation)
- [AI Context APIs](#ai-context-apis-for-external-ai-tools)
- [GitCaddy Runner](#gitcaddy-runner)
- [API Documentation](#api-documentation)
- [Internationalization](#internationalization)
@@ -262,10 +274,43 @@ Enterprise-grade encrypted secrets management:
### AI-Powered Features
- **AI Code Review:** Automated code review suggestions on pull requests
- **Issue Triage:** Automatic categorization and priority assignment
- **Code Explanation:** Generate documentation and explain complex code
- **Error Diagnosis:** AI learning patterns for debugging assistance
GitCaddy integrates with the [gitcaddy-ai](https://git.marketally.com/gitcaddy/gitcaddy-ai) sidecar service to provide two tiers of AI operations:
**Tier 1 - Light Operations** (seconds, via gRPC/REST to gitcaddy-ai):
- **AI Code Review:** Automatic review comments on pull requests when opened or updated
- **Issue Auto-Respond:** AI generates helpful responses to new issues and @bot mentions
- **Issue Triage:** Automatic label assignment based on issue content
- **Code Explanation:** On-demand explanation of code sections
- **Documentation Generation:** Generate docs for code files
**Tier 2 - Agent Operations** (minutes, via Actions runners with Claude Code):
- **Agent Fix:** AI agent clones the repo, investigates issues, creates branches and pull requests
- **Triggered by labels** (e.g., adding `ai-fix` label) or manual API call
- **Sandboxed** in existing Actions runner infrastructure
**Configuration Cascade:**
Settings resolve from most specific to least: Repo > Org > System. Each level can override the AI provider, model, and API key.
**Built-in Safety:**
- Dedicated bot user (`gitcaddy-ai`) for clear attribution
- Loop prevention: bot-created events never re-trigger AI
- Per-repo rate limiting (ops/hour)
- Admin feature gates for every operation type
- Escalation to staff when AI confidence is low or operations fail
- Full audit log of every AI operation with provider, tokens, duration, and status
### Plugin Framework
Extend GitCaddy with external plugins that communicate over gRPC (HTTP/2). The server manages plugin lifecycle, health monitoring, and event dispatch.
- **gRPC Protocol**: Type-safe plugin contract defined in `plugin.proto` with 6 RPCs (Initialize, Shutdown, HealthCheck, GetManifest, OnEvent, HandleHTTP)
- **Health Monitoring**: Automatic periodic health checks with configurable interval; plugins marked offline after 3 consecutive failures
- **Auto-Restart**: Managed plugins are automatically restarted when they become unresponsive
- **Event Dispatch**: Plugins subscribe to server events (e.g., `license:updated`) and receive them in real-time
- **Route Declaration**: Plugins declare REST routes in their manifest; the server can proxy HTTP requests to the plugin
- **Managed & External Modes**: Server can launch and manage plugin processes, or connect to already-running services
See [PLUGINS.md](PLUGINS.md) for the full plugin development guide.
### Landing Pages & Public Releases
@@ -492,15 +537,147 @@ SSL_MODE = disable
### AI Features Configuration
Enable and configure AI-powered features in `app.ini`:
AI features require the [gitcaddy-ai](https://git.marketally.com/gitcaddy/gitcaddy-ai) sidecar service running alongside your GitCaddy instance. See the gitcaddy-ai README for deployment instructions (Docker Compose recommended).
**1. Enable AI in `app.ini`:**
```ini
[server]
ROOT_URL = https://your-instance.com/
[actions]
[ai]
; Master switch — nothing AI-related runs unless this is true
ENABLED = true
; gitcaddy-ai sidecar address (REST port)
SERVICE_URL = localhost:5050
SERVICE_TOKEN =
; Connection settings
TIMEOUT = 30s
MAX_RETRIES = 3
; Default provider/model (fallback when org/repo don't override)
DEFAULT_PROVIDER = claude ; claude, openai, or gemini
DEFAULT_MODEL = claude-sonnet-4-20250514
; System API keys (used when org/repo don't provide their own)
CLAUDE_API_KEY = sk-ant-...
OPENAI_API_KEY =
GEMINI_API_KEY =
; Rate limiting (per repo)
MAX_OPERATIONS_PER_HOUR = 100
MAX_TOKENS_PER_OPERATION = 8192
; Feature gates — admin controls what's available instance-wide
ENABLE_CODE_REVIEW = true ; Tier 1: PR review
ENABLE_ISSUE_TRIAGE = true ; Tier 1: auto-label issues
ENABLE_DOC_GEN = true ; Tier 1: documentation generation
ENABLE_EXPLAIN_CODE = true ; Tier 1: code explanation
ENABLE_CHAT = true ; Tier 1: chat interface
ALLOW_AUTO_RESPOND = true ; Tier 1: auto-respond to issues
ALLOW_AUTO_REVIEW = true ; Tier 1: auto-review PRs
ALLOW_AGENT_MODE = false ; Tier 2: agent fix (off by default, requires runner setup)
; Content limits
MAX_FILE_SIZE_KB = 500
MAX_DIFF_LINES = 5000
; Bot user (created automatically on first startup)
BOT_USER_NAME = gitcaddy-ai
```
**2. Configure org-level settings (optional):**
Organization admins can override the provider, model, and API key for all repos in the org. This is done via:
- **Web UI:** `/{org}/settings/ai`
- **API:** `PUT /api/v2/orgs/{org}/ai/settings`
Org settings include:
- Provider & model selection
- API key (encrypted at rest)
- Rate limits (max ops/hour)
- Allowed operations list
- Agent mode toggle
**3. Enable AI on a repository:**
Repository admins enable AI and choose which operations to activate. This is done via:
- **Web UI:** `/{owner}/{repo}/settings/ai`
- **API:** `PUT /api/v2/repos/{owner}/{repo}/ai/settings`
Repo settings include:
- **Enable/Disable:** Toggles the AI unit on the repo
- **Tier 1 Operations:** Auto-respond to issues, auto-review PRs, auto-triage issues, auto-inspect workflows
- **Tier 2 Agent Mode:** Enable agent fix, configure trigger labels (e.g., `ai-fix`), set max run time
- **Escalation:** Enable staff escalation on failure, set escalation label and team
- **Provider Override:** Override org/system provider and model for this repo
- **Custom Instructions:** System instructions, review instructions, issue response instructions
**4. Set up Tier 2 agent runners (optional):**
For agent mode (AI that creates branches and PRs), you need an Actions runner with Claude Code installed:
1. Register a runner with the `ai-runner` label
2. Install the Claude Code CLI on the runner
3. Place the agent workflow file in your repo at `.gitea/workflows/ai-agent.yml` (a template is available via the API)
4. Set the `AI_API_KEY` secret on the repo or org
When a trigger label is added to an issue (or an agent fix is requested via API), the server dispatches the workflow, the runner clones the repo, runs Claude Code headless, and creates a PR with the proposed fix.
### Plugin Framework Configuration
The plugin framework allows external services (like the AI sidecar) to register with GitCaddy for lifecycle management, health monitoring, and event dispatch. Plugins communicate over gRPC (HTTP/2) using the protocol defined in `modules/plugins/pluginv1/plugin.proto`.
**1. Enable the plugin framework in `app.ini`:**
```ini
[plugins]
; Master switch for the plugin framework
ENABLED = true
; Directory for plugin data
PATH = data/plugins
; How often to health-check external plugins
HEALTH_CHECK_INTERVAL = 30s
```
**2. Register external plugins:**
Each plugin gets its own `[plugins.<name>]` section:
```ini
[plugins.gitcaddy-ai]
ENABLED = true
; Address of the plugin's gRPC endpoint (cleartext HTTP/2)
ADDRESS = localhost:5000
; Health check timeout
HEALTH_TIMEOUT = 5s
; Events the plugin subscribes to (comma-separated)
SUBSCRIBED_EVENTS = license:updated
```
**Managed vs External Mode:**
| Mode | Configuration | Use Case |
|------|--------------|----------|
| **External** | `ADDRESS` only | Plugin runs independently (Docker, systemd, sidecar) |
| **Managed** | `BINARY` + `ARGS` | Server launches and manages the plugin process |
Managed mode example:
```ini
[plugins.my-plugin]
ENABLED = true
BINARY = /usr/local/bin/my-plugin
ARGS = --port 5001
ADDRESS = localhost:5001
HEALTH_TIMEOUT = 5s
```
When a managed plugin becomes unresponsive (3 consecutive health check failures), the server automatically restarts its process.
See [PLUGINS.md](PLUGINS.md) for the full plugin development guide.
### V2 API Configuration
```ini
[api]
; Enable V2 API (enabled by default)
ENABLE_V2_API = true
@@ -800,36 +977,96 @@ advanced:
### AI Features Usage
**AI Code Review:**
1. Create a pull request
2. Enable AI review in PR settings
3. AI will analyze changes and provide suggestions
4. Review and apply suggestions as needed
#### Automatic Operations (Event-Driven)
**Issue Triage:**
- AI automatically categorizes new issues
- Suggests labels based on content
- Estimates complexity
- Recommends relevant files for investigation
Once AI is enabled on a repo with the desired operations toggled on, these fire automatically:
**Code Explanation:**
1. Select code in the file viewer
2. Click "Explain with AI"
3. View generated documentation
| Event | Operations Triggered | Tier |
|-------|---------------------|------|
| New issue created | Auto-respond + Auto-triage | 1 |
| Comment mentions `@gitcaddy-ai` | Auto-respond | 1 |
| Pull request opened | Auto-review | 1 |
| PR branch updated (push) | Auto-review (re-review) | 1 |
| Trigger label added to issue | Agent fix | 2 |
All operations are queued asynchronously and processed by the AI service. The bot user posts comments/reviews on the issue or PR with the results.
#### Manual API Triggers
Trigger AI operations on demand via the v2 API:
**Using AI Context APIs:**
```bash
# Get repository summary
curl https://your-instance.com/api/v2/ai/repo/summary?owner=owner&repo=repo \
# Trigger AI code review on a pull request
curl -X POST https://your-instance.com/api/v2/repos/owner/repo/ai/review/1 \
-H "Authorization: token YOUR_TOKEN"
# Get issue context
curl https://your-instance.com/api/v2/ai/issue/context?owner=owner&repo=repo&issue=123 \
# Trigger AI response to an issue
curl -X POST https://your-instance.com/api/v2/repos/owner/repo/ai/respond/42 \
-H "Authorization: token YOUR_TOKEN"
# Get repository navigation
curl https://your-instance.com/api/v2/ai/repo/navigation?owner=owner&repo=repo&depth=3 \
# Trigger AI triage on an issue
curl -X POST https://your-instance.com/api/v2/repos/owner/repo/ai/triage/42 \
-H "Authorization: token YOUR_TOKEN"
# Explain code in a file
curl -X POST https://your-instance.com/api/v2/repos/owner/repo/ai/explain \
-H "Authorization: token YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"file_path": "main.go", "start_line": 10, "end_line": 50, "question": "What does this function do?"}'
# Trigger Tier 2 agent fix (creates a PR)
curl -X POST https://your-instance.com/api/v2/repos/owner/repo/ai/fix/42 \
-H "Authorization: token YOUR_TOKEN"
```
All trigger endpoints return `202 Accepted` with the queued operation details.
#### Viewing AI Operation History
Every AI operation is logged with full audit details:
```bash
# List operations for a repo (paginated, filterable)
curl "https://your-instance.com/api/v2/repos/owner/repo/ai/operations?status=success&tier=1&page=1" \
-H "Authorization: token YOUR_TOKEN"
# Get a single operation's details
curl https://your-instance.com/api/v2/repos/owner/repo/ai/operations/123 \
-H "Authorization: token YOUR_TOKEN"
# Global operation log (site admin only)
curl https://your-instance.com/api/v2/admin/ai/operations \
-H "Authorization: token YOUR_TOKEN"
```
Each operation log entry includes: operation type, tier, trigger event, provider, model, input/output tokens, duration, status (success/failed/escalated), and the resulting comment or Actions run ID.
#### Escalation
When an AI operation fails or returns low confidence, and the repo has escalation configured:
1. The configured escalation label (e.g., `needs-human-review`) is added to the issue
2. The bot posts a summary comment explaining what it attempted and why it's escalating
3. If an escalation team is configured, a team member is assigned
#### AI Context APIs (for External AI Tools)
These endpoints provide structured context **to** external AI tools (IDE plugins, MCP servers, etc.):
```bash
# Get repository summary (languages, build system, entry points)
curl -X POST https://your-instance.com/api/v2/ai/repo/summary \
-H "Authorization: token YOUR_TOKEN" \
-d '{"owner": "owner", "repo": "repo"}'
# Get issue context (details, comments, complexity hints)
curl -X POST https://your-instance.com/api/v2/ai/issue/context \
-H "Authorization: token YOUR_TOKEN" \
-d '{"owner": "owner", "repo": "repo", "issue": 123}'
# Get repository navigation (directory tree, important paths)
curl -X POST https://your-instance.com/api/v2/ai/repo/navigation \
-H "Authorization: token YOUR_TOKEN" \
-d '{"owner": "owner", "repo": "repo", "depth": 3}'
```
## GitCaddy Runner
@@ -924,6 +1161,29 @@ GET /api/v2/batch/files?paths=file1.go,file2.go&owner=owner&repo=repo
# Stream files (NDJSON)
POST /api/v2/stream/files
# AI: Get/update repo AI settings
GET /api/v2/repos/{owner}/{repo}/ai/settings
PUT /api/v2/repos/{owner}/{repo}/ai/settings
# AI: Trigger operations
POST /api/v2/repos/{owner}/{repo}/ai/review/{pull}
POST /api/v2/repos/{owner}/{repo}/ai/respond/{issue}
POST /api/v2/repos/{owner}/{repo}/ai/triage/{issue}
POST /api/v2/repos/{owner}/{repo}/ai/explain
POST /api/v2/repos/{owner}/{repo}/ai/fix/{issue}
# AI: Operation audit log
GET /api/v2/repos/{owner}/{repo}/ai/operations
GET /api/v2/repos/{owner}/{repo}/ai/operations/{id}
# AI: Org settings
GET /api/v2/orgs/{org}/ai/settings
PUT /api/v2/orgs/{org}/ai/settings
# AI: Admin
GET /api/v2/admin/ai/status
GET /api/v2/admin/ai/operations
```
## Internationalization
@@ -1060,6 +1320,7 @@ GitCaddy is a fork of [Gitea](https://gitea.io), the open-source self-hosted Git
| Project | Description |
|---------|-------------|
| [gitcaddy/act_runner](https://git.marketally.com/gitcaddy/act_runner) | Runner with automatic capability detection |
| [gitcaddy/gitcaddy-ai](https://git.marketally.com/gitcaddy/gitcaddy-ai) | AI sidecar service (.NET 9, gRPC/REST) for code review, triage, and more |
| [gitcaddy/actions-proto-go](https://git.marketally.com/gitcaddy/actions-proto-go) | Protocol buffer definitions for Actions |
**Support:**

View File

@@ -1,206 +0,0 @@
# Gitea
[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea)
[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin")
[English](./README.md) | [繁體中文](./README.zh-tw.md)
## 目的
这个项目的目标是提供最简单、最快速、最无痛的方式来设置自托管的 Git 服务。
由于 Gitea 是用 Go 语言编写的,它可以在 Go 支持的所有平台和架构上运行,包括 Linux、macOS 和 Windows 的 x86、amd64、ARM 和 PowerPC 架构。这个项目自 2016 年 11 月从 [Gogs](https://gogs.io) [分叉](https://blog.gitea.com/welcome-to-gitea/) 而来,但已经有了很多变化。
在线演示可以访问 [demo.gitea.com](https://demo.gitea.com)。
要访问免费的 Gitea 服务(有一定数量的仓库限制),可以访问 [gitea.com](https://gitea.com/user/login)。
要快速部署您自己的专用 Gitea 实例,可以在 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。
## 文件
您可以在我们的官方 [文件网站](https://docs.gitea.com/) 上找到全面的文件。
它包括安装、管理、使用、开发、贡献指南等,帮助您快速入门并有效地探索所有功能。
如果您有任何建议或想要贡献,可以访问 [文件仓库](https://gitea.com/gitea/docs)
## 构建
从源代码树的根目录运行:
TAGS="bindata" make build
如果需要 SQLite 支持:
TAGS="bindata sqlite sqlite_unlock_notify" make build
`build` 目标分为两个子目标:
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定义。
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
需要互联网连接来下载 go 和 npm 模块。从包含预构建前端文件的官方源代码压缩包构建时,不会触发 `frontend` 目标,因此可以在没有 Node.js 的情况下构建。
更多信息https://docs.gitea.com/installation/install-from-source
## 使用
构建后,默认情况下会在源代码树的根目录生成一个名为 `gitea` 的二进制文件。要运行它,请使用:
./gitea web
> [!注意]
> 如果您对使用我们的 API 感兴趣,我们提供了实验性支持,并附有 [文件](https://docs.gitea.com/api)。
## 贡献
预期的工作流程是Fork -> Patch -> Push -> Pull Request
> [!注意]
>
> 1. **在开始进行 Pull Request 之前,您必须阅读 [贡献者指南](CONTRIBUTING.md)。**
> 2. 如果您在项目中发现了漏洞,请私下写信给 **security@gitea.io**。谢谢!
## 翻译
[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com)
翻译通过 [Crowdin](https://translate.gitea.com) 进行。如果您想翻译成新的语言,请在 Crowdin 项目中请求管理员添加新语言。
您也可以创建一个 issue 来添加语言,或者在 discord 的 #translation 频道上询问。如果您需要上下文或发现一些翻译问题,可以在字符串上留言或在 Discord 上询问。对于一般的翻译问题,文档中有一个部分。目前有点空,但我们希望随着问题的出现而填充它。
更多信息请参阅 [文件](https://docs.gitea.com/contributing/localization)。
## 官方和第三方项目
我们提供了一个官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一个名为 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一个 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
我们在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 维护了一个 Gitea 相关项目的列表,您可以在那里发现更多的第三方项目,包括 SDK、插件、主题等。
## 通讯
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
如果您有任何文件未涵盖的问题,可以在我们的 [Discord 服务器](https://discord.gg/Gitea) 上与我们联系,或者在 [discourse 论坛](https://forum.gitea.com/) 上创建帖子。
## 作者
- [维护者](https://github.com/orgs/go-gitea/people)
- [贡献者](https://github.com/go-gitea/gitea/graphs/contributors)
- [翻译者](options/locale/TRANSLATORS)
## 支持者
感谢所有支持者! 🙏 [[成为支持者](https://opencollective.com/gitea#backer)]
<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
## 赞助商
通过成为赞助商来支持这个项目。您的标志将显示在这里,并带有链接到您的网站。 [[成为赞助商](https://opencollective.com/gitea#sponsor)]
<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
## 常见问题
**Gitea 怎么发音?**
Gitea 的发音是 [/ɡɪti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea" 一样g 是硬音。
**为什么这个项目没有托管在 Gitea 实例上?**
我们正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。
**在哪里可以找到安全补丁?**
在 [发布日志](https://github.com/go-gitea/gitea/releases) 或 [变更日志](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索关键词 `SECURITY` 以找到安全补丁。
## 许可证
这个项目是根据 MIT 许可证授权的。
请参阅 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以获取完整的许可证文本。
## 进一步信息
<details>
<summary>寻找界面概述?查看这里!</summary>
### 登录/注册页面
![Login](https://dl.gitea.com/screenshots/login.png)
![Register](https://dl.gitea.com/screenshots/register.png)
### 用户仪表板
![Home](https://dl.gitea.com/screenshots/home.png)
![Issues](https://dl.gitea.com/screenshots/issues.png)
![Pull Requests](https://dl.gitea.com/screenshots/pull_requests.png)
![Milestones](https://dl.gitea.com/screenshots/milestones.png)
### 用户资料
![Profile](https://dl.gitea.com/screenshots/user_profile.png)
### 探索
![Repos](https://dl.gitea.com/screenshots/explore_repos.png)
![Users](https://dl.gitea.com/screenshots/explore_users.png)
![Orgs](https://dl.gitea.com/screenshots/explore_orgs.png)
### 仓库
![Home](https://dl.gitea.com/screenshots/repo_home.png)
![Commits](https://dl.gitea.com/screenshots/repo_commits.png)
![Branches](https://dl.gitea.com/screenshots/repo_branches.png)
![Labels](https://dl.gitea.com/screenshots/repo_labels.png)
![Milestones](https://dl.gitea.com/screenshots/repo_milestones.png)
![Releases](https://dl.gitea.com/screenshots/repo_releases.png)
![Tags](https://dl.gitea.com/screenshots/repo_tags.png)
#### 仓库问题
![List](https://dl.gitea.com/screenshots/repo_issues.png)
![Issue](https://dl.gitea.com/screenshots/repo_issue.png)
#### 仓库拉取请求
![List](https://dl.gitea.com/screenshots/repo_pull_requests.png)
![Pull Request](https://dl.gitea.com/screenshots/repo_pull_request.png)
![File](https://dl.gitea.com/screenshots/repo_pull_request_file.png)
![Commits](https://dl.gitea.com/screenshots/repo_pull_request_commits.png)
#### 仓库操作
![List](https://dl.gitea.com/screenshots/repo_actions.png)
![Details](https://dl.gitea.com/screenshots/repo_actions_run.png)
#### 仓库活动
![Activity](https://dl.gitea.com/screenshots/repo_activity.png)
![Contributors](https://dl.gitea.com/screenshots/repo_contributors.png)
![Code Frequency](https://dl.gitea.com/screenshots/repo_code_frequency.png)
![Recent Commits](https://dl.gitea.com/screenshots/repo_recent_commits.png)
### 组织
![Home](https://dl.gitea.com/screenshots/org_home.png)
</details>

View File

@@ -1,206 +0,0 @@
# Gitea
[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea)
[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin")
[English](./README.md) | [简体中文](./README.zh-cn.md)
## 目的
這個項目的目標是提供最簡單、最快速、最無痛的方式來設置自託管的 Git 服務。
由於 Gitea 是用 Go 語言編寫的,它可以在 Go 支援的所有平台和架構上運行,包括 Linux、macOS 和 Windows 的 x86、amd64、ARM 和 PowerPC 架構。這個項目自 2016 年 11 月從 [Gogs](https://gogs.io) [分叉](https://blog.gitea.com/welcome-to-gitea/) 而來,但已經有了很多變化。
在線演示可以訪問 [demo.gitea.com](https://demo.gitea.com)。
要訪問免費的 Gitea 服務(有一定數量的倉庫限制),可以訪問 [gitea.com](https://gitea.com/user/login)。
要快速部署您自己的專用 Gitea 實例,可以在 [cloud.gitea.com](https://cloud.gitea.com) 開始免費試用。
## 文件
您可以在我們的官方 [文件網站](https://docs.gitea.com/) 上找到全面的文件。
它包括安裝、管理、使用、開發、貢獻指南等,幫助您快速入門並有效地探索所有功能。
如果您有任何建議或想要貢獻,可以訪問 [文件倉庫](https://gitea.com/gitea/docs)
## 構建
從源代碼樹的根目錄運行:
TAGS="bindata" make build
如果需要 SQLite 支援:
TAGS="bindata sqlite sqlite_unlock_notify" make build
`build` 目標分為兩個子目標:
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定義。
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
需要互聯網連接來下載 go 和 npm 模塊。從包含預構建前端文件的官方源代碼壓縮包構建時,不會觸發 `frontend` 目標,因此可以在沒有 Node.js 的情況下構建。
更多信息https://docs.gitea.com/installation/install-from-source
## 使用
構建後,默認情況下會在源代碼樹的根目錄生成一個名為 `gitea` 的二進制文件。要運行它,請使用:
./gitea web
> [!注意]
> 如果您對使用我們的 API 感興趣,我們提供了實驗性支援,並附有 [文件](https://docs.gitea.com/api)。
## 貢獻
預期的工作流程是Fork -> Patch -> Push -> Pull Request
> [!注意]
>
> 1. **在開始進行 Pull Request 之前,您必須閱讀 [貢獻者指南](CONTRIBUTING.md)。**
> 2. 如果您在項目中發現了漏洞,請私下寫信給 **security@gitea.io**。謝謝!
## 翻譯
[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com)
翻譯通過 [Crowdin](https://translate.gitea.com) 進行。如果您想翻譯成新的語言,請在 Crowdin 項目中請求管理員添加新語言。
您也可以創建一個 issue 來添加語言,或者在 discord 的 #translation 頻道上詢問。如果您需要上下文或發現一些翻譯問題,可以在字符串上留言或在 Discord 上詢問。對於一般的翻譯問題,文檔中有一個部分。目前有點空,但我們希望隨著問題的出現而填充它。
更多信息請參閱 [文件](https://docs.gitea.com/contributing/localization)。
## 官方和第三方項目
我們提供了一個官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一個名為 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一個 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
我們在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 維護了一個 Gitea 相關項目的列表,您可以在那裡發現更多的第三方項目,包括 SDK、插件、主題等。
## 通訊
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
如果您有任何文件未涵蓋的問題,可以在我們的 [Discord 服務器](https://discord.gg/Gitea) 上與我們聯繫,或者在 [discourse 論壇](https://forum.gitea.com/) 上創建帖子。
## 作者
- [維護者](https://github.com/orgs/go-gitea/people)
- [貢獻者](https://github.com/go-gitea/gitea/graphs/contributors)
- [翻譯者](options/locale/TRANSLATORS)
## 支持者
感謝所有支持者! 🙏 [[成為支持者](https://opencollective.com/gitea#backer)]
<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
## 贊助商
通過成為贊助商來支持這個項目。您的標誌將顯示在這裡,並帶有鏈接到您的網站。 [[成為贊助商](https://opencollective.com/gitea#sponsor)]
<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
## 常見問題
**Gitea 怎麼發音?**
Gitea 的發音是 [/ɡɪti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea" 一樣g 是硬音。
**為什麼這個項目沒有託管在 Gitea 實例上?**
我們正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。
**在哪裡可以找到安全補丁?**
在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 或 [變更日誌](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索關鍵詞 `SECURITY` 以找到安全補丁。
## 許可證
這個項目是根據 MIT 許可證授權的。
請參閱 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以獲取完整的許可證文本。
## 進一步信息
<details>
<summary>尋找界面概述?查看這裡!</summary>
### 登錄/註冊頁面
![Login](https://dl.gitea.com/screenshots/login.png)
![Register](https://dl.gitea.com/screenshots/register.png)
### 用戶儀表板
![Home](https://dl.gitea.com/screenshots/home.png)
![Issues](https://dl.gitea.com/screenshots/issues.png)
![Pull Requests](https://dl.gitea.com/screenshots/pull_requests.png)
![Milestones](https://dl.gitea.com/screenshots/milestones.png)
### 用戶資料
![Profile](https://dl.gitea.com/screenshots/user_profile.png)
### 探索
![Repos](https://dl.gitea.com/screenshots/explore_repos.png)
![Users](https://dl.gitea.com/screenshots/explore_users.png)
![Orgs](https://dl.gitea.com/screenshots/explore_orgs.png)
### 倉庫
![Home](https://dl.gitea.com/screenshots/repo_home.png)
![Commits](https://dl.gitea.com/screenshots/repo_commits.png)
![Branches](https://dl.gitea.com/screenshots/repo_branches.png)
![Labels](https://dl.gitea.com/screenshots/repo_labels.png)
![Milestones](https://dl.gitea.com/screenshots/repo_milestones.png)
![Releases](https://dl.gitea.com/screenshots/repo_releases.png)
![Tags](https://dl.gitea.com/screenshots/repo_tags.png)
#### 倉庫問題
![List](https://dl.gitea.com/screenshots/repo_issues.png)
![Issue](https://dl.gitea.com/screenshots/repo_issue.png)
#### 倉庫拉取請求
![List](https://dl.gitea.com/screenshots/repo_pull_requests.png)
![Pull Request](https://dl.gitea.com/screenshots/repo_pull_request.png)
![File](https://dl.gitea.com/screenshots/repo_pull_request_file.png)
![Commits](https://dl.gitea.com/screenshots/repo_pull_request_commits.png)
#### 倉庫操作
![List](https://dl.gitea.com/screenshots/repo_actions.png)
![Details](https://dl.gitea.com/screenshots/repo_actions_run.png)
#### 倉庫活動
![Activity](https://dl.gitea.com/screenshots/repo_activity.png)
![Contributors](https://dl.gitea.com/screenshots/repo_contributors.png)
![Code Frequency](https://dl.gitea.com/screenshots/repo_code_frequency.png)
![Recent Commits](https://dl.gitea.com/screenshots/repo_recent_commits.png)
### 組織
![Home](https://dl.gitea.com/screenshots/org_home.png)
</details>

12
go.mod
View File

@@ -2,7 +2,7 @@ module code.gitcaddy.com/server/v3
go 1.25.0
toolchain go1.25.5
toolchain go1.25.9
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
// But some CAs use negative serial number, just relax the check. related:
@@ -16,7 +16,7 @@ require (
connectrpc.com/connect v1.18.1
// GitCaddy Vault plugin (compiled-in)
git.marketally.com/gitcaddy/gitcaddy-vault v1.0.52
git.marketally.com/gitcaddy/gitcaddy-vault v1.0.61
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed
gitea.com/go-chi/cache v0.2.1
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
@@ -326,6 +326,9 @@ replace github.com/go-ini/ini => github.com/go-ini/ini v1.66.6
// Use GitCaddy fork with capability support
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.8
// Mirror of deleted github.com/hexops/gotextdiff
replace github.com/hexops/gotextdiff => git.marketally.com/mirrors/gotextdiff v1.0.3
// Vault plugin - use local directory for development
replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault
@@ -337,4 +340,9 @@ exclude github.com/goccy/go-json v0.4.11
exclude github.com/satori/go.uuid v1.2.0
// Pin SDK to v0.22.0 — v0.24.x requires go 1.26
exclude code.gitea.io/sdk/gitea v0.24.0
exclude code.gitea.io/sdk/gitea v0.24.1
tool code.gitea.io/gitea-vet

6
go.sum
View File

@@ -31,6 +31,10 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.marketally.com/gitcaddy/actions-proto-go v0.5.8 h1:MBipeHvY6A0jcobvziUtzgatZTrV4fs/HE1rPQxREN4=
git.marketally.com/gitcaddy/actions-proto-go v0.5.8/go.mod h1:RPu21UoRD3zSAujoZR6LJwuVNa2uFRBveadslczCRfQ=
git.marketally.com/gitcaddy/gitcaddy-vault v1.0.60 h1:SHmAZG1PxbC9kG9gWdrhrQ7cHVmP0e6iMloBK100kQA=
git.marketally.com/gitcaddy/gitcaddy-vault v1.0.60/go.mod h1:UAmVrJUXHsGe3iJz2gmow2zsIE/KAXaW4jKytrxT8jQ=
git.marketally.com/mirrors/gotextdiff v1.0.3 h1:Mxf+YurdCHT4y1GNiZCTDWYtVXSxhlLUeG7g7i9Za70=
git.marketally.com/mirrors/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c=
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
@@ -490,8 +494,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=

View File

@@ -125,6 +125,21 @@ func (opts FindRunOptions) ToOrders() string {
return "`action_run`.`id` DESC"
}
// GetLatestRunPerWorkflow returns the most recent run for each workflow in a repo.
// Uses a subquery to find the MAX(id) per workflow_id, then loads those runs.
func GetLatestRunPerWorkflow(ctx context.Context, repoID int64) (RunList, error) {
subQuery := builder.Select("MAX(id)").
From("`action_run`").
Where(builder.Eq{"`action_run`.repo_id": repoID}).
GroupBy("`action_run`.workflow_id")
var runs RunList
err := db.GetEngine(ctx).
Where(builder.In("`action_run`.id", subQuery)).
Find(&runs)
return runs, err
}
type StatusInfo struct {
Status int
DisplayedStatus string

View File

@@ -191,9 +191,26 @@ func (r *ActionRunner) GenerateToken() (err error) {
// CanMatchLabels checks whether the runner's labels can match a job's "runs-on"
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on
//
// Labels are matched by name, ignoring any ":scheme" suffix (e.g., ":host", ":docker").
// This means a runner with label "germany-linux:host" will match runs-on "germany-linux",
// and a job with runs-on "germany-linux:host" will also match.
func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool {
runnerLabelSet := container.SetOf(r.AgentLabels...)
return runnerLabelSet.Contains(jobRunsOn...) // match all labels
// Build a set of runner label names (stripped of :scheme suffix)
runnerNames := make(container.Set[string], len(r.AgentLabels))
for _, label := range r.AgentLabels {
name, _, _ := strings.Cut(label, ":")
runnerNames.Add(name)
}
// Check that every runs-on label (also stripped of :scheme) is in the runner set
for _, requiredLabel := range jobRunsOn {
name, _, _ := strings.Cut(requiredLabel, ":")
if !runnerNames.Contains(name) {
return false
}
}
return true
}
func init() {

207
models/ai/operation_log.go Normal file
View File

@@ -0,0 +1,207 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package ai
import (
"context"
"code.gitcaddy.com/server/v3/models/db"
"code.gitcaddy.com/server/v3/modules/timeutil"
"xorm.io/builder"
)
func init() {
db.RegisterModel(new(OperationLog))
}
// OperationLog records every AI operation for auditing
type OperationLog struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
Operation string `xorm:"VARCHAR(50) NOT NULL"` // "code-review", "issue-response", etc.
Tier int `xorm:"NOT NULL"` // 1 or 2
TriggerEvent string `xorm:"VARCHAR(100) NOT NULL"`
TriggerUserID int64 `xorm:"INDEX"`
TargetID int64 `xorm:"INDEX"` // issue/PR ID
TargetType string `xorm:"VARCHAR(20)"` // "issue", "pull", "commit"
Provider string `xorm:"VARCHAR(20)"`
Model string `xorm:"VARCHAR(100)"`
InputTokens int `xorm:"DEFAULT 0"`
OutputTokens int `xorm:"DEFAULT 0"`
Status string `xorm:"VARCHAR(20) NOT NULL"` // "success", "failed", "escalated", "pending"
ResultCommentID int64 `xorm:"DEFAULT 0"`
ActionRunID int64 `xorm:"DEFAULT 0"` // for Tier 2
ErrorMessage string `xorm:"TEXT"`
DurationMs int64 `xorm:"DEFAULT 0"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
}
// TableName returns the table name for OperationLog
func (OperationLog) TableName() string {
return "ai_operation_log"
}
// OperationStatus constants
const (
OperationStatusPending = "pending"
OperationStatusSuccess = "success"
OperationStatusFailed = "failed"
OperationStatusEscalated = "escalated"
)
// InsertOperationLog creates a new operation log entry
func InsertOperationLog(ctx context.Context, log *OperationLog) error {
return db.Insert(ctx, log)
}
// UpdateOperationLog updates an existing operation log entry
func UpdateOperationLog(ctx context.Context, log *OperationLog) error {
_, err := db.GetEngine(ctx).ID(log.ID).AllCols().Update(log)
return err
}
// GetOperationLog returns a single operation log entry by ID
func GetOperationLog(ctx context.Context, id int64) (*OperationLog, error) {
log := &OperationLog{}
has, err := db.GetEngine(ctx).ID(id).Get(log)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return log, nil
}
// FindOperationLogsOptions represents options for finding operation logs
type FindOperationLogsOptions struct {
db.ListOptions
RepoID int64
Operation string
Status string
Tier int
}
func (opts FindOperationLogsOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
if opts.Operation != "" {
cond = cond.And(builder.Eq{"operation": opts.Operation})
}
if opts.Status != "" {
cond = cond.And(builder.Eq{"status": opts.Status})
}
if opts.Tier > 0 {
cond = cond.And(builder.Eq{"tier": opts.Tier})
}
return cond
}
func (opts FindOperationLogsOptions) ToOrders() string {
return "created_unix DESC"
}
// CountRecentOperations counts operations in the last hour for rate limiting
func CountRecentOperations(ctx context.Context, repoID int64) (int64, error) {
oneHourAgo := timeutil.TimeStampNow() - 3600
return db.GetEngine(ctx).Where("repo_id = ? AND created_unix > ?", repoID, oneHourAgo).Count(new(OperationLog))
}
// GlobalOperationStats holds aggregate AI operation statistics for admin dashboard
type GlobalOperationStats struct {
TotalOperations int64 `json:"total_operations"`
Operations24h int64 `json:"operations_24h"`
SuccessCount int64 `json:"success_count"`
FailedCount int64 `json:"failed_count"`
EscalatedCount int64 `json:"escalated_count"`
PendingCount int64 `json:"pending_count"`
CountByTier map[int]int64 `json:"count_by_tier"`
TopRepos []RepoOpCount `json:"top_repos"`
TotalInputTokens int64 `json:"total_input_tokens"`
TotalOutputTokens int64 `json:"total_output_tokens"`
}
// RepoOpCount holds a repo's operation count for the top-repos list
type RepoOpCount struct {
RepoID int64 `json:"repo_id"`
Count int64 `json:"count"`
}
// GetGlobalOperationStats returns aggregate statistics across all repos for the admin dashboard
func GetGlobalOperationStats(ctx context.Context) (*GlobalOperationStats, error) {
e := db.GetEngine(ctx)
stats := &GlobalOperationStats{
CountByTier: make(map[int]int64),
}
// Total operations
total, err := e.Count(new(OperationLog))
if err != nil {
return nil, err
}
stats.TotalOperations = total
// Operations in last 24 hours
oneDayAgo := timeutil.TimeStampNow() - 86400
stats.Operations24h, err = e.Where("created_unix > ?", oneDayAgo).Count(new(OperationLog))
if err != nil {
return nil, err
}
// Counts by status
stats.SuccessCount, err = e.Where("status = ?", OperationStatusSuccess).Count(new(OperationLog))
if err != nil {
return nil, err
}
stats.FailedCount, err = e.Where("status = ?", OperationStatusFailed).Count(new(OperationLog))
if err != nil {
return nil, err
}
stats.EscalatedCount, err = e.Where("status = ?", OperationStatusEscalated).Count(new(OperationLog))
if err != nil {
return nil, err
}
stats.PendingCount, err = e.Where("status = ?", OperationStatusPending).Count(new(OperationLog))
if err != nil {
return nil, err
}
// Counts by tier
type tierCount struct {
Tier int `xorm:"tier"`
Count int64 `xorm:"count"`
}
var tierCounts []tierCount
if err := e.Table("ai_operation_log").Select("tier, count(*) as count").GroupBy("tier").Find(&tierCounts); err != nil {
return nil, err
}
for _, tc := range tierCounts {
stats.CountByTier[tc.Tier] = tc.Count
}
// Top 5 repos by operation count
var topRepos []RepoOpCount
if err := e.Table("ai_operation_log").Select("repo_id, count(*) as count").
GroupBy("repo_id").OrderBy("count DESC").Limit(5).Find(&topRepos); err != nil {
return nil, err
}
stats.TopRepos = topRepos
// Total tokens
type tokenSum struct {
InputTokens int64 `xorm:"input_tokens"`
OutputTokens int64 `xorm:"output_tokens"`
}
var ts tokenSum
if _, err := e.Table("ai_operation_log").Select("COALESCE(SUM(input_tokens),0) as input_tokens, COALESCE(SUM(output_tokens),0) as output_tokens").Get(&ts); err != nil {
return nil, err
}
stats.TotalInputTokens = ts.InputTokens
stats.TotalOutputTokens = ts.OutputTokens
return stats, nil
}

135
models/ai/settings.go Normal file
View File

@@ -0,0 +1,135 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package ai
import (
"context"
"code.gitcaddy.com/server/v3/models/db"
secret_module "code.gitcaddy.com/server/v3/modules/secret"
"code.gitcaddy.com/server/v3/modules/setting"
"code.gitcaddy.com/server/v3/modules/timeutil"
)
func init() {
db.RegisterModel(new(OrgAISettings))
}
// OrgAISettings stores AI configuration per organization
type OrgAISettings struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL INDEX"`
Provider string `xorm:"NOT NULL DEFAULT ''"`
Model string `xorm:"NOT NULL DEFAULT ''"`
APIKeyEncrypted string `xorm:"TEXT"`
MaxOpsPerHour int `xorm:"NOT NULL DEFAULT 0"`
AllowedOps string `xorm:"TEXT"` // JSON array of allowed operation names
AgentModeAllowed bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX UPDATED"`
}
// TableName returns the table name for OrgAISettings
func (OrgAISettings) TableName() string {
return "org_ai_settings"
}
// SetAPIKey encrypts and stores the API key
func (s *OrgAISettings) SetAPIKey(key string) error {
if key == "" {
s.APIKeyEncrypted = ""
return nil
}
encrypted, err := secret_module.EncryptSecret(setting.SecretKey, key)
if err != nil {
return err
}
s.APIKeyEncrypted = encrypted
return nil
}
// GetAPIKey decrypts and returns the API key
func (s *OrgAISettings) GetAPIKey() (string, error) {
if s.APIKeyEncrypted == "" {
return "", nil
}
return secret_module.DecryptSecret(setting.SecretKey, s.APIKeyEncrypted)
}
// GetOrgAISettings returns the AI settings for an organization
func GetOrgAISettings(ctx context.Context, orgID int64) (*OrgAISettings, error) {
settings := &OrgAISettings{OrgID: orgID}
has, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Get(settings)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return settings, nil
}
// CreateOrUpdateOrgAISettings creates or updates AI settings for an organization
func CreateOrUpdateOrgAISettings(ctx context.Context, settings *OrgAISettings) error {
existing := &OrgAISettings{}
has, err := db.GetEngine(ctx).Where("org_id = ?", settings.OrgID).Get(existing)
if err != nil {
return err
}
if has {
settings.ID = existing.ID
_, err = db.GetEngine(ctx).ID(existing.ID).AllCols().Update(settings)
return err
}
return db.Insert(ctx, settings)
}
// ResolveProvider resolves the AI provider using the cascade: repo → org → system
func ResolveProvider(ctx context.Context, orgID int64, repoProvider string) string {
if repoProvider != "" {
return repoProvider
}
if orgID > 0 {
if orgSettings, err := GetOrgAISettings(ctx, orgID); err == nil && orgSettings != nil && orgSettings.Provider != "" {
return orgSettings.Provider
}
}
return setting.AI.DefaultProvider
}
// ResolveModel resolves the AI model using the cascade: repo → org → system
func ResolveModel(ctx context.Context, orgID int64, repoModel string) string {
if repoModel != "" {
return repoModel
}
if orgID > 0 {
if orgSettings, err := GetOrgAISettings(ctx, orgID); err == nil && orgSettings != nil && orgSettings.Model != "" {
return orgSettings.Model
}
}
return setting.AI.DefaultModel
}
// ResolveAPIKey resolves the API key using the cascade: org → system
func ResolveAPIKey(ctx context.Context, orgID int64, provider string) string {
// Try org-level key first
if orgID > 0 {
if orgSettings, err := GetOrgAISettings(ctx, orgID); err == nil && orgSettings != nil {
if key, err := orgSettings.GetAPIKey(); err == nil && key != "" {
return key
}
}
}
// Fall back to system-level key
switch provider {
case "claude":
return setting.AI.ClaudeAPIKey
case "openai":
return setting.AI.OpenAIAPIKey
case "gemini":
return setting.AI.GeminiAPIKey
default:
return ""
}
}

View File

@@ -56,6 +56,7 @@ type BlogPost struct { //revive:disable-line:exported
Status BlogPostStatus `xorm:"SMALLINT NOT NULL DEFAULT 0"`
AllowComments bool `xorm:"NOT NULL DEFAULT true"`
SubscriptionOnly bool `xorm:"NOT NULL DEFAULT false"`
ViewCount int64 `xorm:"NOT NULL DEFAULT 0"`
PublishedUnix timeutil.TimeStamp `xorm:"INDEX"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
@@ -297,9 +298,10 @@ type TagCount struct {
Count int
}
// GetExploreTopTags returns the top N tags across all accessible published blog posts.
// GetExploreTopTags returns the top N tags across recent accessible published blog posts.
// Limits scan to most recent 500 posts for performance.
func GetExploreTopTags(ctx context.Context, actor *user_model.User, limit int) ([]*TagCount, error) {
// Fetch all tags from accessible posts
// Fetch tags from recent accessible posts (limit scan for performance)
type tagRow struct {
Tags string `xorm:"tags"`
}
@@ -307,6 +309,8 @@ func GetExploreTopTags(ctx context.Context, actor *user_model.User, limit int) (
err := exploreBlogBaseSess(ctx, actor).
Cols("blog_post.tags").
Where("blog_post.tags != ''").
OrderBy("blog_post.published_unix DESC").
Limit(500).
Find(&rows)
if err != nil {
return nil, err
@@ -410,6 +414,20 @@ func CountPublishedBlogsByRepoID(ctx context.Context, repoID int64) (int64, erro
return db.GetEngine(ctx).Where("repo_id = ? AND status >= ?", repoID, BlogPostPublic).Count(new(BlogPost))
}
// HasSubscriptionOnlyBlogPosts returns true if the repo has any published subscription-only blog posts.
func HasSubscriptionOnlyBlogPosts(ctx context.Context, repoID int64) (bool, error) {
count, err := db.GetEngine(ctx).Where("repo_id = ? AND status >= ? AND subscription_only = ?", repoID, BlogPostPublic, true).Count(new(BlogPost))
return count > 0, err
}
// IsPublishedBlogFeaturedImage checks if the given attachment ID is used as a
// featured image by any published blog post.
func IsPublishedBlogFeaturedImage(ctx context.Context, attachmentID int64) (bool, error) {
return db.GetEngine(ctx).
Where("featured_image_id = ? AND status >= ?", attachmentID, BlogPostPublic).
Exist(new(BlogPost))
}
// CreateBlogPost inserts a new blog post.
func CreateBlogPost(ctx context.Context, p *BlogPost) error {
_, err := db.GetEngine(ctx).Insert(p)
@@ -428,6 +446,12 @@ func DeleteBlogPost(ctx context.Context, id int64) error {
return err
}
// IncrementBlogPostViewCount atomically increments the view count for a blog post.
func IncrementBlogPostViewCount(ctx context.Context, id int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE blog_post SET view_count = view_count + 1 WHERE id = ?", id)
return err
}
// CountPublishedBlogPostsByAuthorID returns the number of published/public blog posts by a user.
func CountPublishedBlogPostsByAuthorID(ctx context.Context, authorID int64) (int64, error) {
return db.GetEngine(ctx).Where("author_id = ? AND status >= ?", authorID, BlogPostPublic).Count(new(BlogPost))

View File

@@ -438,6 +438,13 @@ func prepareMigrationTasks() []*migration {
newMigration(361, "Create wishlist_comment table", v1_26.CreateWishlistCommentTable),
newMigration(362, "Create wishlist_comment_reaction table", v1_26.CreateWishlistCommentReactionTable),
newMigration(363, "Add keep_packages_private to user", v1_26.AddKeepPackagesPrivateToUser),
newMigration(364, "Add view_count to blog_post", v1_26.AddViewCountToBlogPost),
newMigration(365, "Add public_app_integration to repository", v1_26.AddPublicAppIntegrationToRepository),
newMigration(366, "Add page experiment tables for A/B testing", v1_26.AddPageExperimentTables),
newMigration(367, "Add pages translation table for multi-language support", v1_26.AddPagesTranslationTable),
newMigration(368, "Add owner_display_name to repository", v1_26.AddOwnerDisplayNameToRepository),
newMigration(369, "Add public_release_downloads to repository", v1_26.AddPublicReleaseDownloadsToRepository),
newMigration(370, "Add exclude_hidden_files to push_mirror", v1_26.AddExcludeHiddenFilesToPushMirror),
}
return preparedMigrations
}

View File

@@ -0,0 +1,13 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import "xorm.io/xorm"
func AddViewCountToBlogPost(x *xorm.Engine) error {
type BlogPost struct {
ViewCount int64 `xorm:"NOT NULL DEFAULT 0"`
}
return x.Sync(new(BlogPost))
}

View File

@@ -0,0 +1,13 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import "xorm.io/xorm"
func AddPublicAppIntegrationToRepository(x *xorm.Engine) error {
type Repository struct {
PublicAppIntegration bool `xorm:"NOT NULL DEFAULT true"`
}
return x.Sync(new(Repository))
}

View File

@@ -0,0 +1,54 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"code.gitcaddy.com/server/v3/modules/timeutil"
"xorm.io/xorm"
)
func AddPageExperimentTables(x *xorm.Engine) error {
type PageExperiment struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
Name string `xorm:"VARCHAR(255) NOT NULL"`
Status string `xorm:"VARCHAR(32) NOT NULL DEFAULT 'draft'"`
CreatedByAI bool `xorm:"NOT NULL DEFAULT true"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
EndsUnix timeutil.TimeStamp `xorm:"DEFAULT 0"`
}
type PageVariant struct {
ID int64 `xorm:"pk autoincr"`
ExperimentID int64 `xorm:"INDEX NOT NULL"`
Name string `xorm:"VARCHAR(255) NOT NULL"`
IsControl bool `xorm:"NOT NULL DEFAULT false"`
Weight int `xorm:"NOT NULL DEFAULT 50"`
ConfigOverride string `xorm:"TEXT"`
Impressions int64 `xorm:"NOT NULL DEFAULT 0"`
Conversions int64 `xorm:"NOT NULL DEFAULT 0"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
}
type PageEvent struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
VariantID int64 `xorm:"INDEX DEFAULT 0"`
ExperimentID int64 `xorm:"INDEX DEFAULT 0"`
VisitorID string `xorm:"VARCHAR(64) INDEX"`
EventType string `xorm:"VARCHAR(32) NOT NULL"`
EventData string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
}
if err := x.Sync(new(PageExperiment)); err != nil {
return err
}
if err := x.Sync(new(PageVariant)); err != nil {
return err
}
return x.Sync(new(PageEvent))
}

View File

@@ -0,0 +1,24 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"code.gitcaddy.com/server/v3/modules/timeutil"
"xorm.io/xorm"
)
func AddPagesTranslationTable(x *xorm.Engine) error {
type Translation struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
Lang string `xorm:"VARCHAR(10) NOT NULL"`
ConfigJSON string `xorm:"TEXT"`
AutoGenerated bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(Translation))
}

View File

@@ -0,0 +1,16 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"xorm.io/xorm"
)
func AddOwnerDisplayNameToRepository(x *xorm.Engine) error {
type Repository struct {
OwnerDisplayName string `xorm:"VARCHAR(255)"`
}
return x.Sync(new(Repository))
}

View File

@@ -0,0 +1,16 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"xorm.io/xorm"
)
func AddPublicReleaseDownloadsToRepository(x *xorm.Engine) error {
type Repository struct {
PublicReleaseDownloads bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync(new(Repository))
}

View File

@@ -0,0 +1,16 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"xorm.io/xorm"
)
func AddExcludeHiddenFilesToPushMirror(x *xorm.Engine) error {
type PushMirror struct {
ExcludeHiddenFiles bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync(new(PushMirror))
}

272
models/pages/experiment.go Normal file
View File

@@ -0,0 +1,272 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package pages
import (
"context"
"code.gitcaddy.com/server/v3/models/db"
"code.gitcaddy.com/server/v3/modules/timeutil"
)
// ExperimentStatus represents the status of an A/B test experiment.
type ExperimentStatus string
const (
ExperimentStatusDraft ExperimentStatus = "draft"
ExperimentStatusActive ExperimentStatus = "active"
ExperimentStatusPaused ExperimentStatus = "paused"
ExperimentStatusCompleted ExperimentStatus = "completed"
ExperimentStatusApproved ExperimentStatus = "approved"
)
func init() {
db.RegisterModel(new(PageExperiment))
db.RegisterModel(new(PageVariant))
db.RegisterModel(new(PageEvent))
}
// PageExperiment tracks an A/B test on a landing page.
type PageExperiment struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
Name string `xorm:"VARCHAR(255) NOT NULL"`
Status ExperimentStatus `xorm:"VARCHAR(32) NOT NULL DEFAULT 'draft'"`
CreatedByAI bool `xorm:"NOT NULL DEFAULT true"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
EndsUnix timeutil.TimeStamp `xorm:"DEFAULT 0"`
Variants []*PageVariant `xorm:"-"`
}
// TableName returns the table name for PageExperiment.
func (e *PageExperiment) TableName() string {
return "page_experiment"
}
// PageVariant is one arm of an A/B test experiment.
type PageVariant struct {
ID int64 `xorm:"pk autoincr"`
ExperimentID int64 `xorm:"INDEX NOT NULL"`
Name string `xorm:"VARCHAR(255) NOT NULL"`
IsControl bool `xorm:"NOT NULL DEFAULT false"`
Weight int `xorm:"NOT NULL DEFAULT 50"`
ConfigOverride string `xorm:"TEXT"`
Impressions int64 `xorm:"NOT NULL DEFAULT 0"`
Conversions int64 `xorm:"NOT NULL DEFAULT 0"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
}
// TableName returns the table name for PageVariant.
func (v *PageVariant) TableName() string {
return "page_variant"
}
// ConversionRate returns the conversion rate for this variant.
func (v *PageVariant) ConversionRate() float64 {
if v.Impressions == 0 {
return 0
}
return float64(v.Conversions) / float64(v.Impressions)
}
// PageEvent tracks visitor interactions with a landing page.
type PageEvent struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
VariantID int64 `xorm:"INDEX DEFAULT 0"`
ExperimentID int64 `xorm:"INDEX DEFAULT 0"`
VisitorID string `xorm:"VARCHAR(64) INDEX"`
EventType string `xorm:"VARCHAR(32) NOT NULL"`
EventData string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
}
// TableName returns the table name for PageEvent.
func (e *PageEvent) TableName() string {
return "page_event"
}
// Valid event types
const (
EventTypeImpression = "impression"
EventTypeCTAClick = "cta_click"
EventTypeScrollDepth = "scroll_depth"
EventTypeClick = "click"
EventTypeStar = "star"
EventTypeFork = "fork"
EventTypeClone = "clone"
)
// CreateExperiment creates a new experiment.
func CreateExperiment(ctx context.Context, exp *PageExperiment) error {
_, err := db.GetEngine(ctx).Insert(exp)
return err
}
// GetExperimentByID returns an experiment by ID.
func GetExperimentByID(ctx context.Context, id int64) (*PageExperiment, error) {
exp := new(PageExperiment)
has, err := db.GetEngine(ctx).ID(id).Get(exp)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return exp, nil
}
// GetExperimentsByRepoID returns all experiments for a repository.
func GetExperimentsByRepoID(ctx context.Context, repoID int64) ([]*PageExperiment, error) {
experiments := make([]*PageExperiment, 0, 10)
return experiments, db.GetEngine(ctx).Where("repo_id = ?", repoID).
Desc("created_unix").Find(&experiments)
}
// GetActiveExperimentByRepoID returns the currently active experiment for a repo, if any.
func GetActiveExperimentByRepoID(ctx context.Context, repoID int64) (*PageExperiment, error) {
exp := new(PageExperiment)
has, err := db.GetEngine(ctx).
Where("repo_id = ? AND status = ?", repoID, ExperimentStatusActive).
Get(exp)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return exp, nil
}
// GetAllActiveExperiments returns all active experiments across all repos.
func GetAllActiveExperiments(ctx context.Context) ([]*PageExperiment, error) {
experiments := make([]*PageExperiment, 0, 50)
return experiments, db.GetEngine(ctx).
Where("status = ?", ExperimentStatusActive).
Find(&experiments)
}
// UpdateExperiment updates an experiment.
func UpdateExperiment(ctx context.Context, exp *PageExperiment) error {
_, err := db.GetEngine(ctx).ID(exp.ID).AllCols().Update(exp)
return err
}
// UpdateExperimentStatus updates just the status of an experiment.
func UpdateExperimentStatus(ctx context.Context, id int64, status ExperimentStatus) error {
_, err := db.GetEngine(ctx).ID(id).Cols("status").
Update(&PageExperiment{Status: status})
return err
}
// CreateVariant creates a new variant for an experiment.
func CreateVariant(ctx context.Context, variant *PageVariant) error {
_, err := db.GetEngine(ctx).Insert(variant)
return err
}
// GetVariantByID returns a variant by ID.
func GetVariantByID(ctx context.Context, id int64) (*PageVariant, error) {
variant := new(PageVariant)
has, err := db.GetEngine(ctx).ID(id).Get(variant)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return variant, nil
}
// GetVariantsByExperimentID returns all variants for an experiment.
func GetVariantsByExperimentID(ctx context.Context, experimentID int64) ([]*PageVariant, error) {
variants := make([]*PageVariant, 0, 5)
return variants, db.GetEngine(ctx).
Where("experiment_id = ?", experimentID).
Find(&variants)
}
// IncrementVariantImpressions increments the impression counter for a variant.
func IncrementVariantImpressions(ctx context.Context, variantID int64) error {
_, err := db.GetEngine(ctx).Exec(
"UPDATE `page_variant` SET impressions = impressions + 1 WHERE id = ?", variantID)
return err
}
// IncrementVariantConversions increments the conversion counter for a variant.
func IncrementVariantConversions(ctx context.Context, variantID int64) error {
_, err := db.GetEngine(ctx).Exec(
"UPDATE `page_variant` SET conversions = conversions + 1 WHERE id = ?", variantID)
return err
}
// CreatePageEvent records a visitor event.
func CreatePageEvent(ctx context.Context, event *PageEvent) error {
_, err := db.GetEngine(ctx).Insert(event)
return err
}
// GetEventCountByVariant returns event counts grouped by event type for a variant.
func GetEventCountByVariant(ctx context.Context, variantID int64) (map[string]int64, error) {
type countResult struct {
EventType string `xorm:"event_type"`
Count int64 `xorm:"cnt"`
}
results := make([]countResult, 0)
err := db.GetEngine(ctx).Table("page_event").
Select("event_type, COUNT(*) as cnt").
Where("variant_id = ?", variantID).
GroupBy("event_type").
Find(&results)
if err != nil {
return nil, err
}
counts := make(map[string]int64, len(results))
for _, r := range results {
counts[r.EventType] = r.Count
}
return counts, nil
}
// GetEventCountsByExperiment returns event counts for all variants in an experiment.
func GetEventCountsByExperiment(ctx context.Context, experimentID int64) (map[int64]map[string]int64, error) {
type countResult struct {
VariantID int64 `xorm:"variant_id"`
EventType string `xorm:"event_type"`
Count int64 `xorm:"cnt"`
}
results := make([]countResult, 0)
err := db.GetEngine(ctx).Table("page_event").
Select("variant_id, event_type, COUNT(*) as cnt").
Where("experiment_id = ?", experimentID).
GroupBy("variant_id, event_type").
Find(&results)
if err != nil {
return nil, err
}
counts := make(map[int64]map[string]int64)
for _, r := range results {
if counts[r.VariantID] == nil {
counts[r.VariantID] = make(map[string]int64)
}
counts[r.VariantID][r.EventType] = r.Count
}
return counts, nil
}
// RecordRepoAction records a repo action (star, fork, clone) as a page event
// if the repo has an active experiment.
func RecordRepoAction(ctx context.Context, repoID int64, eventType string) {
exp, err := GetActiveExperimentByRepoID(ctx, repoID)
if err != nil || exp == nil {
return
}
_ = CreatePageEvent(ctx, &PageEvent{
RepoID: repoID,
ExperimentID: exp.ID,
EventType: eventType,
})
}

View File

@@ -0,0 +1,70 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package pages
import (
"context"
"code.gitcaddy.com/server/v3/models/db"
"code.gitcaddy.com/server/v3/modules/timeutil"
)
func init() {
db.RegisterModel(new(Translation))
}
// Translation stores a language-specific translation overlay for a landing page.
type Translation struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
Lang string `xorm:"VARCHAR(10) NOT NULL"`
ConfigJSON string `xorm:"TEXT"`
AutoGenerated bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
// TableName returns the table name for Translation.
func (t *Translation) TableName() string {
return "pages_translation"
}
// GetTranslationsByRepoID returns all translations for a repository.
func GetTranslationsByRepoID(ctx context.Context, repoID int64) ([]*Translation, error) {
translations := make([]*Translation, 0, 10)
return translations, db.GetEngine(ctx).Where("repo_id = ?", repoID).
Asc("lang").Find(&translations)
}
// GetTranslation returns a specific language translation for a repository.
func GetTranslation(ctx context.Context, repoID int64, lang string) (*Translation, error) {
t := new(Translation)
has, err := db.GetEngine(ctx).Where("repo_id = ? AND lang = ?", repoID, lang).Get(t)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return t, nil
}
// CreateTranslation creates a new translation.
func CreateTranslation(ctx context.Context, t *Translation) error {
_, err := db.GetEngine(ctx).Insert(t)
return err
}
// UpdateTranslation updates an existing translation.
func UpdateTranslation(ctx context.Context, t *Translation) error {
_, err := db.GetEngine(ctx).ID(t.ID).Cols("config_json", "auto_generated").Update(t)
return err
}
// DeleteTranslation deletes a translation by repo ID and language.
func DeleteTranslation(ctx context.Context, repoID int64, lang string) error {
_, err := db.GetEngine(ctx).Where("repo_id = ? AND lang = ?", repoID, lang).
Delete(new(Translation))
return err
}

View File

@@ -24,11 +24,12 @@ type PushMirror struct {
RemoteName string
RemoteAddress string `xorm:"VARCHAR(2048)"`
SyncOnCommit bool `xorm:"NOT NULL DEFAULT true"`
Interval time.Duration
CreatedUnix timeutil.TimeStamp `xorm:"created"`
LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"`
LastError string `xorm:"text"`
SyncOnCommit bool `xorm:"NOT NULL DEFAULT true"`
ExcludeHiddenFiles bool `xorm:"NOT NULL DEFAULT false"`
Interval time.Duration
CreatedUnix timeutil.TimeStamp `xorm:"created"`
LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"`
LastError string `xorm:"text"`
}
type PushMirrorOptions struct {
@@ -86,6 +87,11 @@ func UpdatePushMirrorInterval(ctx context.Context, m *PushMirror) error {
return err
}
// UpdatePushMirrorCols updates the specified columns of the push mirror.
func UpdatePushMirrorCols(ctx context.Context, m *PushMirror, cols ...string) (int64, error) {
return db.GetEngine(ctx).ID(m.ID).Cols(cols...).Update(m)
}
func DeletePushMirrors(ctx context.Context, opts PushMirrorOptions) error {
if opts.RepoID > 0 {
_, err := db.Delete[PushMirror](ctx, opts)

View File

@@ -161,6 +161,7 @@ type Repository struct {
Description string `xorm:"TEXT"`
DisplayTitle string `xorm:"VARCHAR(255)"`
GroupHeader string `xorm:"VARCHAR(255)"`
OwnerDisplayName string `xorm:"VARCHAR(255)"`
LicenseType string `xorm:"VARCHAR(50)"`
Website string `xorm:"VARCHAR(2048)"`
OriginalServiceType api.GitServiceType `xorm:"index"`
@@ -221,6 +222,8 @@ type Repository struct {
SubscriptionsEnabled bool `xorm:"NOT NULL DEFAULT false"`
BlogEnabled bool `xorm:"NOT NULL DEFAULT false"`
WishlistEnabled bool `xorm:"NOT NULL DEFAULT false"`
PublicAppIntegration bool `xorm:"NOT NULL DEFAULT true"`
PublicReleaseDownloads bool `xorm:"NOT NULL DEFAULT false"`
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`
TrustModel TrustModelType
@@ -467,6 +470,11 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit
Type: tp,
Config: new(ActionsConfig),
}
case unit.TypeAI:
return &RepoUnit{
Type: tp,
Config: new(AIConfig),
}
case unit.TypeProjects:
cfg := new(ProjectsConfig)
cfg.ProjectsMode = ProjectsModeNone

View File

@@ -219,6 +219,62 @@ func (cfg *ActionsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
// AIConfig describes AI integration config
type AIConfig struct {
// Tier 1: Light AI operations
AutoRespondToIssues bool `json:"auto_respond_issues"`
AutoReviewPRs bool `json:"auto_review_prs"`
AutoInspectWorkflows bool `json:"auto_inspect_workflows"`
AutoTriageIssues bool `json:"auto_triage_issues"`
// Tier 2: Advanced agent operations
AgentModeEnabled bool `json:"agent_mode_enabled"`
AgentTriggerLabels []string `json:"agent_trigger_labels"`
AgentMaxRunMinutes int `json:"agent_max_run_minutes"`
// Escalation
EscalateToStaff bool `json:"escalate_to_staff"`
EscalationLabel string `json:"escalation_label"`
EscalationAssignTeam string `json:"escalation_assign_team"`
// Provider overrides (empty = inherit from org → system)
PreferredProvider string `json:"preferred_provider"`
PreferredModel string `json:"preferred_model"`
// Custom instructions
SystemInstructions string `json:"system_instructions"`
ReviewInstructions string `json:"review_instructions"`
IssueInstructions string `json:"issue_instructions"`
}
// FromDB fills up an AIConfig from serialized format.
func (cfg *AIConfig) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}
// ToDB exports an AIConfig to a serialized format.
func (cfg *AIConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
// IsOperationEnabled returns whether a given AI operation is enabled
func (cfg *AIConfig) IsOperationEnabled(op string) bool {
switch op {
case "issue-response":
return cfg.AutoRespondToIssues
case "issue-triage":
return cfg.AutoTriageIssues
case "code-review":
return cfg.AutoReviewPRs
case "workflow-inspect":
return cfg.AutoInspectWorkflows
case "agent-fix":
return cfg.AgentModeEnabled
default:
return false
}
}
// ProjectsMode represents the projects enabled for a repository
type ProjectsMode string
@@ -281,6 +337,8 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
r.Config = new(IssuesConfig)
case unit.TypeActions:
r.Config = new(ActionsConfig)
case unit.TypeAI:
r.Config = new(AIConfig)
case unit.TypeProjects:
r.Config = new(ProjectsConfig)
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypePackages:
@@ -336,6 +394,11 @@ func (r *RepoUnit) ProjectsConfig() *ProjectsConfig {
return r.Config.(*ProjectsConfig)
}
// AIConfig returns config for unit.TypeAI
func (r *RepoUnit) AIConfig() *AIConfig {
return r.Config.(*AIConfig)
}
func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
var tmpUnits []*RepoUnit
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {

View File

@@ -7,6 +7,7 @@ import (
"context"
"code.gitcaddy.com/server/v3/models/db"
pages_model "code.gitcaddy.com/server/v3/models/pages"
user_model "code.gitcaddy.com/server/v3/models/user"
"code.gitcaddy.com/server/v3/modules/timeutil"
)
@@ -25,7 +26,7 @@ func init() {
// StarRepo or unstar repository.
func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star bool) error {
return db.WithTx(ctx, func(ctx context.Context) error {
err := db.WithTx(ctx, func(ctx context.Context) error {
staring := IsStaring(ctx, doer.ID, repo.ID)
if star {
@@ -64,6 +65,10 @@ func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star
return nil
})
if err == nil && star {
pages_model.RecordRepoAction(ctx, repo.ID, pages_model.EventTypeStar)
}
return err
}
// IsStaring checks if user has starred given repository.

View File

@@ -33,6 +33,7 @@ const (
TypeProjects // 8 Projects
TypePackages // 9 Packages
TypeActions // 10 Actions
TypeAI // 11 AI
// FIXME: TEAM-UNIT-PERMISSION: the team unit "admin" permission's design is not right, when a new unit is added in the future,
// admin team won't inherit the correct admin permission for the new unit, need to have a complete fix before adding any new unit.
@@ -65,6 +66,7 @@ var (
TypeProjects,
TypePackages,
TypeActions,
TypeAI,
}
// DefaultRepoUnits contains the default unit types
@@ -110,6 +112,7 @@ var (
NotAllowedDefaultRepoUnits = []Type{
TypeExternalWiki,
TypeExternalTracker,
TypeAI,
}
disabledRepoUnitsAtomic atomic.Pointer[[]Type] // the units that have been globally disabled
@@ -328,6 +331,15 @@ var (
perm.AccessModeOwner,
}
UnitAI = Unit{
TypeAI,
"repo.ai",
"/ai",
"repo.ai.desc",
8,
perm.AccessModeOwner,
}
// Units contains all the units
Units = map[Type]Unit{
TypeCode: UnitCode,
@@ -340,6 +352,7 @@ var (
TypeProjects: UnitProjects,
TypePackages: UnitPackages,
TypeActions: UnitActions,
TypeAI: UnitAI,
}
)

View File

@@ -72,5 +72,38 @@ func GetSystemUserByName(name string) *User {
if IsGiteaActionsUserName(name) {
return NewActionsUser()
}
if IsAIUserName(name) {
return NewAIUser()
}
return nil
}
const (
AIUserID int64 = -3
AIUserName = "gitcaddy-ai"
AIUserEmail = "ai@gitcaddy.com"
)
func IsAIUserName(name string) bool {
return strings.EqualFold(name, AIUserName)
}
// NewAIUser creates and returns the system bot user for AI operations.
func NewAIUser() *User {
return &User{
ID: AIUserID,
Name: AIUserName,
LowerName: strings.ToLower(AIUserName),
IsActive: true,
FullName: "GitCaddy AI",
Email: AIUserEmail,
KeepEmailPrivate: true,
LoginName: AIUserName,
Type: UserTypeBot,
Visibility: structs.VisibleTypePublic,
}
}
func (u *User) IsAI() bool {
return u != nil && u.ID == AIUserID
}

View File

@@ -190,6 +190,43 @@ func (c *Client) SummarizeChanges(ctx context.Context, req *SummarizeChangesRequ
return &resp, nil
}
// GenerateIssueResponse requests an AI-generated response to an issue
func (c *Client) GenerateIssueResponse(ctx context.Context, req *GenerateIssueResponseRequest) (*GenerateIssueResponseResponse, error) {
if !IsEnabled() || !setting.AI.AllowAutoRespond {
return nil, errors.New("AI auto-respond is not enabled")
}
var resp GenerateIssueResponseResponse
if err := c.doRequest(ctx, http.MethodPost, "/issues/respond", req, &resp); err != nil {
log.Error("AI GenerateIssueResponse failed: %v", err)
return nil, err
}
return &resp, nil
}
// InspectWorkflow sends a workflow for AI inspection
func (c *Client) InspectWorkflow(ctx context.Context, req *InspectWorkflowRequest) (*InspectWorkflowResponse, error) {
resp := &InspectWorkflowResponse{}
if err := c.doRequest(ctx, "POST", "/api/v1/workflows/inspect", req, resp); err != nil {
return nil, err
}
return resp, nil
}
// ExecuteTask executes a generic AI task via the sidecar
func (c *Client) ExecuteTask(ctx context.Context, req *ExecuteTaskRequest) (*ExecuteTaskResponse, error) {
if !IsEnabled() {
return nil, errors.New("AI service is not enabled")
}
var resp ExecuteTaskResponse
if err := c.doRequest(ctx, http.MethodPost, "/execute-task", req, &resp); err != nil {
log.Error("AI ExecuteTask failed: %v", err)
return nil, err
}
return &resp, nil
}
// CheckHealth checks the health of the AI service
func (c *Client) CheckHealth(ctx context.Context) (*HealthCheckResponse, error) {
var resp HealthCheckResponse

View File

@@ -3,6 +3,15 @@
package ai
// ProviderConfig contains per-request AI provider configuration.
// When sent to the AI sidecar, it overrides the sidecar's default provider/model/key.
// Fields left empty fall back to the sidecar's defaults.
type ProviderConfig struct {
Provider string `json:"provider,omitempty"` // "claude", "openai", "gemini"
Model string `json:"model,omitempty"`
APIKey string `json:"api_key,omitempty"`
}
// FileDiff represents a file diff for code review
type FileDiff struct {
Path string `json:"path"`
@@ -54,14 +63,15 @@ type SecurityAnalysis struct {
// ReviewPullRequestRequest is the request for reviewing a pull request
type ReviewPullRequestRequest struct {
RepoID int64 `json:"repo_id"`
PullRequestID int64 `json:"pull_request_id"`
BaseBranch string `json:"base_branch"`
HeadBranch string `json:"head_branch"`
Files []FileDiff `json:"files"`
PRTitle string `json:"pr_title"`
PRDescription string `json:"pr_description"`
Options ReviewOptions `json:"options"`
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
RepoID int64 `json:"repo_id"`
PullRequestID int64 `json:"pull_request_id"`
BaseBranch string `json:"base_branch"`
HeadBranch string `json:"head_branch"`
Files []FileDiff `json:"files"`
PRTitle string `json:"pr_title"`
PRDescription string `json:"pr_description"`
Options ReviewOptions `json:"options"`
}
// ReviewPullRequestResponse is the response from reviewing a pull request
@@ -76,12 +86,13 @@ type ReviewPullRequestResponse struct {
// TriageIssueRequest is the request for triaging an issue
type TriageIssueRequest struct {
RepoID int64 `json:"repo_id"`
IssueID int64 `json:"issue_id"`
Title string `json:"title"`
Body string `json:"body"`
ExistingLabels []string `json:"existing_labels"`
AvailableLabels []string `json:"available_labels"`
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
RepoID int64 `json:"repo_id"`
IssueID int64 `json:"issue_id"`
Title string `json:"title"`
Body string `json:"body"`
ExistingLabels []string `json:"existing_labels"`
AvailableLabels []string `json:"available_labels"`
}
// TriageIssueResponse is the response from triaging an issue
@@ -97,10 +108,11 @@ type TriageIssueResponse struct {
// SuggestLabelsRequest is the request for suggesting labels
type SuggestLabelsRequest struct {
RepoID int64 `json:"repo_id"`
Title string `json:"title"`
Body string `json:"body"`
AvailableLabels []string `json:"available_labels"`
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
RepoID int64 `json:"repo_id"`
Title string `json:"title"`
Body string `json:"body"`
AvailableLabels []string `json:"available_labels"`
}
// LabelSuggestion represents a suggested label
@@ -117,12 +129,13 @@ type SuggestLabelsResponse struct {
// ExplainCodeRequest is the request for explaining code
type ExplainCodeRequest struct {
RepoID int64 `json:"repo_id"`
FilePath string `json:"file_path"`
Code string `json:"code"`
StartLine int `json:"start_line"`
EndLine int `json:"end_line"`
Question string `json:"question,omitempty"`
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
RepoID int64 `json:"repo_id"`
FilePath string `json:"file_path"`
Code string `json:"code"`
StartLine int `json:"start_line"`
EndLine int `json:"end_line"`
Question string `json:"question,omitempty"`
}
// CodeReference represents a reference to related documentation
@@ -140,12 +153,13 @@ type ExplainCodeResponse struct {
// GenerateDocumentationRequest is the request for generating documentation
type GenerateDocumentationRequest struct {
RepoID int64 `json:"repo_id"`
FilePath string `json:"file_path"`
Code string `json:"code"`
DocType string `json:"doc_type"` // function, class, module, api
Language string `json:"language"`
Style string `json:"style"` // jsdoc, docstring, xml, markdown
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
RepoID int64 `json:"repo_id"`
FilePath string `json:"file_path"`
Code string `json:"code"`
DocType string `json:"doc_type"` // function, class, module, api
Language string `json:"language"`
Style string `json:"style"` // jsdoc, docstring, xml, markdown
}
// DocumentationSection represents a section of documentation
@@ -162,9 +176,10 @@ type GenerateDocumentationResponse struct {
// GenerateCommitMessageRequest is the request for generating a commit message
type GenerateCommitMessageRequest struct {
RepoID int64 `json:"repo_id"`
Files []FileDiff `json:"files"`
Style string `json:"style"` // conventional, descriptive, brief
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
RepoID int64 `json:"repo_id"`
Files []FileDiff `json:"files"`
Style string `json:"style"` // conventional, descriptive, brief
}
// GenerateCommitMessageResponse is the response from generating a commit message
@@ -175,9 +190,10 @@ type GenerateCommitMessageResponse struct {
// SummarizeChangesRequest is the request for summarizing changes
type SummarizeChangesRequest struct {
RepoID int64 `json:"repo_id"`
Files []FileDiff `json:"files"`
Context string `json:"context"`
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
RepoID int64 `json:"repo_id"`
Files []FileDiff `json:"files"`
Context string `json:"context"`
}
// SummarizeChangesResponse is the response from summarizing changes
@@ -187,6 +203,77 @@ type SummarizeChangesResponse struct {
ImpactAssessment string `json:"impact_assessment"`
}
// IssueComment represents a comment on an issue for AI context
type IssueComment struct {
Author string `json:"author"`
Body string `json:"body"`
CreatedAt string `json:"created_at,omitempty"`
}
// GenerateIssueResponseRequest is the request for generating an AI response to an issue
type GenerateIssueResponseRequest struct {
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
RepoID int64 `json:"repo_id"`
IssueID int64 `json:"issue_id"`
Title string `json:"title"`
Body string `json:"body"`
Comments []IssueComment `json:"comments,omitempty"`
ResponseType string `json:"response_type,omitempty"` // clarification, solution, acknowledgment
CustomInstructions string `json:"custom_instructions,omitempty"`
}
// GenerateIssueResponseResponse is the response from generating an issue response
type GenerateIssueResponseResponse struct {
Response string `json:"response"`
FollowUpQuestions []string `json:"follow_up_questions,omitempty"`
Confidence float64 `json:"confidence"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
}
// InspectWorkflowRequest is the request for inspecting a workflow file
type InspectWorkflowRequest struct {
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
RepoID int64 `json:"repo_id"`
FilePath string `json:"file_path"`
Content string `json:"content"`
RunnerLabels []string `json:"runner_labels,omitempty"`
}
// WorkflowIssue represents an issue found in a workflow file
type WorkflowIssue struct {
Line int `json:"line"`
Severity string `json:"severity"` // "error", "warning", "info"
Message string `json:"message"`
Fix string `json:"fix,omitempty"`
}
// InspectWorkflowResponse is the response from inspecting a workflow file
type InspectWorkflowResponse struct {
Valid bool `json:"valid"`
Issues []WorkflowIssue `json:"issues"`
Suggestions []string `json:"suggestions"`
Confidence float64 `json:"confidence"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
}
// ExecuteTaskRequest is the request for executing a generic AI task
type ExecuteTaskRequest struct {
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
RepoID int64 `json:"repo_id"`
Task string `json:"task"`
Context map[string]string `json:"context"`
AllowedTools []string `json:"allowed_tools,omitempty"`
}
// ExecuteTaskResponse is the response from executing a generic AI task
type ExecuteTaskResponse struct {
Success bool `json:"success"`
Result string `json:"result"`
Error string `json:"error,omitempty"`
}
// HealthCheckResponse is the response from a health check
type HealthCheckResponse struct {
Healthy bool `json:"healthy"`

View File

@@ -170,6 +170,23 @@ const (
WishlistVoteBudget ErrorCode = "WISHLIST_VOTE_BUDGET_EXCEEDED"
)
// Pages errors (PAGES_)
const (
PagesNotConfigured ErrorCode = "PAGES_NOT_CONFIGURED"
PagesNotEnabled ErrorCode = "PAGES_NOT_ENABLED"
PagesInvalidTemplate ErrorCode = "PAGES_INVALID_TEMPLATE"
)
// AI errors (AI_)
const (
AIDisabled ErrorCode = "AI_DISABLED"
AIUnitNotEnabled ErrorCode = "AI_UNIT_NOT_ENABLED"
AIOperationNotFound ErrorCode = "AI_OPERATION_NOT_FOUND"
AIRateLimitExceeded ErrorCode = "AI_RATE_LIMIT_EXCEEDED"
AIServiceError ErrorCode = "AI_SERVICE_ERROR"
AIOperationDisabled ErrorCode = "AI_OPERATION_DISABLED"
)
// errorInfo contains metadata about an error code
type errorInfo struct {
Message string
@@ -299,6 +316,19 @@ var errorCatalog = map[ErrorCode]errorInfo{
WishlistItemNotFound: {"Wishlist item not found", http.StatusNotFound},
WishlistDisabled: {"Wishlist is disabled for this repository", http.StatusForbidden},
WishlistVoteBudget: {"Vote budget exceeded for this repository", http.StatusConflict},
// Pages errors
PagesNotConfigured: {"Landing page is not configured for this repository", http.StatusNotFound},
PagesNotEnabled: {"Landing page is not enabled for this repository", http.StatusForbidden},
PagesInvalidTemplate: {"Invalid landing page template name", http.StatusBadRequest},
// AI errors
AIDisabled: {"AI features are disabled", http.StatusForbidden},
AIUnitNotEnabled: {"AI unit is not enabled for this repository", http.StatusForbidden},
AIOperationNotFound: {"AI operation not found", http.StatusNotFound},
AIRateLimitExceeded: {"AI operation rate limit exceeded", http.StatusTooManyRequests},
AIServiceError: {"AI service error", http.StatusBadGateway},
AIOperationDisabled: {"This AI operation is not enabled", http.StatusForbidden},
}
// Message returns the human-readable message for an error code

156
modules/git/tree_filter.go Normal file
View File

@@ -0,0 +1,156 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"bytes"
"fmt"
"strings"
"code.gitcaddy.com/server/v3/modules/git/gitcmd"
)
// FilterTreeOptions controls which paths are excluded when building a filtered tree.
type FilterTreeOptions struct {
// ExcludeDotFiles excludes all entries whose name starts with "."
ExcludeDotFiles bool
// ExcludePaths is a set of specific paths to exclude (exact top-level names)
ExcludePaths map[string]bool
}
// ShouldExclude returns true if the given entry name should be filtered out.
func (opts *FilterTreeOptions) ShouldExclude(name string) bool {
if opts.ExcludeDotFiles && strings.HasPrefix(name, ".") {
return true
}
if opts.ExcludePaths != nil && opts.ExcludePaths[name] {
return true
}
return false
}
// HasFilters returns true if any filtering is configured.
func (opts *FilterTreeOptions) HasFilters() bool {
return opts.ExcludeDotFiles || len(opts.ExcludePaths) > 0
}
// BuildFilteredCommit creates a new commit that has the same content as the
// given commit but with excluded paths removed from the top-level tree.
// It uses git plumbing (ls-tree, mktree, commit-tree) and creates no refs —
// only loose objects that git gc will collect naturally.
// Returns the SHA of the new commit, or the original SHA if nothing was filtered.
func (repo *Repository) BuildFilteredCommit(commitSHA string, opts *FilterTreeOptions) (string, error) {
if !opts.HasFilters() {
return commitSHA, nil
}
// 1. Get the top-level tree entries for this commit
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
err := gitcmd.NewCommand("ls-tree").AddArguments("--").AddDynamicArguments(commitSHA).
WithDir(repo.Path).
WithStdout(stdout).
WithStderr(stderr).
Run(repo.Ctx)
if err != nil {
return "", fmt.Errorf("ls-tree %s: %s: %w", commitSHA, stderr.String(), err)
}
// 2. Filter entries
var filteredLines []string
excluded := false
for line := range strings.SplitSeq(strings.TrimRight(stdout.String(), "\n"), "\n") {
if line == "" {
continue
}
// ls-tree output format: "<mode> <type> <sha>\t<name>"
tabIdx := strings.IndexByte(line, '\t')
if tabIdx < 0 {
continue
}
name := line[tabIdx+1:]
if opts.ShouldExclude(name) {
excluded = true
continue
}
filteredLines = append(filteredLines, line)
}
// Nothing was filtered — return original commit
if !excluded {
return commitSHA, nil
}
// 3. Create a new tree from filtered entries via mktree
treeInput := strings.Join(filteredLines, "\n") + "\n"
stdout.Reset()
stderr.Reset()
err = gitcmd.NewCommand("mktree").
WithDir(repo.Path).
WithStdin(strings.NewReader(treeInput)).
WithStdout(stdout).
WithStderr(stderr).
Run(repo.Ctx)
if err != nil {
return "", fmt.Errorf("mktree: %s: %w", stderr.String(), err)
}
newTreeSHA := strings.TrimSpace(stdout.String())
// 4. Get the original commit's parent(s) and message
stdout.Reset()
stderr.Reset()
err = gitcmd.NewCommand("log", "-1", "--format=%P%n%B").AddArguments("--").AddDynamicArguments(commitSHA).
WithDir(repo.Path).
WithStdout(stdout).
WithStderr(stderr).
Run(repo.Ctx)
if err != nil {
return "", fmt.Errorf("log %s: %s: %w", commitSHA, stderr.String(), err)
}
logOutput := stdout.String()
lines := strings.SplitN(logOutput, "\n", 2)
parentLine := ""
message := "filtered commit"
if len(lines) >= 1 {
parentLine = strings.TrimSpace(lines[0])
}
if len(lines) >= 2 {
message = strings.TrimRight(lines[1], "\n")
}
// 5. Create a new commit referencing the filtered tree
cmd := gitcmd.NewCommand("commit-tree").AddDynamicArguments(newTreeSHA)
for parent := range strings.FieldsSeq(parentLine) {
cmd = cmd.AddArguments("-p").AddDynamicArguments(parent)
}
messageReader := strings.NewReader(message + "\n")
stdout.Reset()
stderr.Reset()
err = cmd.
WithDir(repo.Path).
WithStdin(messageReader).
WithStdout(stdout).
WithStderr(stderr).
Run(repo.Ctx)
if err != nil {
return "", fmt.Errorf("commit-tree: %s: %w", stderr.String(), err)
}
return strings.TrimSpace(stdout.String()), nil
}
// UpdateRef updates a ref to point at the given SHA.
func (repo *Repository) UpdateRef(ref, sha string) error {
stderr := new(bytes.Buffer)
err := gitcmd.NewCommand("update-ref").AddDynamicArguments(ref, sha).
WithDir(repo.Path).
WithStderr(stderr).
Run(repo.Ctx)
if err != nil {
return fmt.Errorf("update-ref %s %s: %s: %w", ref, sha, stderr.String(), err)
}
return nil
}

View File

@@ -36,6 +36,12 @@ func License(name string) ([]byte, error) {
return AssetFS().ReadFile("license", name)
}
// LicenseBundleFile reads a single file from a license bundle directory
// (e.g. options/license-bundles/CSL-1.0/GOVERNANCE.md).
func LicenseBundleFile(bundleName, fileName string) ([]byte, error) {
return AssetFS().ReadFile("license-bundles", bundleName, fileName)
}
// Labels reads the content of a specific labels from static/bindata or custom path.
func Labels(name string) ([]byte, error) {
return AssetFS().ReadFile("label", name)

View File

@@ -13,194 +13,375 @@ import (
// LandingConfig represents the parsed .gitea/landing.yaml configuration
type LandingConfig struct {
Enabled bool `yaml:"enabled"`
PublicLanding bool `yaml:"public_landing"`
Template string `yaml:"template"` // open-source-hero, minimalist-docs, saas-conversion, bold-marketing
Enabled bool `yaml:"enabled" json:"enabled"`
PublicLanding bool `yaml:"public_landing" json:"public_landing"`
Template string `yaml:"template" json:"template"` // open-source-hero, minimalist-docs, saas-conversion, bold-marketing
// Custom domain (optional)
Domain string `yaml:"domain,omitempty"`
Domain string `yaml:"domain,omitempty" json:"domain,omitempty"`
// Brand configuration
Brand BrandConfig `yaml:"brand,omitempty"`
Brand BrandConfig `yaml:"brand,omitempty" json:"brand,omitzero"`
// Hero section
Hero HeroConfig `yaml:"hero,omitempty"`
Hero HeroConfig `yaml:"hero,omitempty" json:"hero,omitzero"`
// Stats/metrics
Stats []StatConfig `yaml:"stats,omitempty"`
Stats []StatConfig `yaml:"stats,omitempty" json:"stats,omitempty"`
// Value propositions
ValueProps []ValuePropConfig `yaml:"value_props,omitempty"`
ValueProps []ValuePropConfig `yaml:"value_props,omitempty" json:"value_props,omitempty"`
ValuePropsHeadline string `yaml:"value_props_headline,omitempty" json:"value_props_headline,omitempty"`
ValuePropsSubheadline string `yaml:"value_props_subheadline,omitempty" json:"value_props_subheadline,omitempty"`
// Features
Features []FeatureConfig `yaml:"features,omitempty"`
Features []FeatureConfig `yaml:"features,omitempty" json:"features,omitempty"`
FeaturesHeadline string `yaml:"features_headline,omitempty" json:"features_headline,omitempty"`
FeaturesSubheadline string `yaml:"features_subheadline,omitempty" json:"features_subheadline,omitempty"`
// Social proof
SocialProof SocialProofConfig `yaml:"social_proof,omitempty"`
SocialProof SocialProofConfig `yaml:"social_proof,omitempty" json:"social_proof,omitzero"`
// Pricing (for saas-conversion template)
Pricing PricingConfig `yaml:"pricing,omitempty"`
Pricing PricingConfig `yaml:"pricing,omitempty" json:"pricing,omitzero"`
// CTA section
CTASection CTASectionConfig `yaml:"cta_section,omitempty"`
CTASection CTASectionConfig `yaml:"cta_section,omitempty" json:"cta_section,omitzero"`
// Blog section
Blog BlogSectionConfig `yaml:"blog,omitempty" json:"blog,omitzero"`
// Gallery section
Gallery GallerySectionConfig `yaml:"gallery,omitempty" json:"gallery,omitzero"`
// Comparison section
Comparison ComparisonSectionConfig `yaml:"comparison,omitempty" json:"comparison,omitzero"`
// Cross-promote section
CrossPromote CrossPromoteSectionConfig `yaml:"cross_promote,omitempty" json:"cross_promote,omitzero"`
// Navigation visibility
Navigation NavigationConfig `yaml:"navigation,omitempty" json:"navigation,omitzero"`
// Footer
Footer FooterConfig `yaml:"footer,omitempty"`
Footer FooterConfig `yaml:"footer,omitempty" json:"footer,omitzero"`
// Theme customization
Theme ThemeConfig `yaml:"theme,omitempty"`
Theme ThemeConfig `yaml:"theme,omitempty" json:"theme,omitzero"`
// SEO & Social
SEO SEOConfig `yaml:"seo,omitempty"`
SEO SEOConfig `yaml:"seo,omitempty" json:"seo,omitzero"`
// Analytics
Analytics AnalyticsConfig `yaml:"analytics,omitempty"`
Analytics AnalyticsConfig `yaml:"analytics,omitempty" json:"analytics,omitzero"`
// Advanced settings
Advanced AdvancedConfig `yaml:"advanced,omitempty"`
Advanced AdvancedConfig `yaml:"advanced,omitempty" json:"advanced,omitzero"`
// A/B testing experiments
Experiments ExperimentConfig `yaml:"experiments,omitempty" json:"experiments,omitzero"`
// Multi-language support
I18n I18nConfig `yaml:"i18n,omitempty" json:"i18n,omitzero"`
}
// BrandConfig represents brand/identity settings
type BrandConfig struct {
Name string `yaml:"name,omitempty"`
LogoURL string `yaml:"logo_url,omitempty"`
Tagline string `yaml:"tagline,omitempty"`
Name string `yaml:"name,omitempty" json:"name,omitempty"`
LogoURL string `yaml:"logo_url,omitempty" json:"logo_url,omitempty"`
UploadedLogo string `yaml:"uploaded_logo,omitempty" json:"uploaded_logo,omitempty"`
LogoSource string `yaml:"logo_source,omitempty" json:"logo_source,omitempty"` // "url" (default), "repo", or "org" — selects avatar source
Tagline string `yaml:"tagline,omitempty" json:"tagline,omitempty"`
FaviconURL string `yaml:"favicon_url,omitempty" json:"favicon_url,omitempty"`
UploadedFavicon string `yaml:"uploaded_favicon,omitempty" json:"uploaded_favicon,omitempty"`
}
// ResolvedLogoURL returns the uploaded logo path or the external URL.
func (b *BrandConfig) ResolvedLogoURL() string {
if b.UploadedLogo != "" {
return "/repo-avatars/" + b.UploadedLogo
}
return b.LogoURL
}
// ResolvedFaviconURL returns the uploaded favicon path or the external URL.
func (b *BrandConfig) ResolvedFaviconURL() string {
if b.UploadedFavicon != "" {
return "/repo-avatars/" + b.UploadedFavicon
}
return b.FaviconURL
}
// HeroConfig represents hero section settings
type HeroConfig struct {
Headline string `yaml:"headline,omitempty"`
Subheadline string `yaml:"subheadline,omitempty"`
PrimaryCTA CTAButton `yaml:"primary_cta,omitempty"`
SecondaryCTA CTAButton `yaml:"secondary_cta,omitempty"`
ImageURL string `yaml:"image_url,omitempty"`
CodeExample string `yaml:"code_example,omitempty"`
VideoURL string `yaml:"video_url,omitempty"`
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
PrimaryCTA CTAButton `yaml:"primary_cta,omitempty" json:"primary_cta,omitzero"`
SecondaryCTA CTAButton `yaml:"secondary_cta,omitempty" json:"secondary_cta,omitzero"`
ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"`
UploadedImage string `yaml:"uploaded_image,omitempty" json:"uploaded_image,omitempty"` // filename in repo-avatars storage
CodeExample string `yaml:"code_example,omitempty" json:"code_example,omitempty"`
VideoURL string `yaml:"video_url,omitempty" json:"video_url,omitempty"`
}
// ResolvedImageURL returns the effective hero image URL, preferring uploaded image over URL.
func (h *HeroConfig) ResolvedImageURL() string {
if h.UploadedImage != "" {
return "/repo-avatars/" + h.UploadedImage
}
return h.ImageURL
}
// CTAButton represents a call-to-action button
type CTAButton struct {
Label string `yaml:"label,omitempty"`
URL string `yaml:"url,omitempty"`
Variant string `yaml:"variant,omitempty"` // primary, secondary, outline, text
Label string `yaml:"label,omitempty" json:"label,omitempty"`
URL string `yaml:"url,omitempty" json:"url,omitempty"`
Variant string `yaml:"variant,omitempty" json:"variant,omitempty"` // primary, secondary, outline, text
}
// StatConfig represents a single stat/metric
type StatConfig struct {
Value string `yaml:"value,omitempty"`
Label string `yaml:"label,omitempty"`
Value string `yaml:"value,omitempty" json:"value,omitempty"`
Label string `yaml:"label,omitempty" json:"label,omitempty"`
}
// ValuePropConfig represents a value proposition
type ValuePropConfig struct {
Title string `yaml:"title,omitempty"`
Description string `yaml:"description,omitempty"`
Icon string `yaml:"icon,omitempty"`
Title string `yaml:"title,omitempty" json:"title,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Icon string `yaml:"icon,omitempty" json:"icon,omitempty"`
}
// FeatureConfig represents a single feature item
type FeatureConfig struct {
Title string `yaml:"title,omitempty"`
Description string `yaml:"description,omitempty"`
Icon string `yaml:"icon,omitempty"`
ImageURL string `yaml:"image_url,omitempty"`
Title string `yaml:"title,omitempty" json:"title,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Icon string `yaml:"icon,omitempty" json:"icon,omitempty"`
ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"`
}
// SocialProofConfig represents social proof section
type SocialProofConfig struct {
Logos []string `yaml:"logos,omitempty"`
Testimonial TestimonialConfig `yaml:"testimonial,omitempty"`
Testimonials []TestimonialConfig `yaml:"testimonials,omitempty"`
Logos []string `yaml:"logos,omitempty" json:"logos,omitempty"`
Testimonial TestimonialConfig `yaml:"testimonial,omitempty" json:"testimonial,omitzero"`
Testimonials []TestimonialConfig `yaml:"testimonials,omitempty" json:"testimonials,omitempty"`
}
// TestimonialConfig represents a testimonial
type TestimonialConfig struct {
Quote string `yaml:"quote,omitempty"`
Author string `yaml:"author,omitempty"`
Role string `yaml:"role,omitempty"`
Avatar string `yaml:"avatar,omitempty"`
Quote string `yaml:"quote,omitempty" json:"quote,omitempty"`
Author string `yaml:"author,omitempty" json:"author,omitempty"`
Role string `yaml:"role,omitempty" json:"role,omitempty"`
Avatar string `yaml:"avatar,omitempty" json:"avatar,omitempty"`
}
// PricingConfig represents pricing section
type PricingConfig struct {
Headline string `yaml:"headline,omitempty"`
Subheadline string `yaml:"subheadline,omitempty"`
Plans []PricingPlanConfig `yaml:"plans,omitempty"`
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
Plans []PricingPlanConfig `yaml:"plans,omitempty" json:"plans,omitempty"`
}
// PricingPlanConfig represents a pricing plan
type PricingPlanConfig struct {
Name string `yaml:"name,omitempty"`
Price string `yaml:"price,omitempty"`
Period string `yaml:"period,omitempty"`
Features []string `yaml:"features,omitempty"`
CTA string `yaml:"cta,omitempty"`
Featured bool `yaml:"featured,omitempty"`
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Price string `yaml:"price,omitempty" json:"price,omitempty"`
Period string `yaml:"period,omitempty" json:"period,omitempty"`
Features []string `yaml:"features,omitempty" json:"features,omitempty"`
CTA string `yaml:"cta,omitempty" json:"cta,omitempty"`
Featured bool `yaml:"featured,omitempty" json:"featured,omitempty"`
}
// CTASectionConfig represents the final CTA section
type CTASectionConfig struct {
Headline string `yaml:"headline,omitempty"`
Subheadline string `yaml:"subheadline,omitempty"`
Button CTAButton `yaml:"button,omitempty"`
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
Button CTAButton `yaml:"button,omitempty" json:"button,omitzero"`
}
// BlogSectionConfig represents blog section settings on the landing page
type BlogSectionConfig struct {
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
MaxPosts int `yaml:"max_posts,omitempty" json:"max_posts,omitempty"` // default 3
ShowExcerpt bool `yaml:"show_excerpt,omitempty" json:"show_excerpt,omitempty"` // show subtitle as excerpt
CTAButton CTAButton `yaml:"cta_button,omitempty" json:"cta_button,omitzero"` // "View All Posts" link
}
// GallerySectionConfig represents gallery section settings on the landing page
type GallerySectionConfig struct {
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
MaxImages int `yaml:"max_images,omitempty" json:"max_images,omitempty"` // default 6
Columns int `yaml:"columns,omitempty" json:"columns,omitempty"` // grid columns, default 3
}
// ComparisonSectionConfig represents a feature comparison matrix section
type ComparisonSectionConfig struct {
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
Columns []ComparisonColumnConfig `yaml:"columns,omitempty" json:"columns,omitempty"`
Groups []ComparisonGroupConfig `yaml:"groups,omitempty" json:"groups,omitempty"`
}
// HasData returns true if the comparison section has columns and at least one feature
func (c *ComparisonSectionConfig) HasData() bool {
if len(c.Columns) == 0 || len(c.Groups) == 0 {
return false
}
for _, g := range c.Groups {
if len(g.Features) > 0 {
return true
}
}
return false
}
// ComparisonColumnConfig represents a column header in the comparison table
type ComparisonColumnConfig struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Highlight bool `yaml:"highlight,omitempty" json:"highlight,omitempty"`
}
// ComparisonGroupConfig represents a group of features in the comparison table
type ComparisonGroupConfig struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Features []ComparisonFeatureConfig `yaml:"features,omitempty" json:"features,omitempty"`
}
// ComparisonFeatureConfig represents a single feature row in the comparison table
type ComparisonFeatureConfig struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Values []string `yaml:"values,omitempty" json:"values,omitempty"` // "true"/"false" for check/x, anything else displayed as text
}
// CrossPromoteSectionConfig controls the cross-promote section on the landing page
type CrossPromoteSectionConfig struct {
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
}
// NavigationConfig controls which built-in navigation links appear in the header and footer
type NavigationConfig struct {
ShowDocs bool `yaml:"show_docs,omitempty" json:"show_docs,omitempty"`
ShowAPI bool `yaml:"show_api,omitempty" json:"show_api,omitempty"`
ShowRepository bool `yaml:"show_repository,omitempty" json:"show_repository,omitempty"`
ShowReleases bool `yaml:"show_releases,omitempty" json:"show_releases,omitempty"`
ShowIssues bool `yaml:"show_issues,omitempty" json:"show_issues,omitempty"`
// Translatable labels for nav items and section headers (defaults to English)
LabelValueProps string `yaml:"label_value_props,omitempty" json:"label_value_props,omitempty"`
LabelFeatures string `yaml:"label_features,omitempty" json:"label_features,omitempty"`
LabelPricing string `yaml:"label_pricing,omitempty" json:"label_pricing,omitempty"`
LabelBlog string `yaml:"label_blog,omitempty" json:"label_blog,omitempty"`
LabelGallery string `yaml:"label_gallery,omitempty" json:"label_gallery,omitempty"`
LabelCompare string `yaml:"label_compare,omitempty" json:"label_compare,omitempty"`
LabelCrossPromote string `yaml:"label_cross_promote,omitempty" json:"label_cross_promote,omitempty"`
LabelDocs string `yaml:"label_docs,omitempty" json:"label_docs,omitempty"`
LabelReleases string `yaml:"label_releases,omitempty" json:"label_releases,omitempty"`
LabelAPI string `yaml:"label_api,omitempty" json:"label_api,omitempty"`
LabelIssues string `yaml:"label_issues,omitempty" json:"label_issues,omitempty"`
}
// FooterConfig represents footer settings
type FooterConfig struct {
Links []FooterLink `yaml:"links,omitempty"`
Social []SocialLink `yaml:"social,omitempty"`
Copyright string `yaml:"copyright,omitempty"`
ShowPoweredBy bool `yaml:"show_powered_by,omitempty"`
Links []FooterLink `yaml:"links,omitempty" json:"links,omitempty"`
Social []SocialLink `yaml:"social,omitempty" json:"social,omitempty"`
Copyright string `yaml:"copyright,omitempty" json:"copyright,omitempty"`
ShowPoweredBy bool `yaml:"show_powered_by,omitempty" json:"show_powered_by,omitempty"`
}
// FooterLink represents a single footer link
type FooterLink struct {
Label string `yaml:"label,omitempty"`
URL string `yaml:"url,omitempty"`
Label string `yaml:"label,omitempty" json:"label,omitempty"`
URL string `yaml:"url,omitempty" json:"url,omitempty"`
}
// SocialLink represents a social media link
type SocialLink struct {
Platform string `yaml:"platform,omitempty"` // twitter, github, discord, linkedin, youtube
URL string `yaml:"url,omitempty"`
Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` // bluesky, discord, facebook, github, instagram, linkedin, mastodon, reddit, rss, substack, threads, tiktok, twitch, twitter, youtube
URL string `yaml:"url,omitempty" json:"url,omitempty"`
}
// ThemeConfig represents theme customization
type ThemeConfig struct {
PrimaryColor string `yaml:"primary_color,omitempty"`
AccentColor string `yaml:"accent_color,omitempty"`
Mode string `yaml:"mode,omitempty"` // light, dark, auto
PrimaryColor string `yaml:"primary_color,omitempty" json:"primary_color,omitempty"`
AccentColor string `yaml:"accent_color,omitempty" json:"accent_color,omitempty"`
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"` // light, dark, auto
}
// SEOConfig represents SEO and social sharing settings
type SEOConfig struct {
Title string `yaml:"title,omitempty"`
Description string `yaml:"description,omitempty"`
Keywords []string `yaml:"keywords,omitempty"`
OGImage string `yaml:"og_image,omitempty"`
TwitterCard string `yaml:"twitter_card,omitempty"`
TwitterSite string `yaml:"twitter_site,omitempty"`
Title string `yaml:"title,omitempty" json:"title,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Keywords []string `yaml:"keywords,omitempty" json:"keywords,omitempty"`
OGImage string `yaml:"og_image,omitempty" json:"og_image,omitempty"`
UseMediaKitOG bool `yaml:"use_media_kit_og,omitempty" json:"use_media_kit_og,omitempty"`
TwitterCard string `yaml:"twitter_card,omitempty" json:"twitter_card,omitempty"`
TwitterSite string `yaml:"twitter_site,omitempty" json:"twitter_site,omitempty"`
}
// AnalyticsConfig represents analytics settings
type AnalyticsConfig struct {
Plausible string `yaml:"plausible,omitempty"`
Umami UmamiConfig `yaml:"umami,omitempty"`
GoogleAnalytics string `yaml:"google_analytics,omitempty"`
Plausible string `yaml:"plausible,omitempty" json:"plausible,omitempty"`
Umami UmamiConfig `yaml:"umami,omitempty" json:"umami,omitzero"`
GoogleAnalytics string `yaml:"google_analytics,omitempty" json:"google_analytics,omitempty"`
}
// UmamiConfig represents Umami analytics settings
type UmamiConfig struct {
WebsiteID string `yaml:"website_id,omitempty"`
URL string `yaml:"url,omitempty"`
WebsiteID string `yaml:"website_id,omitempty" json:"website_id,omitempty"`
URL string `yaml:"url,omitempty" json:"url,omitempty"`
}
// ExperimentConfig represents A/B testing experiment settings
type ExperimentConfig struct {
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
AutoOptimize bool `yaml:"auto_optimize,omitempty" json:"auto_optimize,omitempty"`
MinImpressions int `yaml:"min_impressions,omitempty" json:"min_impressions,omitempty"`
ApprovalRequired bool `yaml:"approval_required,omitempty" json:"approval_required,omitempty"`
}
// I18nConfig represents multi-language settings for the landing page
type I18nConfig struct {
DefaultLang string `yaml:"default_lang,omitempty" json:"default_lang,omitempty"`
Languages []string `yaml:"languages,omitempty" json:"languages,omitempty"`
}
// LanguageDisplayNames returns a map of language codes to native display names
func LanguageDisplayNames() map[string]string {
return map[string]string{
"en": "English",
"es": "Español",
"de": "Deutsch",
"fr": "Français",
"ja": "日本語",
"zh": "中文",
"pt": "Português",
"ru": "Русский",
"ko": "한국어",
"it": "Italiano",
"hi": "हिन्दी",
"ar": "العربية",
"nl": "Nederlands",
"pl": "Polski",
"tr": "Türkçe",
}
}
// AdvancedConfig represents advanced settings
type AdvancedConfig struct {
CustomCSS string `yaml:"custom_css,omitempty"`
CustomHead string `yaml:"custom_head,omitempty"`
Redirects map[string]string `yaml:"redirects,omitempty"`
PublicReleases bool `yaml:"public_releases,omitempty"`
CustomCSS string `yaml:"custom_css,omitempty" json:"custom_css,omitempty"`
CustomHead string `yaml:"custom_head,omitempty" json:"custom_head,omitempty"`
Redirects map[string]string `yaml:"redirects,omitempty" json:"redirects,omitempty"`
StaticRoutes []string `yaml:"static_routes,omitempty" json:"static_routes,omitempty"`
PublicReleases bool `yaml:"public_releases,omitempty" json:"public_releases,omitempty"`
HideMobileReleases bool `yaml:"hide_mobile_releases,omitempty" json:"hide_mobile_releases,omitempty"`
GooglePlayID string `yaml:"google_play_id,omitempty" json:"google_play_id,omitempty"`
AppStoreID string `yaml:"app_store_id,omitempty" json:"app_store_id,omitempty"`
}
// ParseLandingConfig parses a landing.yaml file content
@@ -258,6 +439,11 @@ func DefaultConfig() *LandingConfig {
{Title: "Flexible", Description: "Adapts to your workflow, not the other way around.", Icon: "gear"},
{Title: "Open Source", Description: "Free forever. Community driven.", Icon: "heart"},
},
Navigation: NavigationConfig{
ShowDocs: true,
ShowRepository: true,
ShowReleases: true,
},
CTASection: CTASectionConfig{
Headline: "Ready to get started?",
Subheadline: "Join thousands of developers already using this project.",
@@ -277,7 +463,7 @@ func DefaultConfig() *LandingConfig {
// ValidTemplates returns the list of valid template names
func ValidTemplates() []string {
return []string{"open-source-hero", "minimalist-docs", "saas-conversion", "bold-marketing"}
return []string{"open-source-hero", "minimalist-docs", "saas-conversion", "bold-marketing", "documentation-first", "developer-tool", "visual-showcase", "cli-terminal", "architecture-deep-dive"}
}
// IsValidTemplate checks if a template name is valid
@@ -288,9 +474,82 @@ func IsValidTemplate(name string) bool {
// TemplateDisplayNames returns a map of template names to display names
func TemplateDisplayNames() map[string]string {
return map[string]string{
"open-source-hero": "Open Source Hero",
"minimalist-docs": "Minimalist Docs",
"saas-conversion": "SaaS Conversion",
"bold-marketing": "Bold Marketing",
"open-source-hero": "Open Source Product",
"minimalist-docs": "Minimalist Product",
"saas-conversion": "SaaS Product",
"bold-marketing": "Bold Marketing Product",
"documentation-first": "Documentation First",
"developer-tool": "Developer Tool",
"visual-showcase": "Visual Showcase",
"cli-terminal": "CLI Terminal",
"architecture-deep-dive": "Architecture Deep Dive",
}
}
// TemplateDefaultLabels returns the template-specific default section labels.
// These are the creative names each template uses for its sections.
func TemplateDefaultLabels(template string) NavigationConfig {
switch template {
case "architecture-deep-dive":
return NavigationConfig{
LabelValueProps: "Systems Analysis",
LabelFeatures: "Technical Specifications",
LabelPricing: "Resource Allocation",
LabelBlog: "Dispatches",
LabelGallery: "Visual Index",
LabelCompare: "Compare",
LabelCrossPromote: "Related Offerings",
}
case "bold-marketing":
return NavigationConfig{
LabelValueProps: "Why choose this",
LabelFeatures: "Capabilities",
LabelPricing: "Investment",
LabelBlog: "Blog",
LabelGallery: "Gallery",
LabelCompare: "Compare",
LabelCrossPromote: "Related Offerings",
}
case "minimalist-docs":
return NavigationConfig{
LabelValueProps: "Why choose this",
LabelFeatures: "Capabilities",
LabelPricing: "Investment",
LabelBlog: "Blog",
LabelGallery: "Gallery",
LabelCompare: "Compare",
LabelCrossPromote: "Related Offerings",
}
case "open-source-hero":
return NavigationConfig{
LabelValueProps: "Why choose us",
LabelFeatures: "Capabilities",
LabelPricing: "Pricing",
LabelBlog: "Blog",
LabelGallery: "Gallery",
LabelCompare: "Compare",
LabelCrossPromote: "Related Offerings",
}
case "saas-conversion":
return NavigationConfig{
LabelValueProps: "Why",
LabelFeatures: "Features",
LabelPricing: "Pricing",
LabelBlog: "Blog",
LabelGallery: "Gallery",
LabelCompare: "Compare",
LabelCrossPromote: "Related Offerings",
}
default:
// developer-tool, documentation-first, visual-showcase, cli-terminal
return NavigationConfig{
LabelValueProps: "Why choose us",
LabelFeatures: "Capabilities",
LabelPricing: "Pricing",
LabelBlog: "Blog",
LabelGallery: "Gallery",
LabelCompare: "Compare",
LabelCrossPromote: "Related Offerings",
}
}
}

96
modules/plugins/config.go Normal file
View File

@@ -0,0 +1,96 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package plugins
import (
"strings"
"time"
"code.gitcaddy.com/server/v3/modules/log"
"code.gitcaddy.com/server/v3/modules/setting"
)
// ExternalPluginConfig holds configuration for a single external plugin
type ExternalPluginConfig struct {
Name string
Enabled bool
// Managed mode: server launches the binary
Binary string
Args string
// External mode: connect to already-running process
Address string
// Common
SubscribedEvents []string
HealthTimeout time.Duration
}
// Config holds the global [plugins] configuration
type Config struct {
Enabled bool
Path string
HealthCheckInterval time.Duration
ExternalPlugins map[string]*ExternalPluginConfig
}
// LoadConfig loads plugin configuration from app.ini [plugins] and [plugins.*] sections
func LoadConfig() *Config {
cfg := &Config{
ExternalPlugins: make(map[string]*ExternalPluginConfig),
}
sec := setting.CfgProvider.Section("plugins")
cfg.Enabled = sec.Key("ENABLED").MustBool(true)
cfg.Path = sec.Key("PATH").MustString("data/plugins")
cfg.HealthCheckInterval = sec.Key("HEALTH_CHECK_INTERVAL").MustDuration(30 * time.Second)
// Load [plugins.*] sections for external plugins
for _, childSec := range sec.ChildSections() {
name := strings.TrimPrefix(childSec.Name(), "plugins.")
if name == "" {
continue
}
pluginCfg := &ExternalPluginConfig{
Name: name,
Enabled: childSec.Key("ENABLED").MustBool(true),
Binary: childSec.Key("BINARY").MustString(""),
Args: childSec.Key("ARGS").MustString(""),
Address: childSec.Key("ADDRESS").MustString(""),
HealthTimeout: childSec.Key("HEALTH_TIMEOUT").MustDuration(5 * time.Second),
}
// Parse subscribed events
if eventsStr := childSec.Key("SUBSCRIBED_EVENTS").MustString(""); eventsStr != "" {
pluginCfg.SubscribedEvents = splitAndTrim(eventsStr)
}
// Validate: must have either binary or address
if pluginCfg.Binary == "" && pluginCfg.Address == "" {
log.Warn("Plugin %q has neither BINARY nor ADDRESS configured, skipping", name)
continue
}
cfg.ExternalPlugins[name] = pluginCfg
log.Info("Loaded external plugin config: %s (managed=%v)", name, pluginCfg.IsManaged())
}
return cfg
}
// IsManaged returns true if the server manages the plugin's lifecycle (has a binary)
func (c *ExternalPluginConfig) IsManaged() bool {
return c.Binary != ""
}
// splitAndTrim splits a comma-separated string and trims whitespace
func splitAndTrim(s string) []string {
var result []string
for part := range strings.SplitSeq(s, ",") {
part = strings.TrimSpace(part)
if part != "" {
result = append(result, part)
}
}
return result
}

359
modules/plugins/external.go Normal file
View File

@@ -0,0 +1,359 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package plugins
import (
"context"
"crypto/tls"
"fmt"
"maps"
"net"
"net/http"
"os"
"os/exec"
"strings"
"sync"
"time"
"connectrpc.com/connect"
"golang.org/x/net/http2"
"code.gitcaddy.com/server/v3/modules/graceful"
"code.gitcaddy.com/server/v3/modules/log"
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
"code.gitcaddy.com/server/v3/modules/plugins/pluginv1/pluginv1connect"
)
// PluginStatus represents the status of an external plugin
type PluginStatus string
const (
PluginStatusStarting PluginStatus = "starting"
PluginStatusOnline PluginStatus = "online"
PluginStatusOffline PluginStatus = "offline"
PluginStatusError PluginStatus = "error"
// ProtocolVersion is the current plugin protocol version.
// Increment this when new RPCs are added to PluginService.
// The server uses this to avoid calling RPCs that older plugins don't implement.
ProtocolVersion int32 = 1
)
// ManagedPlugin tracks the state of an external plugin
type ManagedPlugin struct {
config *ExternalPluginConfig
process *os.Process
status PluginStatus
lastSeen time.Time
manifest *pluginv1.PluginManifest
protocolVersion int32 // protocol version reported by the plugin (0 = pre-versioning, treated as 1)
failCount int
client pluginv1connect.PluginServiceClient
mu sync.RWMutex
}
// ExternalPluginManager manages external plugins (both managed and external mode)
type ExternalPluginManager struct {
mu sync.RWMutex
plugins map[string]*ManagedPlugin
config *Config
ctx context.Context
cancel context.CancelFunc
}
var globalExternalManager *ExternalPluginManager
// GetExternalManager returns the global external plugin manager
func GetExternalManager() *ExternalPluginManager {
return globalExternalManager
}
// NewExternalPluginManager creates a new external plugin manager
func NewExternalPluginManager(config *Config) *ExternalPluginManager {
ctx, cancel := context.WithCancel(context.Background())
m := &ExternalPluginManager{
plugins: make(map[string]*ManagedPlugin),
config: config,
ctx: ctx,
cancel: cancel,
}
globalExternalManager = m
return m
}
// StartAll launches managed plugins and connects to external ones
func (m *ExternalPluginManager) StartAll() error {
m.mu.Lock()
defer m.mu.Unlock()
for name, cfg := range m.config.ExternalPlugins {
if !cfg.Enabled {
log.Info("External plugin %s is disabled, skipping", name)
continue
}
address := cfg.Address
if address == "" {
log.Error("External plugin %s has no address configured", name)
continue
}
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
address = "http://" + address
}
mp := &ManagedPlugin{
config: cfg,
status: PluginStatusStarting,
client: pluginv1connect.NewPluginServiceClient(
newH2CClient(cfg.HealthTimeout),
address,
connect.WithGRPC(),
),
}
m.plugins[name] = mp
if cfg.IsManaged() {
if err := m.startManagedPlugin(mp); err != nil {
log.Error("Failed to start managed plugin %s: %v", name, err)
mp.status = PluginStatusError
continue
}
}
// Try to initialize the plugin
if err := m.initializePlugin(mp); err != nil {
log.Error("Failed to initialize external plugin %s: %v", name, err)
mp.status = PluginStatusError
continue
}
mp.status = PluginStatusOnline
mp.lastSeen = time.Now()
log.Info("External plugin %s is online (managed=%v)", name, cfg.IsManaged())
}
return nil
}
// StopAll gracefully shuts down all external plugins
func (m *ExternalPluginManager) StopAll() {
m.cancel()
m.mu.Lock()
defer m.mu.Unlock()
for name, mp := range m.plugins {
log.Info("Shutting down external plugin: %s", name)
// Send shutdown request via Connect RPC
m.shutdownPlugin(mp)
// Kill managed process
if mp.process != nil {
if err := mp.process.Signal(os.Interrupt); err != nil {
log.Warn("Failed to send interrupt to plugin %s, killing: %v", name, err)
_ = mp.process.Kill()
}
}
mp.status = PluginStatusOffline
}
}
// GetPlugin returns an external plugin by name
func (m *ExternalPluginManager) GetPlugin(name string) *ManagedPlugin {
m.mu.RLock()
defer m.mu.RUnlock()
return m.plugins[name]
}
// AllPlugins returns all external plugins
func (m *ExternalPluginManager) AllPlugins() map[string]*ManagedPlugin {
m.mu.RLock()
defer m.mu.RUnlock()
result := make(map[string]*ManagedPlugin, len(m.plugins))
maps.Copy(result, m.plugins)
return result
}
// OnEvent dispatches an event to all interested plugins (fire-and-forget with timeout)
func (m *ExternalPluginManager) OnEvent(event *pluginv1.PluginEvent) {
m.mu.RLock()
defer m.mu.RUnlock()
for name, mp := range m.plugins {
mp.mu.RLock()
if mp.status != PluginStatusOnline || mp.manifest == nil {
mp.mu.RUnlock()
continue
}
// Check if this plugin is subscribed to this event
subscribed := false
for _, e := range mp.manifest.SubscribedEvents {
if e == event.EventType || e == "*" {
subscribed = true
break
}
}
mp.mu.RUnlock()
if !subscribed {
continue
}
// Dispatch in background with timeout
go func(pluginName string, p *ManagedPlugin) {
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
defer cancel()
resp, err := p.client.OnEvent(ctx, connect.NewRequest(event))
if err != nil {
log.Error("Failed to dispatch event %s to plugin %s: %v", event.EventType, pluginName, err)
return
}
if resp.Msg.Error != "" {
log.Error("Plugin %s returned error for event %s: %s", pluginName, event.EventType, resp.Msg.Error)
}
}(name, mp)
}
}
// HandleHTTP proxies an HTTP request to a plugin that declares the matching route
func (m *ExternalPluginManager) HandleHTTP(method, path string, headers map[string]string, body []byte) (*pluginv1.HTTPResponse, error) {
m.mu.RLock()
defer m.mu.RUnlock()
for name, mp := range m.plugins {
mp.mu.RLock()
if mp.status != PluginStatusOnline || mp.manifest == nil {
mp.mu.RUnlock()
continue
}
for _, route := range mp.manifest.Routes {
if route.Method == method && strings.HasPrefix(path, route.Path) {
mp.mu.RUnlock()
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
defer cancel()
resp, err := mp.client.HandleHTTP(ctx, connect.NewRequest(&pluginv1.HTTPRequest{
Method: method,
Path: path,
Headers: headers,
Body: body,
}))
if err != nil {
return nil, fmt.Errorf("plugin %s HandleHTTP failed: %w", name, err)
}
return resp.Msg, nil
}
}
mp.mu.RUnlock()
}
return nil, fmt.Errorf("no plugin handles %s %s", method, path)
}
// Status returns the status of a plugin
func (mp *ManagedPlugin) Status() PluginStatus {
mp.mu.RLock()
defer mp.mu.RUnlock()
return mp.status
}
// Manifest returns the plugin's manifest
func (mp *ManagedPlugin) Manifest() *pluginv1.PluginManifest {
mp.mu.RLock()
defer mp.mu.RUnlock()
return mp.manifest
}
// SupportsProtocol returns true if the plugin supports the given protocol version.
// Use this before calling RPCs added after protocol version 1.
func (mp *ManagedPlugin) SupportsProtocol(version int32) bool {
mp.mu.RLock()
defer mp.mu.RUnlock()
return mp.protocolVersion >= version
}
// --- Internal methods ---
func (m *ExternalPluginManager) startManagedPlugin(mp *ManagedPlugin) error {
args := strings.Fields(mp.config.Args)
cmd := exec.Command(mp.config.Binary, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start binary %s: %w", mp.config.Binary, err)
}
mp.process = cmd.Process
// Register with graceful manager for proper shutdown
graceful.GetManager().RunAtShutdown(m.ctx, func() {
if mp.process != nil {
_ = mp.process.Signal(os.Interrupt)
}
})
// Wait a bit for the process to start
time.Sleep(2 * time.Second)
return nil
}
func (m *ExternalPluginManager) initializePlugin(mp *ManagedPlugin) error {
resp, err := mp.client.Initialize(m.ctx, connect.NewRequest(&pluginv1.InitializeRequest{
ServerVersion: "3.0.0",
Config: map[string]string{},
ProtocolVersion: ProtocolVersion,
}))
if err != nil {
return fmt.Errorf("plugin Initialize RPC failed: %w", err)
}
if !resp.Msg.Success {
return fmt.Errorf("plugin initialization failed: %s", resp.Msg.Error)
}
mp.mu.Lock()
mp.manifest = resp.Msg.Manifest
mp.protocolVersion = resp.Msg.ProtocolVersion
if mp.protocolVersion == 0 {
mp.protocolVersion = 1 // pre-versioning plugins are treated as v1
}
mp.mu.Unlock()
log.Info("Plugin reports protocol version %d", mp.protocolVersion)
return nil
}
func (m *ExternalPluginManager) shutdownPlugin(mp *ManagedPlugin) {
_, err := mp.client.Shutdown(m.ctx, connect.NewRequest(&pluginv1.ShutdownRequest{
Reason: "server shutdown",
}))
if err != nil {
log.Warn("Plugin shutdown call failed: %v", err)
}
}
// newH2CClient creates an HTTP client that supports cleartext HTTP/2 (h2c)
// for communicating with gRPC services without TLS.
func newH2CClient(timeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
Transport: &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, network, addr)
},
},
}
}

136
modules/plugins/health.go Normal file
View File

@@ -0,0 +1,136 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package plugins
import (
"context"
"maps"
"time"
"connectrpc.com/connect"
"code.gitcaddy.com/server/v3/modules/graceful"
"code.gitcaddy.com/server/v3/modules/log"
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
)
const (
maxConsecutiveFailures = 3
)
// StartHealthMonitoring begins periodic health checks for all external plugins.
// It runs as a background goroutine managed by the graceful manager.
func (m *ExternalPluginManager) StartHealthMonitoring() {
interval := m.config.HealthCheckInterval
if interval <= 0 {
interval = 30 * time.Second
}
graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-m.ctx.Done():
return
case <-ticker.C:
m.checkAllPlugins(ctx)
}
}
})
}
func (m *ExternalPluginManager) checkAllPlugins(ctx context.Context) {
m.mu.RLock()
plugins := make(map[string]*ManagedPlugin, len(m.plugins))
maps.Copy(plugins, m.plugins)
m.mu.RUnlock()
for name, mp := range plugins {
if err := m.checkPlugin(ctx, name, mp); err != nil {
log.Warn("Health check failed for plugin %s: %v", name, err)
}
}
}
func (m *ExternalPluginManager) checkPlugin(ctx context.Context, name string, mp *ManagedPlugin) error {
healthCtx, cancel := context.WithTimeout(ctx, mp.config.HealthTimeout)
defer cancel()
resp, err := mp.client.HealthCheck(healthCtx, connect.NewRequest(&pluginv1.HealthCheckRequest{}))
mp.mu.Lock()
defer mp.mu.Unlock()
if err != nil {
mp.failCount++
if mp.failCount >= maxConsecutiveFailures {
if mp.status != PluginStatusOffline {
log.Error("Plugin %s is now offline after %d consecutive health check failures", name, mp.failCount)
mp.status = PluginStatusOffline
}
// Auto-restart managed plugins
if mp.config.IsManaged() && mp.process != nil {
log.Info("Attempting to restart managed plugin %s", name)
go m.restartManagedPlugin(name, mp)
}
}
return err
}
// Health check succeeded
if mp.status != PluginStatusOnline {
log.Info("Plugin %s is back online", name)
}
mp.failCount = 0
mp.status = PluginStatusOnline
mp.lastSeen = time.Now()
if !resp.Msg.Healthy {
log.Warn("Plugin %s reports unhealthy: %s", name, resp.Msg.Status)
mp.status = PluginStatusError
}
return nil
}
func (m *ExternalPluginManager) restartManagedPlugin(name string, mp *ManagedPlugin) {
// Kill the old process first
if mp.process != nil {
_ = mp.process.Kill()
mp.process = nil
}
mp.mu.Lock()
mp.status = PluginStatusStarting
mp.mu.Unlock()
if err := m.startManagedPlugin(mp); err != nil {
log.Error("Failed to restart managed plugin %s: %v", name, err)
mp.mu.Lock()
mp.status = PluginStatusError
mp.mu.Unlock()
return
}
if err := m.initializePlugin(mp); err != nil {
log.Error("Failed to re-initialize managed plugin %s: %v", name, err)
mp.mu.Lock()
mp.status = PluginStatusError
mp.mu.Unlock()
return
}
mp.mu.Lock()
mp.status = PluginStatusOnline
mp.lastSeen = time.Now()
mp.failCount = 0
mp.mu.Unlock()
log.Info("Managed plugin %s restarted successfully", name)
}

1242
modules/plugins/pluginv1/plugin.pb.go generated Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,106 @@
syntax = "proto3";
package plugin.v1;
option go_package = "code.gitcaddy.com/server/v3/modules/plugins/pluginv1;pluginv1";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
// PluginService is the RPC interface that external plugins must implement.
// The server calls these methods to manage the plugin's lifecycle and dispatch events.
service PluginService {
// Initialize is called when the server starts or the plugin is loaded
rpc Initialize(InitializeRequest) returns (InitializeResponse);
// Shutdown is called when the server is shutting down
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
// HealthCheck checks if the plugin is healthy
rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
// GetManifest returns the plugin's manifest describing its capabilities
rpc GetManifest(GetManifestRequest) returns (PluginManifest);
// OnEvent is called when an event the plugin is subscribed to occurs
rpc OnEvent(PluginEvent) returns (EventResponse);
// HandleHTTP proxies an HTTP request to the plugin
rpc HandleHTTP(HTTPRequest) returns (HTTPResponse);
}
message InitializeRequest {
string server_version = 1;
map<string, string> config = 2;
// protocol_version is the plugin protocol version the server supports.
// The current version is 1. Plugins should check this to know what RPCs
// the server may call. A value of 0 means the server predates versioning.
int32 protocol_version = 3;
}
message InitializeResponse {
bool success = 1;
string error = 2;
PluginManifest manifest = 3;
// protocol_version is the plugin protocol version the plugin supports.
// The current version is 1. The server uses this to avoid calling RPCs
// that the plugin doesn't implement. A value of 0 means the plugin
// predates versioning and is treated as protocol version 1.
int32 protocol_version = 4;
}
message ShutdownRequest {
string reason = 1;
}
message ShutdownResponse {
bool success = 1;
}
message HealthCheckRequest {}
message HealthCheckResponse {
bool healthy = 1;
string status = 2;
map<string, string> details = 3;
}
message GetManifestRequest {}
message PluginManifest {
string name = 1;
string version = 2;
string description = 3;
repeated string subscribed_events = 4;
repeated PluginRoute routes = 5;
repeated string required_permissions = 6;
string license_tier = 7;
}
message PluginRoute {
string method = 1;
string path = 2;
string description = 3;
}
message PluginEvent {
string event_type = 1;
google.protobuf.Struct payload = 2;
google.protobuf.Timestamp timestamp = 3;
int64 repo_id = 4;
int64 org_id = 5;
}
message EventResponse {
bool handled = 1;
string error = 2;
}
message HTTPRequest {
string method = 1;
string path = 2;
map<string, string> headers = 3;
bytes body = 4;
map<string, string> query_params = 5;
}
message HTTPResponse {
int32 status_code = 1;
map<string, string> headers = 2;
bytes body = 3;
}

View File

@@ -0,0 +1,264 @@
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: modules/plugins/pluginv1/plugin.proto
package pluginv1connect
import (
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
connect "connectrpc.com/connect"
context "context"
errors "errors"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// PluginServiceName is the fully-qualified name of the PluginService service.
PluginServiceName = "plugin.v1.PluginService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// PluginServiceInitializeProcedure is the fully-qualified name of the PluginService's Initialize
// RPC.
PluginServiceInitializeProcedure = "/plugin.v1.PluginService/Initialize"
// PluginServiceShutdownProcedure is the fully-qualified name of the PluginService's Shutdown RPC.
PluginServiceShutdownProcedure = "/plugin.v1.PluginService/Shutdown"
// PluginServiceHealthCheckProcedure is the fully-qualified name of the PluginService's HealthCheck
// RPC.
PluginServiceHealthCheckProcedure = "/plugin.v1.PluginService/HealthCheck"
// PluginServiceGetManifestProcedure is the fully-qualified name of the PluginService's GetManifest
// RPC.
PluginServiceGetManifestProcedure = "/plugin.v1.PluginService/GetManifest"
// PluginServiceOnEventProcedure is the fully-qualified name of the PluginService's OnEvent RPC.
PluginServiceOnEventProcedure = "/plugin.v1.PluginService/OnEvent"
// PluginServiceHandleHTTPProcedure is the fully-qualified name of the PluginService's HandleHTTP
// RPC.
PluginServiceHandleHTTPProcedure = "/plugin.v1.PluginService/HandleHTTP"
)
// PluginServiceClient is a client for the plugin.v1.PluginService service.
type PluginServiceClient interface {
// Initialize is called when the server starts or the plugin is loaded
Initialize(context.Context, *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error)
// Shutdown is called when the server is shutting down
Shutdown(context.Context, *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error)
// HealthCheck checks if the plugin is healthy
HealthCheck(context.Context, *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error)
// GetManifest returns the plugin's manifest describing its capabilities
GetManifest(context.Context, *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error)
// OnEvent is called when an event the plugin is subscribed to occurs
OnEvent(context.Context, *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error)
// HandleHTTP proxies an HTTP request to the plugin
HandleHTTP(context.Context, *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error)
}
// NewPluginServiceClient constructs a client for the plugin.v1.PluginService service. By default,
// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and
// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC()
// or connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewPluginServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PluginServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
pluginServiceMethods := pluginv1.File_modules_plugins_pluginv1_plugin_proto.Services().ByName("PluginService").Methods()
return &pluginServiceClient{
initialize: connect.NewClient[pluginv1.InitializeRequest, pluginv1.InitializeResponse](
httpClient,
baseURL+PluginServiceInitializeProcedure,
connect.WithSchema(pluginServiceMethods.ByName("Initialize")),
connect.WithClientOptions(opts...),
),
shutdown: connect.NewClient[pluginv1.ShutdownRequest, pluginv1.ShutdownResponse](
httpClient,
baseURL+PluginServiceShutdownProcedure,
connect.WithSchema(pluginServiceMethods.ByName("Shutdown")),
connect.WithClientOptions(opts...),
),
healthCheck: connect.NewClient[pluginv1.HealthCheckRequest, pluginv1.HealthCheckResponse](
httpClient,
baseURL+PluginServiceHealthCheckProcedure,
connect.WithSchema(pluginServiceMethods.ByName("HealthCheck")),
connect.WithClientOptions(opts...),
),
getManifest: connect.NewClient[pluginv1.GetManifestRequest, pluginv1.PluginManifest](
httpClient,
baseURL+PluginServiceGetManifestProcedure,
connect.WithSchema(pluginServiceMethods.ByName("GetManifest")),
connect.WithClientOptions(opts...),
),
onEvent: connect.NewClient[pluginv1.PluginEvent, pluginv1.EventResponse](
httpClient,
baseURL+PluginServiceOnEventProcedure,
connect.WithSchema(pluginServiceMethods.ByName("OnEvent")),
connect.WithClientOptions(opts...),
),
handleHTTP: connect.NewClient[pluginv1.HTTPRequest, pluginv1.HTTPResponse](
httpClient,
baseURL+PluginServiceHandleHTTPProcedure,
connect.WithSchema(pluginServiceMethods.ByName("HandleHTTP")),
connect.WithClientOptions(opts...),
),
}
}
// pluginServiceClient implements PluginServiceClient.
type pluginServiceClient struct {
initialize *connect.Client[pluginv1.InitializeRequest, pluginv1.InitializeResponse]
shutdown *connect.Client[pluginv1.ShutdownRequest, pluginv1.ShutdownResponse]
healthCheck *connect.Client[pluginv1.HealthCheckRequest, pluginv1.HealthCheckResponse]
getManifest *connect.Client[pluginv1.GetManifestRequest, pluginv1.PluginManifest]
onEvent *connect.Client[pluginv1.PluginEvent, pluginv1.EventResponse]
handleHTTP *connect.Client[pluginv1.HTTPRequest, pluginv1.HTTPResponse]
}
// Initialize calls plugin.v1.PluginService.Initialize.
func (c *pluginServiceClient) Initialize(ctx context.Context, req *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error) {
return c.initialize.CallUnary(ctx, req)
}
// Shutdown calls plugin.v1.PluginService.Shutdown.
func (c *pluginServiceClient) Shutdown(ctx context.Context, req *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error) {
return c.shutdown.CallUnary(ctx, req)
}
// HealthCheck calls plugin.v1.PluginService.HealthCheck.
func (c *pluginServiceClient) HealthCheck(ctx context.Context, req *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error) {
return c.healthCheck.CallUnary(ctx, req)
}
// GetManifest calls plugin.v1.PluginService.GetManifest.
func (c *pluginServiceClient) GetManifest(ctx context.Context, req *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error) {
return c.getManifest.CallUnary(ctx, req)
}
// OnEvent calls plugin.v1.PluginService.OnEvent.
func (c *pluginServiceClient) OnEvent(ctx context.Context, req *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error) {
return c.onEvent.CallUnary(ctx, req)
}
// HandleHTTP calls plugin.v1.PluginService.HandleHTTP.
func (c *pluginServiceClient) HandleHTTP(ctx context.Context, req *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error) {
return c.handleHTTP.CallUnary(ctx, req)
}
// PluginServiceHandler is an implementation of the plugin.v1.PluginService service.
type PluginServiceHandler interface {
// Initialize is called when the server starts or the plugin is loaded
Initialize(context.Context, *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error)
// Shutdown is called when the server is shutting down
Shutdown(context.Context, *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error)
// HealthCheck checks if the plugin is healthy
HealthCheck(context.Context, *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error)
// GetManifest returns the plugin's manifest describing its capabilities
GetManifest(context.Context, *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error)
// OnEvent is called when an event the plugin is subscribed to occurs
OnEvent(context.Context, *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error)
// HandleHTTP proxies an HTTP request to the plugin
HandleHTTP(context.Context, *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error)
}
// NewPluginServiceHandler builds an HTTP handler from the service implementation. It returns the
// path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewPluginServiceHandler(svc PluginServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
pluginServiceMethods := pluginv1.File_modules_plugins_pluginv1_plugin_proto.Services().ByName("PluginService").Methods()
pluginServiceInitializeHandler := connect.NewUnaryHandler(
PluginServiceInitializeProcedure,
svc.Initialize,
connect.WithSchema(pluginServiceMethods.ByName("Initialize")),
connect.WithHandlerOptions(opts...),
)
pluginServiceShutdownHandler := connect.NewUnaryHandler(
PluginServiceShutdownProcedure,
svc.Shutdown,
connect.WithSchema(pluginServiceMethods.ByName("Shutdown")),
connect.WithHandlerOptions(opts...),
)
pluginServiceHealthCheckHandler := connect.NewUnaryHandler(
PluginServiceHealthCheckProcedure,
svc.HealthCheck,
connect.WithSchema(pluginServiceMethods.ByName("HealthCheck")),
connect.WithHandlerOptions(opts...),
)
pluginServiceGetManifestHandler := connect.NewUnaryHandler(
PluginServiceGetManifestProcedure,
svc.GetManifest,
connect.WithSchema(pluginServiceMethods.ByName("GetManifest")),
connect.WithHandlerOptions(opts...),
)
pluginServiceOnEventHandler := connect.NewUnaryHandler(
PluginServiceOnEventProcedure,
svc.OnEvent,
connect.WithSchema(pluginServiceMethods.ByName("OnEvent")),
connect.WithHandlerOptions(opts...),
)
pluginServiceHandleHTTPHandler := connect.NewUnaryHandler(
PluginServiceHandleHTTPProcedure,
svc.HandleHTTP,
connect.WithSchema(pluginServiceMethods.ByName("HandleHTTP")),
connect.WithHandlerOptions(opts...),
)
return "/plugin.v1.PluginService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case PluginServiceInitializeProcedure:
pluginServiceInitializeHandler.ServeHTTP(w, r)
case PluginServiceShutdownProcedure:
pluginServiceShutdownHandler.ServeHTTP(w, r)
case PluginServiceHealthCheckProcedure:
pluginServiceHealthCheckHandler.ServeHTTP(w, r)
case PluginServiceGetManifestProcedure:
pluginServiceGetManifestHandler.ServeHTTP(w, r)
case PluginServiceOnEventProcedure:
pluginServiceOnEventHandler.ServeHTTP(w, r)
case PluginServiceHandleHTTPProcedure:
pluginServiceHandleHTTPHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedPluginServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedPluginServiceHandler struct{}
func (UnimplementedPluginServiceHandler) Initialize(context.Context, *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.Initialize is not implemented"))
}
func (UnimplementedPluginServiceHandler) Shutdown(context.Context, *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.Shutdown is not implemented"))
}
func (UnimplementedPluginServiceHandler) HealthCheck(context.Context, *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.HealthCheck is not implemented"))
}
func (UnimplementedPluginServiceHandler) GetManifest(context.Context, *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.GetManifest is not implemented"))
}
func (UnimplementedPluginServiceHandler) OnEvent(context.Context, *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.OnEvent is not implemented"))
}
func (UnimplementedPluginServiceHandler) HandleHTTP(context.Context, *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.HandleHTTP is not implemented"))
}

View File

@@ -6,6 +6,8 @@ package secretscan
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"path/filepath"
@@ -14,6 +16,7 @@ import (
"code.gitcaddy.com/server/v3/modules/git"
"code.gitcaddy.com/server/v3/modules/git/gitcmd"
"code.gitcaddy.com/server/v3/modules/json"
"code.gitcaddy.com/server/v3/modules/log"
"code.gitcaddy.com/server/v3/modules/setting"
)
@@ -46,6 +49,60 @@ type Scanner struct {
ignoredFiles []string
}
// IgnoreEntry represents an entry in a .gitsecrets-ignore file
type IgnoreEntry struct {
ContentHash string `json:"contentHash"`
PatternID string `json:"patternId"`
FilePath string `json:"filePath"`
Reason string `json:"reason"`
Confidence float64 `json:"confidence,omitempty"`
AddedAt int64 `json:"addedAt"`
}
// contentHash computes the same hash the GitSecrets addon uses: SHA-256 truncated to 16 hex chars
func contentHash(text string) string {
h := sha256.Sum256([]byte(text))
return hex.EncodeToString(h[:])[:16]
}
// parseIgnoreFile parses a .gitsecrets-ignore file and returns a map keyed by contentHash
func parseIgnoreFile(data string) map[string]IgnoreEntry {
entries := make(map[string]IgnoreEntry)
for line := range strings.SplitSeq(data, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
var entry IgnoreEntry
if err := json.Unmarshal([]byte(line), &entry); err != nil {
continue
}
if entry.ContentHash != "" {
entries[entry.ContentHash] = entry
}
}
return entries
}
// filterIgnored removes detected secrets that match entries in the .gitsecrets-ignore file.
// Matching is done on contentHash + patternId.
func filterIgnored(secrets []DetectedSecret, ignoreEntries map[string]IgnoreEntry) []DetectedSecret {
if len(ignoreEntries) == 0 {
return secrets
}
var filtered []DetectedSecret
for _, s := range secrets {
hash := contentHash(s.MatchedText)
if entry, ok := ignoreEntries[hash]; ok && entry.PatternID == s.PatternID {
log.Debug("Secret scan: Skipping ignored secret (hash=%s, pattern=%s, reason=%s)", hash, s.PatternID, entry.Reason)
continue
}
filtered = append(filtered, s)
}
return filtered
}
// NewScanner creates a new secret scanner
func NewScanner() *Scanner {
return &Scanner{
@@ -292,6 +349,21 @@ func (s *Scanner) ScanCommitRange(ctx context.Context, repo *git.Repository, old
result.Secrets = append(result.Secrets, secrets...)
}
// Load .gitsecrets-ignore from the commit being pushed
commit, err := repo.GetCommit(newCommitID)
if err == nil {
if ignoreContent, err := commit.GetFileContent(".gitsecrets-ignore", 256*1024); err == nil {
ignoreEntries := parseIgnoreFile(ignoreContent)
if len(ignoreEntries) > 0 {
before := len(result.Secrets)
result.Secrets = filterIgnored(result.Secrets, ignoreEntries)
if skipped := before - len(result.Secrets); skipped > 0 {
log.Info("Secret scan: Skipped %d secret(s) via .gitsecrets-ignore in %s", skipped, repo.Path)
}
}
}
}
result.ScanDuration = time.Since(startTime)
// Determine if push should be blocked

View File

@@ -11,31 +11,62 @@ import (
// AI settings for the GitCaddy AI service integration
var AI = struct {
Enabled bool
ServiceURL string
ServiceToken string
Timeout time.Duration
MaxRetries int
Enabled bool
ServiceURL string
ServiceToken string
Timeout time.Duration
MaxRetries int
// Provider/model defaults (fallback when org doesn't configure)
DefaultProvider string
DefaultModel string
// System API keys (used when org/repo doesn't provide their own)
ClaudeAPIKey string
OpenAIAPIKey string
GeminiAPIKey string
// Rate limiting
MaxOperationsPerHour int
MaxTokensPerOperation int
// Feature gates (admin controls what's available)
EnableCodeReview bool
EnableIssueTriage bool
EnableDocGen bool
EnableExplainCode bool
EnableChat bool
MaxFileSizeKB int64
MaxDiffLines int
AllowAutoRespond bool
AllowAutoReview bool
AllowAgentMode bool
// Content limits
MaxFileSizeKB int64
MaxDiffLines int
// Bot user
BotUserName string
}{
Enabled: false,
ServiceURL: "localhost:50051",
ServiceToken: "",
Timeout: 30 * time.Second,
MaxRetries: 3,
EnableCodeReview: true,
EnableIssueTriage: true,
EnableDocGen: true,
EnableExplainCode: true,
EnableChat: true,
MaxFileSizeKB: 500,
MaxDiffLines: 5000,
Enabled: false,
ServiceURL: "localhost:50051",
ServiceToken: "",
Timeout: 30 * time.Second,
MaxRetries: 3,
DefaultProvider: "claude",
DefaultModel: "claude-sonnet-4-20250514",
MaxOperationsPerHour: 100,
MaxTokensPerOperation: 8192,
EnableCodeReview: true,
EnableIssueTriage: true,
EnableDocGen: true,
EnableExplainCode: true,
EnableChat: true,
AllowAutoRespond: true,
AllowAutoReview: true,
AllowAgentMode: false,
MaxFileSizeKB: 500,
MaxDiffLines: 5000,
BotUserName: "gitcaddy-ai",
}
func loadAIFrom(rootCfg ConfigProvider) {
@@ -45,14 +76,46 @@ func loadAIFrom(rootCfg ConfigProvider) {
AI.ServiceToken = sec.Key("SERVICE_TOKEN").MustString("")
AI.Timeout = sec.Key("TIMEOUT").MustDuration(30 * time.Second)
AI.MaxRetries = sec.Key("MAX_RETRIES").MustInt(3)
// Provider/model
AI.DefaultProvider = sec.Key("DEFAULT_PROVIDER").MustString("claude")
AI.DefaultModel = sec.Key("DEFAULT_MODEL").MustString("claude-sonnet-4-20250514")
// Validate provider
switch AI.DefaultProvider {
case "claude", "openai", "gemini":
// valid
default:
log.Error("[ai] DEFAULT_PROVIDER %q is not supported, falling back to claude", AI.DefaultProvider)
AI.DefaultProvider = "claude"
}
// System API keys
AI.ClaudeAPIKey = sec.Key("CLAUDE_API_KEY").MustString("")
AI.OpenAIAPIKey = sec.Key("OPENAI_API_KEY").MustString("")
AI.GeminiAPIKey = sec.Key("GEMINI_API_KEY").MustString("")
// Rate limiting
AI.MaxOperationsPerHour = sec.Key("MAX_OPERATIONS_PER_HOUR").MustInt(100)
AI.MaxTokensPerOperation = sec.Key("MAX_TOKENS_PER_OPERATION").MustInt(8192)
// Feature gates
AI.EnableCodeReview = sec.Key("ENABLE_CODE_REVIEW").MustBool(true)
AI.EnableIssueTriage = sec.Key("ENABLE_ISSUE_TRIAGE").MustBool(true)
AI.EnableDocGen = sec.Key("ENABLE_DOC_GEN").MustBool(true)
AI.EnableExplainCode = sec.Key("ENABLE_EXPLAIN_CODE").MustBool(true)
AI.EnableChat = sec.Key("ENABLE_CHAT").MustBool(true)
AI.AllowAutoRespond = sec.Key("ALLOW_AUTO_RESPOND").MustBool(true)
AI.AllowAutoReview = sec.Key("ALLOW_AUTO_REVIEW").MustBool(true)
AI.AllowAgentMode = sec.Key("ALLOW_AGENT_MODE").MustBool(false)
// Content limits
AI.MaxFileSizeKB = sec.Key("MAX_FILE_SIZE_KB").MustInt64(500)
AI.MaxDiffLines = sec.Key("MAX_DIFF_LINES").MustInt(5000)
// Bot user
AI.BotUserName = sec.Key("BOT_USER_NAME").MustString("gitcaddy-ai")
if AI.Enabled && AI.ServiceURL == "" {
log.Error("AI is enabled but SERVICE_URL is not configured")
AI.Enabled = false

View File

@@ -69,6 +69,9 @@ type ThemeStruct struct {
ExploreOrgDisplayFormat *config.Value[string]
EnableBlogs *config.Value[bool]
BlogsInTopNav *config.Value[bool]
ShowFooterPoweredBy *config.Value[bool]
ShowFooterLicenses *config.Value[bool]
ShowFooterAPI *config.Value[bool]
}
type ConfigStruct struct {
@@ -109,6 +112,9 @@ func initDefaultConfig() {
ExploreOrgDisplayFormat: config.ValueJSON[string]("theme.explore_org_display_format").WithDefault("list"),
EnableBlogs: config.ValueJSON[bool]("theme.enable_blogs").WithDefault(false),
BlogsInTopNav: config.ValueJSON[bool]("theme.blogs_in_top_nav").WithDefault(false),
ShowFooterPoweredBy: config.ValueJSON[bool]("theme.show_footer_powered_by").WithFileConfig(config.CfgSecKey{Sec: "other", Key: "SHOW_FOOTER_POWERED_BY"}).WithDefault(true),
ShowFooterLicenses: config.ValueJSON[bool]("theme.show_footer_licenses").WithDefault(true),
ShowFooterAPI: config.ValueJSON[bool]("theme.show_footer_api").WithDefault(true),
},
}
}

View File

@@ -132,6 +132,7 @@ type CardData struct {
LanguageColor string // hex color like "#3572A5"
RepoAvatarURL string
RepoFullName string
BrandName string // replaces the GitCaddy logo when set (from repo Owner Display Name setting)
SolidColor string // hex color for "solid" style
BgImage image.Image // pre-loaded background image for "image" style
UnsplashAuthor string // attribution text for Unsplash images
@@ -264,12 +265,8 @@ func (r *Renderer) renderPresetCard(data CardData, theme Theme) ([]byte, error)
}
drawText(img, xCursor, metaY, data.RepoFullName, faces.meta, theme.SubtextColor)
// GitCaddy logo (bottom right)
logoBounds := r.logo.Bounds()
logoX := CardWidth - rightPad - logoBounds.Dx()
logoY := CardHeight - 40 - logoBounds.Dy()
draw.Draw(img, image.Rect(logoX, logoY, logoX+logoBounds.Dx(), logoY+logoBounds.Dy()),
r.logo, logoBounds.Min, draw.Over)
// Brand name or GitCaddy logo (bottom right)
r.drawBrandOrLogo(img, data.BrandName, faces.meta, theme.SubtextColor, CardWidth-rightPad, CardHeight-40)
return encodePNG(img)
}
@@ -353,12 +350,8 @@ func (r *Renderer) renderSolidCard(data CardData) ([]byte, error) {
repoW := measureText(data.RepoFullName, faces.meta)
drawText(img, (w-repoW)/2, metaY, data.RepoFullName, faces.meta, theme.SubtextColor)
// GitCaddy logo (bottom right)
logoBounds := r.logo.Bounds()
logoX := w - pad - logoBounds.Dx()
logoY := h - 40 - logoBounds.Dy()
draw.Draw(img, image.Rect(logoX, logoY, logoX+logoBounds.Dx(), logoY+logoBounds.Dy()),
r.logo, logoBounds.Min, draw.Over)
// Brand name or GitCaddy logo (bottom right)
r.drawBrandOrLogo(img, data.BrandName, faces.meta, theme.SubtextColor, w-pad, h-40)
// Unsplash attribution (lower-left of image area)
if data.UnsplashAuthor != "" {
@@ -458,16 +451,28 @@ func (r *Renderer) renderImageCard(data CardData) ([]byte, error) {
yBottom -= 60
}
// GitCaddy logo (bottom right)
logoBounds := r.logo.Bounds()
logoX := w - pad - logoBounds.Dx()
logoY := h - pad + 10 - logoBounds.Dy()
draw.Draw(img, image.Rect(logoX, logoY, logoX+logoBounds.Dx(), logoY+logoBounds.Dy()),
r.logo, logoBounds.Min, draw.Over)
// Brand name or GitCaddy logo (bottom right)
subtextC := color.RGBA{R: 170, G: 170, B: 170, A: 255}
r.drawBrandOrLogo(img, data.BrandName, faces.meta, subtextC, w-pad, h-pad+10)
return encodePNG(img)
}
// drawBrandOrLogo draws either the custom brand name text or the default GitCaddy logo
// at the bottom-right of the card. rightX is the right edge, baselineY is the text baseline.
func (r *Renderer) drawBrandOrLogo(img *image.RGBA, brandName string, face font.Face, textColor color.RGBA, rightX, baselineY int) {
if brandName != "" {
textW := measureText(brandName, face)
drawText(img, rightX-textW, baselineY, brandName, face, textColor)
} else {
logoBounds := r.logo.Bounds()
logoX := rightX - logoBounds.Dx()
logoY := baselineY - logoBounds.Dy()
draw.Draw(img, image.Rect(logoX, logoY, logoX+logoBounds.Dx(), logoY+logoBounds.Dy()),
r.logo, logoBounds.Min, draw.Over)
}
}
// fontFaces holds pre-created font faces for a single render.
type fontFaces struct {
title font.Face

View File

@@ -191,3 +191,25 @@ type OrgProfileContent struct {
Readme string `json:"readme,omitempty"`
HasCSS bool `json:"has_css"`
}
// OrgRecentActivity represents a recently updated repo in the org overview
type OrgRecentActivity struct {
RepoName string `json:"repo_name"`
RepoFullName string `json:"repo_full_name"`
DefaultBranch string `json:"default_branch"`
CommitMessage string `json:"commit_message,omitempty"`
CommitTime int64 `json:"commit_time,omitempty"`
IsPrivate bool `json:"is_private"`
}
// OrgOverviewV2 represents the enhanced organization overview for v2 API
type OrgOverviewV2 struct {
Organization *Organization `json:"organization"`
PinnedRepos []*OrgPinnedRepo `json:"pinned_repos"`
PinnedGroups []*OrgPinnedGroup `json:"pinned_groups"`
PublicMembers []*OrgPublicMember `json:"public_members"`
TotalMembers int64 `json:"total_members"`
Stats *OrgOverviewStats `json:"stats"`
Profile *OrgProfileContent `json:"profile,omitempty"`
RecentActivity []OrgRecentActivity `json:"recent_activity,omitempty"`
}

View File

@@ -205,3 +205,45 @@ type ActionRunnersResponse struct {
Entries []*ActionRunner `json:"runners"`
TotalCount int64 `json:"total_count"`
}
// ActionWorkflowStatus represents the latest run status for a single workflow
type ActionWorkflowStatus struct {
WorkflowID string `json:"workflow_id"`
WorkflowName string `json:"workflow_name"`
Status string `json:"status"`
Conclusion string `json:"conclusion,omitempty"`
RunID int64 `json:"run_id"`
RunNumber int64 `json:"run_number"`
Event string `json:"event"`
HeadBranch string `json:"head_branch,omitempty"`
HTMLURL string `json:"html_url"`
// swagger:strfmt date-time
StartedAt time.Time `json:"started_at"`
// swagger:strfmt date-time
CompletedAt time.Time `json:"completed_at"`
}
// ActionWorkflowStatusResponse returns the latest run status per workflow
type ActionWorkflowStatusResponse struct {
Workflows []*ActionWorkflowStatus `json:"workflows"`
}
// ActionJobFailureDetail represents a failed job with its log excerpt
type ActionJobFailureDetail struct {
JobID int64 `json:"job_id"`
JobName string `json:"job_name"`
Status string `json:"status"`
Conclusion string `json:"conclusion,omitempty"`
FailedSteps []string `json:"failed_steps"`
Log string `json:"log"`
}
// ActionRunFailureLog returns a structured failure summary for a workflow run
type ActionRunFailureLog struct {
RunID int64 `json:"run_id"`
Status string `json:"status"`
Conclusion string `json:"conclusion,omitempty"`
WorkflowID string `json:"workflow_id"`
WorkflowYAML string `json:"workflow_yaml,omitempty"`
FailedJobs []*ActionJobFailureDetail `json:"failed_jobs"`
}

123
modules/structs/repo_ai.go Normal file
View File

@@ -0,0 +1,123 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
import "time"
// AISettingsV2 represents the AI settings for a repository
type AISettingsV2 struct {
// Tier 1: Light AI operations
AutoRespondToIssues bool `json:"auto_respond_issues"`
AutoReviewPRs bool `json:"auto_review_prs"`
AutoInspectWorkflows bool `json:"auto_inspect_workflows"`
AutoTriageIssues bool `json:"auto_triage_issues"`
// Tier 2: Advanced agent operations
AgentModeEnabled bool `json:"agent_mode_enabled"`
AgentTriggerLabels []string `json:"agent_trigger_labels"`
AgentMaxRunMinutes int `json:"agent_max_run_minutes"`
// Escalation
EscalateToStaff bool `json:"escalate_to_staff"`
EscalationLabel string `json:"escalation_label"`
EscalationAssignTeam string `json:"escalation_assign_team"`
// Provider overrides (empty = inherit from org → system)
PreferredProvider string `json:"preferred_provider"`
PreferredModel string `json:"preferred_model"`
// Custom instructions
SystemInstructions string `json:"system_instructions"`
ReviewInstructions string `json:"review_instructions"`
IssueInstructions string `json:"issue_instructions"`
// Resolved values (read-only, computed from cascade)
ResolvedProvider string `json:"resolved_provider,omitempty"`
ResolvedModel string `json:"resolved_model,omitempty"`
}
// UpdateAISettingsOption represents the options for updating AI settings
type UpdateAISettingsOption struct {
AutoRespondToIssues *bool `json:"auto_respond_issues"`
AutoReviewPRs *bool `json:"auto_review_prs"`
AutoInspectWorkflows *bool `json:"auto_inspect_workflows"`
AutoTriageIssues *bool `json:"auto_triage_issues"`
AgentModeEnabled *bool `json:"agent_mode_enabled"`
AgentTriggerLabels []string `json:"agent_trigger_labels"`
AgentMaxRunMinutes *int `json:"agent_max_run_minutes"`
EscalateToStaff *bool `json:"escalate_to_staff"`
EscalationLabel *string `json:"escalation_label"`
EscalationAssignTeam *string `json:"escalation_assign_team"`
PreferredProvider *string `json:"preferred_provider"`
PreferredModel *string `json:"preferred_model"`
SystemInstructions *string `json:"system_instructions"`
ReviewInstructions *string `json:"review_instructions"`
IssueInstructions *string `json:"issue_instructions"`
}
// OrgAISettingsV2 represents the AI settings for an organization
type OrgAISettingsV2 struct {
Provider string `json:"provider"`
Model string `json:"model"`
HasAPIKey bool `json:"has_api_key"` // never expose actual key
MaxOpsPerHour int `json:"max_ops_per_hour"`
AllowedOps string `json:"allowed_ops"`
AgentModeAllowed bool `json:"agent_mode_allowed"`
}
// UpdateOrgAISettingsOption represents the options for updating org AI settings
type UpdateOrgAISettingsOption struct {
Provider *string `json:"provider"`
Model *string `json:"model"`
APIKey *string `json:"api_key"`
MaxOpsPerHour *int `json:"max_ops_per_hour"`
AllowedOps *string `json:"allowed_ops"`
AgentModeAllowed *bool `json:"agent_mode_allowed"`
}
// AIOperationV2 represents an AI operation log entry
type AIOperationV2 struct {
ID int64 `json:"id"`
RepoID int64 `json:"repo_id"`
Operation string `json:"operation"`
Tier int `json:"tier"`
TriggerEvent string `json:"trigger_event"`
TriggerUserID int64 `json:"trigger_user_id"`
TargetID int64 `json:"target_id"`
TargetType string `json:"target_type"`
Provider string `json:"provider"`
Model string `json:"model"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
Status string `json:"status"`
ResultCommentID int64 `json:"result_comment_id,omitempty"`
ActionRunID int64 `json:"action_run_id,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
DurationMs int64 `json:"duration_ms"`
CreatedAt time.Time `json:"created_at"`
}
// AIOperationListV2 represents a paginated list of AI operations
type AIOperationListV2 struct {
Operations []*AIOperationV2 `json:"operations"`
TotalCount int64 `json:"total_count"`
}
// AIExplainRequest represents a request to explain code
type AIExplainRequest struct {
FilePath string `json:"file_path" binding:"Required"`
StartLine int `json:"start_line"`
EndLine int `json:"end_line"`
Question string `json:"question"`
}
// AIServiceStatusV2 represents the AI service health status
type AIServiceStatusV2 struct {
Enabled bool `json:"enabled"`
Healthy bool `json:"healthy"`
ServiceURL string `json:"service_url"`
Version string `json:"version,omitempty"`
ProviderStatus map[string]string `json:"provider_status,omitempty"`
TotalOpsToday int64 `json:"total_ops_today"`
}

View File

@@ -47,3 +47,231 @@ type PagesInfo struct {
Config *PagesConfig `json:"config"`
Domains []*PagesDomain `json:"domains,omitempty"`
}
// ---------------------------------------------------------------------------
// v2 Landing Page Configuration API structs
// ---------------------------------------------------------------------------
// UpdatePagesConfigOption represents the full landing page config update.
// For PATCH, only non-nil fields are applied. For PUT, all fields replace existing config.
type UpdatePagesConfigOption struct {
Enabled *bool `json:"enabled"`
PublicLanding *bool `json:"public_landing"`
Template *string `json:"template"`
Brand *UpdatePagesBrandOption `json:"brand"`
Hero *UpdatePagesHeroOption `json:"hero"`
Stats *[]PagesStatOption `json:"stats"`
ValueProps *[]PagesValuePropOption `json:"value_props"`
Features *[]PagesFeatureOption `json:"features"`
SocialProof *UpdatePagesSocialOption `json:"social_proof"`
Pricing *UpdatePagesPricingOption `json:"pricing"`
CTASection *UpdatePagesCTAOption `json:"cta_section"`
Blog *UpdatePagesBlogOption `json:"blog"`
Gallery *UpdatePagesGalleryOption `json:"gallery"`
Comparison *UpdatePagesComparisonOption `json:"comparison"`
Navigation *UpdatePagesNavOption `json:"navigation"`
Footer *UpdatePagesFooterOption `json:"footer"`
Theme *UpdatePagesThemeOption `json:"theme"`
SEO *UpdatePagesSEOOption `json:"seo"`
Advanced *UpdatePagesAdvancedOption `json:"advanced"`
}
// UpdatePagesBrandOption represents brand section update
type UpdatePagesBrandOption struct {
Name *string `json:"name"`
LogoURL *string `json:"logo_url"`
Tagline *string `json:"tagline"`
FaviconURL *string `json:"favicon_url"`
}
// UpdatePagesHeroOption represents hero section update
type UpdatePagesHeroOption struct {
Headline *string `json:"headline"`
Subheadline *string `json:"subheadline"`
ImageURL *string `json:"image_url"`
VideoURL *string `json:"video_url"`
CodeExample *string `json:"code_example"`
PrimaryCTA *PagesCTAButtonOption `json:"primary_cta"`
SecondaryCTA *PagesCTAButtonOption `json:"secondary_cta"`
}
// PagesCTAButtonOption represents a CTA button
type PagesCTAButtonOption struct {
Label *string `json:"label"`
URL *string `json:"url"`
Variant *string `json:"variant"`
}
// PagesStatOption represents a stat item
type PagesStatOption struct {
Value string `json:"value"`
Label string `json:"label"`
}
// PagesValuePropOption represents a value proposition item
type PagesValuePropOption struct {
Title string `json:"title"`
Description string `json:"description"`
Icon string `json:"icon"`
}
// PagesFeatureOption represents a feature item
type PagesFeatureOption struct {
Title string `json:"title"`
Description string `json:"description"`
Icon string `json:"icon"`
ImageURL string `json:"image_url"`
}
// UpdatePagesSocialOption represents social proof section update
type UpdatePagesSocialOption struct {
Logos *[]string `json:"logos"`
Testimonials *[]PagesTestimonialOption `json:"testimonials"`
}
// PagesTestimonialOption represents a testimonial item
type PagesTestimonialOption struct {
Quote string `json:"quote"`
Author string `json:"author"`
Role string `json:"role"`
Avatar string `json:"avatar"`
}
// UpdatePagesPricingOption represents pricing section update
type UpdatePagesPricingOption struct {
Headline *string `json:"headline"`
Subheadline *string `json:"subheadline"`
Plans *[]PagesPricingPlanOption `json:"plans"`
}
// PagesPricingPlanOption represents a pricing plan item
type PagesPricingPlanOption struct {
Name string `json:"name"`
Price string `json:"price"`
Period string `json:"period"`
Features []string `json:"features"`
CTA string `json:"cta"`
Featured bool `json:"featured"`
}
// UpdatePagesCTAOption represents CTA section update
type UpdatePagesCTAOption struct {
Headline *string `json:"headline"`
Subheadline *string `json:"subheadline"`
Button *PagesCTAButtonOption `json:"button"`
}
// UpdatePagesBlogOption represents blog section update
type UpdatePagesBlogOption struct {
Enabled *bool `json:"enabled"`
Headline *string `json:"headline"`
Subheadline *string `json:"subheadline"`
MaxPosts *int `json:"max_posts"`
}
// UpdatePagesGalleryOption represents gallery section update
type UpdatePagesGalleryOption struct {
Enabled *bool `json:"enabled"`
Headline *string `json:"headline"`
Subheadline *string `json:"subheadline"`
MaxImages *int `json:"max_images"`
Columns *int `json:"columns"`
}
// UpdatePagesComparisonOption represents comparison section update
type UpdatePagesComparisonOption struct {
Enabled *bool `json:"enabled"`
Headline *string `json:"headline"`
Subheadline *string `json:"subheadline"`
Columns *[]PagesComparisonColumnOption `json:"columns"`
Groups *[]PagesComparisonGroupOption `json:"groups"`
}
// PagesComparisonColumnOption represents a comparison column
type PagesComparisonColumnOption struct {
Name string `json:"name"`
Highlight bool `json:"highlight"`
}
// PagesComparisonGroupOption represents a comparison group
type PagesComparisonGroupOption struct {
Name string `json:"name"`
Features []PagesComparisonFeatureOption `json:"features"`
}
// PagesComparisonFeatureOption represents a comparison feature row
type PagesComparisonFeatureOption struct {
Name string `json:"name"`
Values []string `json:"values"`
}
// UpdatePagesNavOption represents navigation section update
type UpdatePagesNavOption struct {
ShowDocs *bool `json:"show_docs"`
ShowAPI *bool `json:"show_api"`
ShowRepository *bool `json:"show_repository"`
ShowReleases *bool `json:"show_releases"`
ShowIssues *bool `json:"show_issues"`
}
// UpdatePagesFooterOption represents footer section update
type UpdatePagesFooterOption struct {
Copyright *string `json:"copyright"`
ShowPoweredBy *bool `json:"show_powered_by"`
Links *[]PagesFooterLinkOption `json:"links"`
Social *[]PagesSocialLinkOption `json:"social"`
CTASection *UpdatePagesCTAOption `json:"cta_section"`
}
// PagesFooterLinkOption represents a footer link
type PagesFooterLinkOption struct {
Label string `json:"label"`
URL string `json:"url"`
}
// PagesSocialLinkOption represents a social link
type PagesSocialLinkOption struct {
Platform string `json:"platform"`
URL string `json:"url"`
}
// UpdatePagesThemeOption represents theme section update
type UpdatePagesThemeOption struct {
PrimaryColor *string `json:"primary_color"`
AccentColor *string `json:"accent_color"`
Mode *string `json:"mode"`
}
// UpdatePagesSEOOption represents SEO section update
type UpdatePagesSEOOption struct {
Title *string `json:"title"`
Description *string `json:"description"`
Keywords *[]string `json:"keywords"`
OGImage *string `json:"og_image"`
UseMediaKitOG *bool `json:"use_media_kit_og"`
TwitterCard *string `json:"twitter_card"`
TwitterSite *string `json:"twitter_site"`
}
// UpdatePagesAdvancedOption represents advanced settings update
type UpdatePagesAdvancedOption struct {
CustomCSS *string `json:"custom_css"`
CustomHead *string `json:"custom_head"`
StaticRoutes *[]string `json:"static_routes"`
PublicReleases *bool `json:"public_releases"`
HideMobileReleases *bool `json:"hide_mobile_releases"`
GooglePlayID *string `json:"google_play_id"`
AppStoreID *string `json:"app_store_id"`
}
// UpdatePagesContentOption bundles content-page sections for PUT /config/content
type UpdatePagesContentOption struct {
Blog *UpdatePagesBlogOption `json:"blog"`
Gallery *UpdatePagesGalleryOption `json:"gallery"`
ComparisonEnabled *bool `json:"comparison_enabled"`
Stats *[]PagesStatOption `json:"stats"`
ValueProps *[]PagesValuePropOption `json:"value_props"`
Features *[]PagesFeatureOption `json:"features"`
Navigation *UpdatePagesNavOption `json:"navigation"`
Advanced *UpdatePagesAdvancedOption `json:"advanced"`
}

View File

@@ -127,9 +127,6 @@ func NewFuncMap() template.FuncMap {
"ShowFooterTemplateLoadTime": func() bool {
return setting.Other.ShowFooterTemplateLoadTime
},
"ShowFooterPoweredBy": func() bool {
return setting.Other.ShowFooterPoweredBy
},
"AllowedReactions": func() []string {
return setting.UI.Reactions
},

View File

@@ -0,0 +1,85 @@
# Community Specification Contribution Policy 1.0
This document provides the contribution policy for specifications and other documents developed using the Community Specification process in a repository (each a “Working Group”). Additional or alternate contribution policies may be adopted and documented by the Working Group.
## 1. Contribution Guidelines.
This Working Group accepts contributions via pull requests. The following section outlines the process for merging contributions to the specification
**1.1. Issues.** Issues are used as the primary method for tracking anything to do with this specification Working Group.
**1.1.1. Issue Types.** There are three types of issues (each with their own corresponding label):
**1.1.1.1. Discussion.** These are support or functionality inquiries that we want to have a record of for future reference. Depending on the discussion, these can turn into "Spec Change" issues.
**1.1.1.2. Proposal.** Used for items that propose a new ideas or functionality that require a larger discussion. This allows for feedback from others before a specification change is actually written. All issues that are proposals should both have a label and an issue title of "Proposal: [the rest of the title]." A proposal can become a "Spec Change" and does not require a milestone.
**1.1.1.3. Spec Change:** These track specific spec changes and ideas until they are complete. They can evolve from "Proposal" and "Discussion" items, or can be submitted individually depending on the size. Each spec change should be placed into a milestone.
## 2. Issue Lifecycle.
The issue lifecycle is mainly driven by the Maintainer. All issue types follow the same general lifecycle. Differences are noted below.
**2.1. Issue Creation.**
**2.2. Triage.**
o The Editor in charge of triaging will apply the proper labels for the issue. This includes labels for priority, type, and metadata.
o (If needed) Clean up the title to succinctly and clearly state the issue. Also ensure that proposals are prefaced with "Proposal".
**2.3. Discussion.**
o "Spec Change" issues should be connected to the pull request that resolves it.
o Whoever is working on a "Spec Change" issue should either assign the issue to themselves or make a comment in the issue saying that they are taking it.
o "Proposal" and "Discussion" issues should stay open until resolved.
**2.4. Issue Closure.**
## 3. How to Contribute a Patch.
The Working Group uses pull requests to track changes. To submit a change to the specification:
**3.1 Fork the Repo, modify the Specification to Address the Issue.**
**3.2. Submit a Pull Request.**
## 4. Pull Request Workflow.
The next section contains more information on the workflow followed for Pull Requests.
**4.1. Pull Request Creation.**
o We welcome pull requests that are currently in progress. They are a great way to keep track of important work that is in-flight, but useful for others to see. If a pull request is a work in progress, it should be prefaced with "WIP: [title]". You should also add the wip label Once the pull request is ready for review, remove "WIP" from the title and label.
o It is preferred, but not required, to have a pull request tied to a specific issue. There can be circumstances where if it is a quick fix then an issue might be overkill. The details provided in the pull request description would suffice in this case.
**4.2. Triage**
o The Editor in charge of triaging will apply the proper labels for the issue. This should include at least a size label, a milestone, and awaiting review once all labels are applied.
**4.3. Reviewing/Discussion.**
o All reviews will be completed using the review tool.
o A "Comment" review should be used when there are questions about the spec that should be answered, but that don't involve spec changes. This type of review does not count as approval.
o A "Changes Requested" review indicates that changes to the spec need to be made before they will be merged.
o Reviewers should update labels as needed (such as needs rebase).
o When a review is approved, the reviewer should add LGTM as a comment.
o Final approval is required by a designated Editor. Merging is blocked without this final approval. Editors will factor reviews from all other reviewers into their approval process.
**4.4. Responsive.** Pull request owner should try to be responsive to comments by answering questions or changing text. Once all comments have been addressed, the pull request is ready to be merged.
**4.5. Merge or Close.**
o A pull request should stay open until a Maintainer has marked the pull request as approved.
o Pull requests can be closed by the author without merging.
o Pull requests may be closed by a Maintainer if the decision is made that it is not going to be merged.

View File

@@ -0,0 +1,51 @@
# Community Specification Governance Policy 1.0
This document provides the governance policy for specifications and other documents developed using the Community Specification process in a repository (each a “Working Group”). Each Working Group and must adhere to the requirements in this document.
## 1. Roles.
Each Working Group may include the following roles. Additional roles may be adopted and documented by the Working Group.
**1.1. Maintainer.** “Maintainers” are responsible for organizing activities around developing, maintaining, and updating the specification(s) developed by the Working Group. Maintainers are also responsible for determining consensus and coordinating appeals. Each Working Group will designate one or more Maintainer for that Working Group. A Working Group may select a new or additional Maintainer(s) upon Approval of the Working Group Participants.
**1.2. Editor.** “Editors” are responsible for ensuring that the contents of the document accurately reflect the decisions that have been made by the group, and that the specification adheres to formatting and content guidelines. Each Working Group will designate an Editor for that Working Group. A Working Group may select a new Editor upon Approval of the Working Group Participants.
**1.3. Participants.** “Participants” are those that have made Contributions to the Working Group subject to the Community Specification License.
## 2. Decision Making.
**2.1. Consensus-Based Decision Making.** Working Groups make decisions through a consensus process (“Approval” or “Approved”). While the agreement of all Participants is preferred, it is not required for consensus. Rather, the Maintainer will determine consensus based on their good faith consideration of a number of factors, including the dominant view of the Working Group Participants and nature of support and objections. The Maintainer will document evidence of consensus in accordance with these requirements.
**2.2. Appeal Process.** Decisions may be appealed be via a pull request or an issue, and that appeal will be considered by the Maintainer in good faith, who will respond in writing within a reasonable time.
## 3. Ways of Working.
Inspired by [ANSIs Essential Requirements for Due Process](https://share.ansi.org/Shared%20Documents/Standards%20Activities/American%20National%20Standards/Procedures,%20Guides,%20and%20Forms/2020_ANSI_Essential_Requirements.pdf), Community Specification Working Groups must adhere to consensus-based due process requirements. These requirements apply to activities related to the development of consensus for approval, revision, reaffirmation, and withdrawal of Community Specifications. Due process means that any person (organization, company, government agency, individual, etc.) with a direct and material interest has a right to participate by: a) expressing a position and its basis, b) having that position considered, and c) having the right to appeal. Due process allows for equity and fair play. The following constitute the minimum acceptable due process requirements for the development of consensus.
**3.1. Openness.** Participation shall be open to all persons who are directly and materially affected by the activity in question. There shall be no undue financial barriers to participation. Voting membership on the consensus body shall not be conditional upon membership in any organization, nor unreasonably restricted on the basis of technical qualifications or other such requirements. Membership in a Working Groups parent organization, if any, may be required.
**3.2. Lack of Dominance.** The development process shall not be dominated by any single interest category, individual or organization. Dominance means a position or exercise of dominant authority, leadership, or influence by reason of superior leverage, strength, or representation to the exclusion of fair and equitable consideration of other viewpoints.
**3.3. Balance.** The development process should have a balance of interests. Participants from diverse interest categories shall be sought with the objective of achieving balance.
**3.4. Coordination and Harmonization.** Good faith efforts shall be made to resolve potential conflicts between and among deliverables developed under this Working Group and existing industry standards.
**3.5. Consideration of Views and Objections.** Prompt consideration shall be given to the written views and objections of all Participants.
**3.6. Written procedures.** This governance document and other materials documenting the Community Specification development process shall be available to any interested person.
## 4. Specification Development Process.
**4.1. Pre-Draft.** Any Participant may submit a proposed initial draft document as a candidate Draft Specification of that Working Group. The Maintainer will designate each submission as a “Pre-Draft” document.
**4.2. Draft.** Each Pre-Draft document of a Working Group must first be Approved to become a” Draft Specification”. Once the Working Group approves a document as a Draft Specification, the Draft Specification becomes the basis for all going forward work on that specification.
**4.3. Working Group Approval.** Once a Working Group believes it has achieved the objectives for its specification as described in the Scope, it will Approve that Draft Specification and progress it to “Approved Specification” status.
**4.4. Publication and Submission.** Upon the designation of a Draft Specification as an Approved Specification, the Maintainer will publish the Approved Specification in a manner agreed upon by the Working Group Participants (i.e., Working Group Participant only location, publicly available location, Working Group maintained website, Working Group member website, etc.). The publication of an Approved Specification in a publicly accessible manner must include the terms under which the Approved Specification is being made available under.
**4.5. Submissions to Standards Bodies.** No Draft Specification or Approved Specification may be submitted to another standards development organization without Working group Approval. Upon reaching Approval, the Maintainer will coordinate the submission of the applicable Draft Specification or Approved Specification to another standards development organization. Working Group Participants that developed that Draft Specification or Approved Specification agree to grant the copyright rights necessary to make those submissions.
## 5. Non-Confidential, Restricted Disclosure.
Information disclosed in connection with any Working Group activity, including but not limited to meetings, Contributions, and submissions, is not confidential, regardless of any markings or statements to the contrary. Notwithstanding the foregoing, if the Working Group is collaborating via a private repository, the Participants will not make any public disclosures of that information contained in that private repository without the Approval of the Working Group.

View File

@@ -0,0 +1,99 @@
# Community Specification License 1.0
**The Purpose of this License.** This License sets forth the terms under which 1) Contributor will participate in and contribute to the development of specifications, standards, best practices, guidelines, and other similar materials under this Working Group, and 2) how the materials developed under this License may be used. It is not intended for source code. Capitalized terms are defined in the Licenses last section.
**1. Copyright.**
**1.1. Copyright License.** Contributor grants everyone a non-sublicensable, perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as expressly stated in this License) copyright license, without any obligation for accounting, to reproduce, prepare derivative works of, publicly display, publicly perform, and distribute any materials it submits to the full extent of its copyright interest in those materials. Contributor also acknowledges that the Working Group may exercise copyright rights in the Specification, including the rights to submit the Specification to another standards organization.
**1.2. Copyright Attribution.** As a condition, anyone exercising this copyright license must include attribution to the Working Group in any derivative work based on materials developed by the Working Group. That attribution must include, at minimum, the materials name, version number, and source from where the materials were retrieved. Attribution is not required for implementations of the Specification.
**2. Patents.**
**2.1. Patent License.**
**2.1.1. As a Result of Contributions.**
**2.1.1.1. As a Result of Contributions to Draft Specifications.** Contributor grants Licensee a non-sublicensable, perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as expressly stated in this License) license to its Necessary Claims in 1) Contributors Contributions and 2) to the Draft Specification that is within Scope as of the date of that Contribution, in both cases for Licensees Implementation of the Draft Specification, except for those patent claims excluded by Contributor under Section 3.
**2.1.1.2. For Approved Specifications.** Contributor grants Licensee a non-sublicensable, perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as expressly stated in this License) license to its Necessary Claims included in the Approved Specification that are within Scope for Licensees Implementation of the Approved Specification, except for those patent claims excluded by Contributor under Section 3.
**2.1.2. Patent Grant from Licensee.** Licensee grants each other Licensee a non-sublicensable, perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as expressly stated in this License) license to its Necessary Claims for its Implementation, except for those patent claims excluded under Section 3.
**2.1.3. Licensee Acceptance.** The patent grants set forth in Section 2.1 extend only to Licensees that have indicated their agreement to this License as follows:
**2.1.3.1. Source Code Distributions.** For distribution in source code, by including this License in the root directory of the source code with the Implementation;
**2.1.3.2. Non-Source Code Distributions.** For distribution in any form other than source code, by including this License in the documentation, legal notices, via notice in the software, and/or other written materials provided with the Implementation; or
**2.1.3.3. Via Notices.md.** By issuing pull request or commit to the Specifications repositorys Notices.md file by the Implementers authorized representative, including the Implementers name, authorized individual and system identifier, and Specification version.
**2.1.4. Defensive Termination.** If any Licensee files or maintains a claim in a court asserting that a Necessary Claim is infringed by an Implementation, any licenses granted under this License to the Licensee are immediately terminated unless 1) that claim is directly in response to a claim against Licensee regarding an Implementation, or 2) that claim was brought to enforce the terms of this License, including intervention in a third-party action by a Licensee.
**2.1.5. Additional Conditions.** This License is not an assurance (i) that any of Contributors copyrights or issued patent claims cover an Implementation of the Specification or are enforceable or (ii) that an Implementation of the Specification would not infringe intellectual property rights of any third party.
**2.2. Patent Licensing Commitment.** In addition to the rights granted in Section 2.1, Contributor agrees to grant everyone a no charge, royalty-free license on reasonable and non-discriminatory terms to Contributors Necessary Claims that are within Scope for:
1) Implementations of a Draft Specification, where such license applies only to those Necessary Claims infringed by implementing Contributor's Contribution(s) included in that Draft Specification, and
2) Implementations of the Approved Specification.
This patent licensing commitment does not apply to those claims subject to Contributors Exclusion Notice under Section 3.
**2.3. Effect of Withdrawal.** Contributor may withdraw from the Working Group by issuing a pull request or commit providing notice of withdrawal to the Working Group repositorys Notices.md file. All of Contributors existing commitments and obligations with respect to the Working Group up to the date of that withdrawal notice will remain in effect, but no new obligations will be incurred.
**2.4. Binding Encumbrance.** This License is binding on any future owner, assignee, or party who has been given the right to enforce any Necessary Claims against third parties.
**3. Patent Exclusion.**
**3.1. As a Result of Contributions.** Contributor may exclude Necessary Claims from its licensing commitments incurred under Section 2.1.1 by issuing an Exclusion Notice within 45 days of the date of that Contribution. Contributor may not issue an Exclusion Notice for any material that has been included in a Draft Deliverable for more than 45 days prior to the date of that Contribution.
**3.2. As a Result of a Draft Specification Becoming an Approved Specification.** Prior to the adoption of a Draft Specification as an Approved Specification, Contributor may exclude Necessary Claims from its licensing commitments under this Agreement by issuing an Exclusion Notice. Contributor may not issue an Exclusion Notice for patents that were eligible to have been excluded pursuant to Section 3.1.
**4. Source Code License.** Any source code developed by the Working Group is solely subject the source code license included in the Working Groups repository for that code. If no source code license is included, the source code will be subject to the MIT License.
**5. No Other Rights.** Except as specifically set forth in this License, no other express or implied patent, trademark, copyright, or other rights are granted under this License, including by implication, waiver, or estoppel.
**6. Antitrust Compliance.** Contributor acknowledge that it may compete with other participants in various lines of business and that it is therefore imperative that they and their respective representatives act in a manner that does not violate any applicable antitrust laws and regulations. This License does not restrict any Contributor from engaging in similar specification development projects. Each Contributor may design, develop, manufacture, acquire or market competitive deliverables, products, and services, and conduct its business, in whatever way it chooses. No Contributor is obligated to announce or market any products or services. Without limiting the generality of the foregoing, the Contributors agree not to have any discussion relating to any product pricing, methods or channels of product distribution, division of markets, allocation of customers or any other topic that should not be discussed among competitors under the auspices of the Working Group.
**7. Non-Circumvention.** Contributor agrees that it will not intentionally take or willfully assist any third party to take any action for the purpose of circumventing any obligations under this License.
**8. Representations, Warranties and Disclaimers.**
**8.1. Representations, Warranties and Disclaimers.** Contributor and Licensee represents and warrants that 1) it is legally entitled to grant the rights set forth in this License and 2) it will not intentionally include any third party materials in any Contribution unless those materials are available under terms that do not conflict with this License. IN ALL OTHER RESPECTS ITS CONTRIBUTIONS ARE PROVIDED "AS IS." The entire risk as to implementing or otherwise using the Contribution or the Specification is assumed by the implementer and user. Except as stated herein, CONTRIBUTOR AND LICENSEE EXPRESSLY DISCLAIM ANY WARRANTIES (EXPRESS, IMPLIED, OR OTHERWISE), INCLUDING IMPLIED WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, FITNESS FOR A PARTICULAR PURPOSE, CONDITIONS OF QUALITY, OR TITLE, RELATED TO THE CONTRIBUTION OR THE SPECIFICATION. IN NO EVENT WILL ANY PARTY BE LIABLE TO ANY OTHER PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF ACTION OF ANY KIND WITH RESPECT TO THIS AGREEMENT, WHETHER BASED ON BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND WHETHER OR NOT THE OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Any obligations regarding the transfer, successors in interest, or assignment of Necessary Claims will be satisfied if Contributor or Licensee notifies the transferee or assignee of any patent that it knows contains Necessary Claims or necessary claims under this License. Nothing in this License requires Contributor to undertake a patent search. If Contributor is 1) employed by or acting on behalf of an employer, 2) is making a Contribution under the direction or control of a third party, or 3) is making the Contribution as a consultant, contractor, or under another similar relationship with a third party, Contributor represents that they have been authorized by that party to enter into this License on its behalf.
**8.2. Distribution Disclaimer.** Any distributions of technical information to third parties must include a notice materially similar to the following: “THESE MATERIALS ARE PROVIDED “AS IS.” The Contributors and Licensees expressly disclaim any warranties (express, implied, or otherwise), including implied warranties of merchantability, non-infringement, fitness for a particular purpose, or title, related to the materials. The entire risk as to implementing or otherwise using the materials is assumed by the implementer and user. IN NO EVENT WILL THE CONTRIBUTORS OR LICENSEES BE LIABLE TO ANY OTHER PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF ACTION OF ANY KIND WITH RESPECT TO THIS DELIVERABLE OR ITS GOVERNING AGREEMENT, WHETHER BASED ON BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND WHETHER OR NOT THE OTHER MEMBER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.”
**9. Definitions.**
**9.1. Affiliate.** “Affiliate” means an entity that directly or indirectly Controls, is Controlled by, or is under common Control of that party.
**9.2. Approved Specification.** “Approved Specification” means the final version and contents of any Draft Specification designated as an Approved Specification as set forth in the accompanying Governance.md file.
**9.3. Contribution.** “Contribution” means any original work of authorship, including any modifications or additions to an existing work, that Contributor submits for inclusion in a Draft Specification, which is included in a Draft Specification or Approved Specification.
**9.4. Contributor.** “Contributor” means any person or entity that has indicated its acceptance of the License 1) by making a Contribution to the Specification, or 2) by entering into the Community Specification Contributor License Agreement for the Specification. Contributor includes its Affiliates, assigns, agents, and successors in interest.
**9.5. Control.** “Control” means direct or indirect control of more than 50% of the voting power to elect directors of that corporation, or for any other entity, the power to direct management of such entity.
**9.6. Draft Specification.** “Draft Specification” means all versions of the material (except an Approved Specification) developed by this Working Group for the purpose of creating, commenting on, revising, updating, modifying, or adding to any document that is to be considered for inclusion in the Approved Specification.
**9.7. Exclusion Notice.** “Exclusion Notice” means a written notice made by making a pull request or commit to the repositorys Notices.md file that identifies patents that Contributor is excluding from its patent licensing commitments under this License. The Exclusion Notice for issued patents and published applications must include the Draft Specifications name, patent number(s) or title and application number(s), as the case may be, for each of the issued patent(s) or pending patent application(s) that the Contributor is excluding from the royalty-free licensing commitment set forth in this License. If an issued patent or pending patent application that may contain Necessary Claims is not set forth in the Exclusion Notice, those Necessary Claims shall continue to be subject to the licensing commitments under this License. The Exclusion Notice for unpublished patent applications must provide either: (i) the text of the filed application; or (ii) identification of the specific part(s) of the Draft Specification whose implementation makes the excluded claim a Necessary Claim. If (ii) is chosen, the effect of the exclusion will be limited to the identified part(s) of the Draft Specification.
**9.8. Implementation.** “Implementation” means making, using, selling, offering for sale, importing or distributing any implementation of the Specification 1) only to the extent it implements the Specification and 2) so long as all required portions of the Specification are implemented.
**9.9. License.** “License” means this Community Specification License.
**9.10. Licensee.** “Licensee” means any person or entity that has indicated its acceptance of the License as set forth in Section 2.1.3. Licensee includes its Affiliates, assigns, agents, and successors in interest.
**9.11. Necessary Claims.** “Necessary Claims” are those patent claims, if any, that a party owns or controls, including those claims later acquired, that are necessary to implement the required portions (including the required elements of optional portions) of the Specification that are described in detail and not merely referenced in the Specification.
**9.12. Specification.** “Specification” means a Draft Specification or Approved Specification included in the Working Groups repository subject to this License, and the version of the Specification implemented by the Licensee.
**9.13. Scope.** “Scope” has the meaning as set forth in the accompanying Scope.md file included in this Specifications repository. Changes to Scope do not apply retroactively. If no Scope is provided, each Contributors Necessary Claims are limited to that Contributors Contributions.
**9.14. Working Group.** “Working Group” means this project to develop specifications, standards, best practices, guidelines, and other similar materials under this License.
*The text of this Community Specification License is Copyright 2020 Joint Development Foundation and is licensed under the Creative Commons Attribution 4.0 International License available at https://creativecommons.org/licenses/by/4.0/.*
SPDX-License-Identifier: CC-BY-4.0

View File

@@ -0,0 +1,58 @@
# Notices
## Code of Conduct
Contact for Code of Conduct issues or inquires: _________________
[Ideally list two different individuals above (not a generic mailing list) as someone submitting a Code of Conduct complaint will want to know exactly who is receiving the complaint. We recommend two individuals in the case one of the individuals is the subject of or directly involved in the subject of a complaint.]
## License Acceptance
Per Community Specification License 1.0 Section 2.1.3.3, Licensees may indicate their acceptance of the Community Specification License by issuing a pull request to the Specifications repositorys Notice.md file, including the Licensees name, authorized individuals' names, and repository system identifier (e.g. GitHub ID), and specification version.
A Licensee may consent to accepting the current Community Specification License version or any future version of the Community Specification License by indicating "or later" after their specification version.
---------------------------------------------------------------------------------
Licensees name:
Authorized individual and system identifier:
Specification version:
---------------------------------------------------------------------------------
## Withdrawals
Name of party withdrawing:
Date of withdrawal:
---------------------------------------------------------------------------------
## Exclusions
This section includes any Exclusion Notices made against a Draft Deliverable or Approved Deliverable as set forth in the Community Specification Development License. Each Exclusion Notice must include the following information:
- Name of party making the Exclusion Notice:
- Name of patent owner:
- Specification:
- Version number:
**For issued patents and published patent applications:**
(i) patent number(s) or title and application number(s), as the case may be:
(ii) identification of the specific part(s) of the Specification whose implementation makes the excluded claim a Necessary Claim.
**For unpublished patent applications must provide either:**
(i) the text of the filed application; or
(ii) identification of the specific part(s) of the Specification whose implementation makes the excluded claim a Necessary Claim.
-----------------------------------------------------------------------------------------

326
options/license/ACL-1.0 Normal file
View File

@@ -0,0 +1,326 @@
# Auditable Commercial License (ACL) v1.0
**A Source-Available Commercial License for the AI Era**
---
## Preamble
This license balances transparency with commercial sustainability. It grants the Licensee the right to inspect, audit, and locally modify source code while protecting the Licensor's ability to sustain development. It explicitly addresses the use of licensed code in artificial intelligence training, automated code generation, and derivative redistribution.
This license is **not** an open-source license and does not satisfy the Open Source Definition. It is a **source-available commercial license** that prioritizes auditability, defined commercial terms, and fair use.
To provide adopters with a defined long-term guarantee, each released version of the Licensed Work automatically converts to the Apache License, Version 2.0 on the Change Date defined in Section 5.2. This ensures that adopters are never permanently dependent on the Licensor's continued operation or good will with respect to any given version of the Licensed Work.
After the Change Date for any given version, that version is governed entirely by the Change License, and the restrictions in Section 2 — including the AI-training restrictions in Section 2.3 — no longer apply to that version. This trade-off is intentional: Licensors who require indefinite AI-training protection should evaluate whether this license is appropriate for their work.
---
## Definitions
**"Affiliate"** — Any entity that controls, is controlled by, or is under common control with a party, where "control" means direct or indirect ownership of more than fifty percent (50%) of the voting interests of such entity.
**"AI System"** — Any machine learning model, large language model, neural network, embedding system, retrieval-augmented generation system, code generation model, or other automated system that (i) is trained, fine-tuned, or evaluated on data, or (ii) produces code, text, images, audio, or other derivative outputs as a function of training on data.
**"AI Tool"** — A software product that embeds or invokes an AI System to assist the Licensee's personnel with development, analysis, or operational tasks (examples include code assistants, IDE integrations, and agentic development tools). An AI Tool may be operated by the Licensee, by a third-party vendor, or by both.
**"Change Date"** — With respect to any given version of the Licensed Work, the fourth (4th) anniversary of the date that version is first publicly released by the Licensor under this license, or such earlier date as the Licensor may specify for that version in writing or as accelerated under Section 5.6.
**"Change License"** — The Apache License, Version 2.0, as published by the Apache Software Foundation, available at `https://www.apache.org/licenses/LICENSE-2.0`.
**"Competing Product"** — A product or service that the Licensee markets as a substitute for, or drop-in replacement for, the Licensed Work.
**"Derivative Work"** — Any software that incorporates, adapts, translates, modifies, or is substantially based on the Licensed Work or any material portion thereof. Independently developed software that merely interoperates with the Licensed Work through its public interfaces is not a Derivative Work.
**"Hosted Service"** — Any offering that makes the functionality of the Licensed Work or a Derivative Work available to third parties over a network — directly or through agents, automated systems, or other intermediaries acting on the Licensee's behalf — including software-as-a-service, platform-as-a-service, managed service, application programming interface, or embedded offering, whether or not the Licensed Work is directly accessible to such third parties.
**"Internal Use"** — Use of the Licensed Work by the Licensee and its Affiliates within their own organizations, infrastructure, and operations, including use by the Licensee's employees and contractors in the course of their work for the Licensee, but excluding any provision of the Licensed Work's functionality to third parties as a Hosted Service.
**"Licensed Work"** — The software, source code, object code, documentation, configuration files, and associated materials distributed by the Licensor under this license, as identified in the applicable release.
**"Licensee"** — The individual or entity that obtains, accesses, or uses the Licensed Work under this license. Where the Licensee is an entity, the term includes its Affiliates solely for purposes of Internal Use under Section 1. References to the Licensee's actions include actions taken by automated agents, scripts, or systems operated by or on behalf of the Licensee; the Licensee is responsible for ensuring that such automated actions comply with this license.
**"Licensor"** — The individual or entity identified as the copyright holder in the Licensed Work's distribution materials.
**"Production Use"** — Use of the Licensed Work in support of the Licensee's live operations, customer-facing systems, or revenue-generating activities, as distinguished from evaluation, development, or testing use.
**"Third Party"** — Any individual or entity other than the Licensee and its Affiliates.
**"Tier Schedule"** — The separate document maintained by the Licensor that specifies the available commercial tiers, their pricing, their usage parameters, and any tier-specific terms, as referenced in Section 4.
---
## 1. Grant of Rights
Subject to the terms of this license and payment of any applicable fees under the Tier Schedule, the Licensor grants the Licensee a **non-exclusive, non-transferable** (except as provided in Section 8.3) license to:
### 1.1 Source Inspection and Audit
Read, review, and audit the source code of the Licensed Work for purposes of security evaluation, compliance verification, operational understanding, and internal technical review.
### 1.2 Internal Use
Install, execute, and use the Licensed Work for Internal Use in accordance with the applicable tier defined in the Tier Schedule.
### 1.3 Local Modification
Create modifications to the Licensed Work for Internal Use only, provided such modifications are not distributed, sublicensed, or made available to Third Parties except as permitted under Sections 2 and 4.
### 1.4 Integration
Integrate the Licensed Work with the Licensee's own systems, applications, and workflows for Internal Use.
### 1.5 Patent License
The Licensor grants the Licensee a non-exclusive, worldwide, royalty-free license under any patents owned or controlled by the Licensor that would otherwise be infringed by the Licensee's exercise of the rights granted in Sections 1.1 through 1.4. This patent license terminates automatically if the Licensee initiates patent litigation against the Licensor or any other Licensee alleging that the Licensed Work infringes a patent.
---
## 2. Restrictions
### 2.1 No Redistribution
The Licensee may not redistribute, sublicense, sell, lease, publish, or otherwise transfer the Licensed Work, its source code, or any Derivative Work to any Third Party, except as expressly permitted by a separate written agreement with the Licensor or by the OEM / Embedding tier defined in Section 4.4.
### 2.2 No Hosted Service
The Licensee may not make the Licensed Work or any Derivative Work available to Third Parties as a Hosted Service. For the avoidance of doubt, this restriction is objective and does not depend on the Licensor's discretion: any offering meeting the definition of Hosted Service is prohibited absent a separate written agreement.
### 2.3 No AI Training
**(a) Prohibited Uses.** The Licensee may not knowingly:
1. Use the Licensed Work, including its source code, documentation, architecture, or test data, as training data, fine-tuning data, reinforcement signal, or evaluation data for any AI System;
2. Submit the Licensed Work to any AI System for the purpose of generating code, designs, or architectures intended to replicate, approximate, or be derived from the Licensed Work for use outside the scope of this license; or
3. Use any AI System to extract, summarize, or reproduce the functional logic, structural patterns, or implementation details of the Licensed Work for use in any project other than the Licensee's permitted Internal Use.
**(b) Permitted AI Tool Use.** The Licensee may use AI Tools to work with the Licensed Work in the course of permitted Internal Use — for example, to understand, debug, modify, or extend the Licensee's own local copies — provided that:
1. The AI Tool is operated under terms that do not, by default, incorporate the Licensee's inputs into the training or fine-tuning of the AI Tool's underlying AI System;
2. The Licensee does not intentionally configure the AI Tool to contribute the Licensed Work to training data; and
3. The Licensee takes reasonable measures, consistent with industry practice at the time, to prevent inadvertent contribution of the Licensed Work to AI System training. The Licensee is not responsible for AI Tool vendor behavior that the Licensee could not reasonably have known about or controlled.
**(c) Safe Harbor.** Use of an AI Tool that is operated under an enterprise, business, or commercial agreement whose default terms exclude customer inputs from AI System training shall be deemed to satisfy Section 2.3(b) absent the Licensee's actual knowledge to the contrary.
**(d) Flow-Through Obligation.** The Licensee shall not enter into an agreement with any AI Tool vendor that requires the Licensee to permit the use of the Licensed Work as training data. Where the Licensee becomes aware that an AI Tool it uses has incorporated the Licensed Work into training data in breach of this section, the Licensee shall promptly notify the Licensor and take commercially reasonable steps to cause the AI Tool vendor to cease such use.
### 2.4 No Obfuscation of Origin
The Licensee may not remove, alter, or obscure any copyright notices, license references, or attribution statements included in the Licensed Work.
---
## 3. Contributions
### 3.1 Grant of Rights in Contributions
If the Licensee submits modifications, improvements, extensions, bug fixes, or other materials to the Licensor (each, a "Contribution"), whether voluntarily or through an agreed contribution mechanism, the Licensee grants the Licensor:
**(a)** a perpetual, irrevocable, worldwide, royalty-free, sublicensable copyright license to use, reproduce, modify, distribute, and sublicense the Contribution as part of the Licensed Work under any license of the Licensor's choosing; and
**(b)** a perpetual, irrevocable, worldwide, royalty-free, sublicensable patent license under any patents owned or controlled by the Licensee that would otherwise be infringed by the incorporation or use of the Contribution in the Licensed Work.
### 3.2 No Obligation to Accept
The Licensor is under no obligation to accept, integrate, or maintain any Contribution submitted by the Licensee.
### 3.3 Contributor Recognition
The Licensor will make reasonable efforts to credit contributors in release notes or documentation, unless the contributor requests anonymity.
### 3.4 Representations
The Licensee represents that it has the legal right to submit each Contribution and to grant the licenses described in Section 3.1, and that each Contribution is either the Licensee's original work or is properly attributed and licensed from its original source.
### 3.5 Acceptance Mechanism
Contributions must be submitted with a Developer Certificate of Origin (DCO) sign-off affirming the Contributor's authority to grant the licenses described in Section 3.1. The Licensor may also require contributors to execute a separate Contributor License Agreement before accepting Contributions.
---
## 4. Commercial Tiers
The Licensed Work is offered under commercial tiers described in a separate **Tier Schedule** maintained by the Licensor. The Tier Schedule specifies the available tiers, pricing, usage parameters, support levels, and any tier-specific terms. At minimum, the following tiers are contemplated:
### 4.1 Community Tier (Free)
- Access to compiled or binary releases
- Community documentation
- No source code access
- No support guarantee
- Internal Use only; no Production Use by for-profit organizations with more than the headcount specified in the Tier Schedule
### 4.2 Professional Tier (Paid)
- Full source code access (read, audit, local modification)
- Standard support channels
- Internal Use up to the user or instance count specified in the Tier Schedule
- Production Use permitted, subject to the user or instance count specified in the Tier Schedule
- Updates and patches during the subscription period
### 4.3 Enterprise Tier (Paid)
- Full source code access with extended modification rights
- Priority support and defined service levels
- Internal Use without user or instance limits
- Right to request scoped custom modifications
- Deployment assistance and consultation
- Extended audit and compliance documentation
- Limited IP indemnification as set forth in Section 6.2
### 4.4 OEM / Embedding Tier (Negotiated)
- Right to embed the Licensed Work within the Licensee's own products
- Distribution and sublicensing rights under separately negotiated terms
- Co-branding or white-label options
- Custom commercial terms, which may override provisions of this license as expressly specified in the governing agreement
---
## 5. Term, Change Date, and Termination
### 5.1 Term
This license is effective from the date the Licensee first accesses or uses the Licensed Work and continues for the duration of the applicable subscription or agreement under the Tier Schedule, subject to the Change Date provision in Section 5.2 and the termination provisions in Section 5.3.
### 5.2 Change Date (Automatic Conversion to Apache 2.0)
Effective on the Change Date applicable to any version of the Licensed Work, that version — and only that version — shall be re-licensed and made available to the Licensee and to the public under the Change License (Apache License, Version 2.0). From and after the Change Date for a given version:
**(a)** the Licensee's use of that version is governed by the Change License rather than this license;
**(b)** the restrictions in Section 2 no longer apply to that version; and
**(c)** subsequent versions of the Licensed Work released by the Licensor may continue to be distributed under this license, each with its own Change Date.
The Licensor may, at its discretion, accelerate the Change Date for any version by publishing written notice to that effect. The Licensor may not extend or revoke the Change Date for a version once that version has been publicly released.
### 5.3 Termination for Breach
The Licensor may terminate this license, in whole or in part, upon written notice if the Licensee materially breaches this license and fails to cure such breach within thirty (30) days after receiving written notice of the breach. Breaches of Section 2.1 (No Redistribution), Section 2.2 (No Hosted Service), or Section 2.3 (No AI Training) are deemed material.
### 5.4 Effect of Termination and Wind-Down
Upon termination of this license other than conversion under Section 5.2:
**(a) Immediate Obligations.** The Licensee shall cease new deployments of the Licensed Work, cease granting new internal user access, and cease any activities that caused the termination.
**(b) Wind-Down Period.** Unless the termination is for a willful or repeated material breach, the Licensee shall have a wind-down period to migrate away from the Licensed Work. The wind-down period is thirty (30) days for the Community Tier, ninety (90) days for the Professional Tier, and twelve (12) months for the Enterprise Tier and OEM Tier. During the wind-down period, the Licensee may continue Production Use of the Licensed Work strictly to maintain existing deployments while migrating, but may not expand such use.
**(c) Return or Destruction.** At the end of the wind-down period, the Licensee shall destroy or return all copies of the Licensed Work and its source code in the Licensee's possession, except that the Licensee may retain a single archival copy to the extent required by applicable law, regulatory obligation, or internal audit policy, provided such archival copy is not used for any active purpose.
**(d) Versions Past Change Date.** Termination of this license does not affect the Licensee's rights under the Change License with respect to any version of the Licensed Work whose Change Date has already occurred.
### 5.5 Survival
Sections 2 (Restrictions, until the applicable Change Date for any given version), 3.1 (Grant of Rights in Contributions), 6 (Warranties and IP Indemnification), 7 (Limitation of Liability), and 8 (General Provisions) survive termination or expiration of this license.
### 5.6 Continuity Event
A "Continuity Event" occurs upon any of the following: (i) the Licensor ceases commercial distribution and support of the Licensed Work for one hundred eighty (180) consecutive days; (ii) the Licensor files a voluntary petition under bankruptcy or insolvency law, or has an involuntary petition filed against it that is not dismissed within sixty (60) days; or (iii) the Licensor publicly announces end-of-life of the Licensed Work without identifying a successor licensor.
Upon a Continuity Event, the Change Date for every then-current version of the Licensed Work shall be automatically accelerated to thirty (30) days after the Continuity Event. From and after that accelerated Change Date, those versions shall be governed by the Change License in accordance with Section 5.2.
---
## 6. Warranties and IP Indemnification
### 6.1 Disclaimer of Warranties
EXCEPT AS EXPRESSLY SET FORTH IN SECTION 6.2, THE LICENSED WORK IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ACCURACY, OR UNINTERRUPTED OPERATION. THE LICENSEE ASSUMES ALL RISK ARISING FROM ITS USE OF THE LICENSED WORK.
### 6.2 Limited IP Indemnification (Enterprise and OEM Tiers)
For Licensees under the Enterprise Tier or OEM Tier and in good standing with respect to payment obligations, the Licensor shall defend and indemnify the Licensee against any Third Party claim that the Licensed Work, when used in accordance with this license, infringes any valid patent, copyright, trademark, or trade secret, and shall pay damages finally awarded by a court of competent jurisdiction or agreed in settlement, subject to the following:
**(a) Conditions.** The Licensee must (i) promptly notify the Licensor in writing of the claim, (ii) grant the Licensor sole control of the defense and settlement, and (iii) provide reasonable cooperation at the Licensor's expense.
**(b) Exclusions.** The Licensor has no obligation under this Section 6.2 to the extent a claim arises from (i) modifications to the Licensed Work not made or authorized by the Licensor, (ii) combination of the Licensed Work with other software or systems not provided or authorized by the Licensor, (iii) use of the Licensed Work in breach of this license, or (iv) use of a version of the Licensed Work after the Licensor has made a non-infringing version available to the Licensee.
**(c) Remedies.** If the Licensed Work is held to infringe, or the Licensor reasonably believes it is likely to be held to infringe, the Licensor may at its option (i) procure the right for the Licensee to continue using the Licensed Work, (ii) modify the Licensed Work to be non-infringing, or (iii) terminate the affected license and refund the prorated fees paid for the then-current subscription term.
**(d) Exclusive Remedy.** This Section 6.2 states the Licensee's exclusive remedy and the Licensor's entire liability for intellectual property infringement claims.
### 6.3 Mutual Patent Termination
If the Licensee or any of its Affiliates initiates patent litigation (including a cross-claim or counterclaim) against the Licensor or any other Licensee alleging that the Licensed Work infringes a patent, then all licenses granted to the Licensee under Sections 1, 2, and 6.2 shall terminate immediately.
---
## 7. Limitation of Liability
### 7.1 Exclusion of Indirect Damages
IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO LOST PROFITS, LOST REVENUE, LOST DATA, BUSINESS INTERRUPTION, OR COST OF SUBSTITUTE GOODS OR SERVICES, ARISING OUT OF OR RELATING TO THIS LICENSE OR THE LICENSED WORK, WHETHER IN CONTRACT, TORT, OR UNDER ANY OTHER THEORY OF LIABILITY, AND EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
### 7.2 Liability Cap
THE LICENSOR'S TOTAL CUMULATIVE LIABILITY ARISING OUT OF OR RELATING TO THIS LICENSE SHALL NOT EXCEED THE AMOUNTS PAID BY THE LICENSEE TO THE LICENSOR UNDER THE APPLICABLE TIER IN THE TWELVE (12) MONTHS IMMEDIATELY PRECEDING THE EVENT GIVING RISE TO THE CLAIM.
### 7.3 Exceptions
The limitations in this Section 7 do not apply to (i) the Licensor's indemnification obligations under Section 6.2, (ii) breaches of confidentiality obligations under a separate agreement, or (iii) liability that cannot be limited under applicable law.
---
## 8. General Provisions
### 8.1 Governing Law and Venue
This license shall be governed by and construed in accordance with the laws of [Jurisdiction], without regard to its conflict-of-law principles. The parties irrevocably submit to the exclusive jurisdiction of the state and federal courts located in [Venue] for the resolution of any dispute arising out of or relating to this license, except as otherwise provided in Section 8.2.
### 8.2 Dispute Resolution
The parties shall first attempt in good faith to resolve any dispute arising out of or relating to this license through direct negotiation between executives with authority to settle the dispute. If the dispute is not resolved within sixty (60) days of written notice of the dispute, either party may initiate proceedings in the courts identified in Section 8.1. Nothing in this Section 8.2 prevents either party from seeking injunctive relief at any time to protect its intellectual property or confidential information.
### 8.3 Assignment and Change of Control
The Licensee may assign this license to a successor in interest in connection with a merger, acquisition, reorganization, or sale of all or substantially all of the Licensee's assets, upon written notice to the Licensor.
If, at the time of assignment, the successor entity is engaged in developing or marketing a Competing Product, then within thirty (30) days of the Licensor's written request, the parties shall negotiate in good faith either (i) a wind-down of the successor's use of the Licensed Work over a period not to exceed twelve (12) months, or (ii) a new license agreement on commercial terms appropriate to the successor's circumstances. If the parties have not reached agreement within ninety (90) days of the Licensor's request, the Licensor may terminate the license on ninety (90) days' written notice to the successor.
Other assignments by the Licensee require the prior written consent of the Licensor. The Licensor may assign this license to any successor to its business or to an Affiliate without notice or consent.
### 8.4 Third-Party Components
The Licensed Work may incorporate third-party open-source or commercial components, each of which is governed by its own license terms. Such components are identified in a NOTICE or THIRD_PARTY_LICENSES file distributed with the Licensed Work. Nothing in this license alters the terms under which such third-party components are licensed.
### 8.5 Compliance with Laws and Export Controls
Each party shall comply with all laws applicable to its performance under this license. The Licensee represents that it is not located in, and will not use, export, or re-export the Licensed Work to, any country or Person subject to comprehensive economic sanctions administered by the United States, the European Union, or the United Kingdom, and that it is not identified on any restricted-party list maintained by such authorities.
### 8.6 Data Protection
The Licensor is not a data processor with respect to any personal data processed by the Licensee through its use of the Licensed Work. The Licensee is solely responsible for compliance with applicable data-protection laws in its use of the Licensed Work.
### 8.7 Notices
All notices required under this license must be in writing and delivered to the address designated by the receiving party in the applicable Tier Schedule or subscription agreement, or — where no such address has been designated — to the Licensor's published contact address. Notices are effective upon receipt, or three (3) business days after dispatch if sent by registered mail or recognized courier service.
### 8.8 Force Majeure
Neither party shall be liable for any failure or delay in performance (other than payment obligations) caused by events beyond its reasonable control, including acts of God, war, terrorism, civil disturbance, labor dispute, utility failure, pandemic, or government action. The affected party shall promptly notify the other and use reasonable efforts to resume performance.
### 8.9 Severability
If any provision of this license is held unenforceable by a court of competent jurisdiction, that provision shall be modified to the minimum extent necessary to make it enforceable, and the remaining provisions shall continue in full force and effect.
### 8.10 Entire Agreement
This license, together with the applicable Tier Schedule, subscription agreement, and any separately negotiated terms for OEM Tier Licensees, constitutes the entire agreement between the parties regarding the Licensed Work and supersedes all prior or contemporaneous communications, representations, and agreements on that subject matter.
### 8.11 Amendment of This License
The Licensor may publish updated versions of this license for future releases of the Licensed Work. Existing Licensees remain bound by the version of this license in effect at the time of their current subscription term, and will be offered the opportunity to accept or reject the updated version at the time of their next renewal.
### 8.12 No Partnership
Nothing in this license creates a partnership, joint venture, employment, franchise, or agency relationship between the parties. Neither party has authority to bind the other.
### 8.13 Relationship to Tier Schedule
In the event of any conflict between this license and the Tier Schedule, this license controls, except where the Tier Schedule expressly identifies a specific provision of this license and states that it is modified for the applicable tier. OEM Tier agreements may expressly override provisions of this license as specified in the governing OEM agreement.
The Licensor may update the Tier Schedule from time to time. Changes that expand Licensee rights or reduce fees take effect immediately upon publication. Changes that restrict Licensee rights or increase fees take effect at the Licensee's next renewal following ninety (90) days' notice, except where required by law or by the Licensor's third-party obligations.
---
## Attribution
This license is published as the **Auditable Commercial License (ACL) v1.0**. Other projects may adopt, adapt, or reference this license with attribution to the original authors.
### Suggested Notice for Source Files
```
Copyright (c) [Year] [Licensor Name]
Licensed under the Auditable Commercial License (ACL) v1.0
See LICENSE.md for terms. This is not open-source software.
On the Change Date specified in Section 5.2, this version
converts to the Apache License, Version 2.0.
```
### Suggested Notice for Distribution Packages
```
This software is distributed under the Auditable Commercial
License (ACL) v1.0 — a source-available commercial license.
Full terms: [URL to LICENSE.md]
Tier details: [URL to Tier Schedule]
Change Date for this version: [YYYY-MM-DD]
```
---
## Drafting Notes for Adoption
The following items must be customized before this license is applied to a specific Licensed Work:
1. **Section 8.1 — Governing Law and Venue.** Fill in the jurisdiction and venue appropriate to the publishing entity. Common choices include Delaware (broad U.S. commercial precedent), Singapore (Asia-Pacific), or the jurisdiction of the Licensor's principal place of business.
2. **Section 4 — Tier Schedule.** Publish or link to a Tier Schedule defining user counts, pricing, and support terms for each tier.
3. **Section 8.7 — Notices.** Specify the Licensor's contact address for legal notices.
4. **Per-Version Change Date.** For each released version, record the Change Date in the release metadata (the default being the fourth anniversary of public release, subject to acceleration under Section 5.6).
5. **Third-Party Components.** Maintain a NOTICE or THIRD_PARTY_LICENSES file listing embedded open-source or commercial components.
---
*Auditable Commercial License (ACL) v1.0*
*This license should be reviewed by qualified legal counsel in the Licensor's jurisdiction before being applied to any production Licensed Work. This document is drafted as a polished commercial license template and is intended to minimize the review effort required of counsel, but it is not a substitute for that review.*

96
options/license/BUSL-1.1 Normal file
View File

@@ -0,0 +1,96 @@
Business Source License 1.1
Parameters
Licensor: <copyright holders>
Licensed Work: <program>
The Licensed Work is (c) <year> <copyright holders>
Additional Use Grant: None
Change Date: Four years from the date the Licensed Work is published.
Change License: Apache License, Version 2.0
For information about alternative licensing arrangements for the Licensed Work,
please contact the Licensor.
Notice
The Business Source License (this document, or the "License") is not an Open
Source license. However, the Licensed Work will eventually be made available
under an Open Source License, as stated in this License.
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
"Business Source License" is a trademark of MariaDB Corporation Ab.
-----------------------------------------------------------------------------
Business Source License 1.1
Terms
The Licensor hereby grants you the right to copy, modify, create derivative
works, redistribute, and make non-production use of the Licensed Work. The
Licensor may make an Additional Use Grant, above, permitting limited
production use.
Effective on the Change Date, or the fourth anniversary of the first publicly
available distribution of a specific version of the Licensed Work under this
License, whichever comes first, the Licensor hereby grants you rights under
the terms of the Change License, and the rights granted in the paragraph
above terminate.
If your use of the Licensed Work does not comply with the requirements
currently in effect as described in this License, you must purchase a
commercial license from the Licensor, its affiliated entities, or authorized
resellers, or you must refrain from using the Licensed Work.
All copies of the original and modified Licensed Work, and derivative works
of the Licensed Work, are subject to this License. This License applies
separately for each version of the Licensed Work and the Change Date may vary
for each version of the Licensed Work released by Licensor.
You must conspicuously display this License on each original or modified copy
of the Licensed Work. If you receive the Licensed Work in original or
modified form from a third party, the terms and conditions set forth in this
License apply to your use of that work.
Any use of the Licensed Work in violation of this License will automatically
terminate your rights under this License for the current and all other
versions of the Licensed Work.
This License does not grant you any right in any trademark or logo of
Licensor or its affiliates (provided that you may use a trademark or logo of
Licensor as expressly required by this License).
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
TITLE.
MariaDB hereby grants you permission to use this License's text to license
your works, and to refer to it using the trademark "Business Source License",
as long as you comply with the Covenants of Licensor below.
Covenants of Licensor
In consideration of the right to use this License's text and the "Business
Source License" name and trademark, Licensor covenants to MariaDB, and to all
other recipients of the licensed work to be provided by Licensor:
1. To specify as the Change License the GPL Version 2.0 or any later version,
or a license that is compatible with GPL Version 2.0 or a later version,
where "compatible" means that software provided under the Change License can
be included in a program with software provided under GPL Version 2.0 or a
later version. Licensor may specify additional Change Licenses without
limitation.
2. To either: (a) specify an additional grant of rights to use that does not
impose any additional restriction on the right granted in this License, as
the Additional Use Grant; or (b) insert the text "None".
3. To specify a Change Date.
4. Not to modify this License in any other way.

View File

@@ -0,0 +1,158 @@
Creative Commons Attribution-NonCommercial 4.0 International
Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors.
Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensors permission is not necessary for any reasonfor example, because of any applicable exception or limitation to copyrightthen that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public.
Creative Commons Attribution-NonCommercial 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
Section 1 Definitions.
a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
h. Licensor means the individual(s) or entity(ies) granting rights under this Public License.
i. NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange.
j. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
k. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
l. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
Section 2 Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and
B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
3. Term. The term of this Public License is specified in Section 6(a).
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
5. Downstream recipients.
A. Offer from the Licensor Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this Public License.
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes.
Section 3 License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified form), You must:
A. retain the following if it is supplied by the Licensor with the Licensed Material:
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of warranties;
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License.
Section 4 Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only;
b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
Section 5 Disclaimer of Warranties and Limitation of Liability.
a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
Section 6 Term and Termination.
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
Section 7 Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
Section 8 Interpretation.
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
Creative Commons may be contacted at creativecommons.org.

View File

@@ -0,0 +1,154 @@
Creative Commons Attribution-NoDerivatives 4.0 International
Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors.
Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensors permission is not necessary for any reasonfor example, because of any applicable exception or limitation to copyrightthen that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public.
Creative Commons Attribution-NoDerivatives 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NoDerivatives 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
Section 1 Definitions.
a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
b. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
c. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
d. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
e. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
f. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
g. Licensor means the individual(s) or entity(ies) granting rights under this Public License.
h. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
i. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
j. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
Section 2 Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
A. reproduce and Share the Licensed Material, in whole or in part; and
B. produce and reproduce, but not Share, Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
3. Term. The term of this Public License is specified in Section 6(a).
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
5. Downstream recipients.
A. Offer from the Licensor Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this Public License.
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.
Section 3 License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
a. Attribution.
1. If You Share the Licensed Material, You must:
A. retain the following if it is supplied by the Licensor with the Licensed Material:
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of warranties;
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
2. For the avoidance of doubt, You do not have permission under this Public License to Share Adapted Material.
3. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
4. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
Section 4 Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database, provided You do not Share Adapted Material;
b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
Section 5 Disclaimer of Warranties and Limitation of Liability.
a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
Section 6 Term and Termination.
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
2. upon express reinstatement by the Licensor.
c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
Section 7 Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
Section 8 Interpretation.
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
Creative Commons may be contacted at creativecommons.org.

View File

@@ -413,6 +413,7 @@
"admin.dashboard.cleanup_packages": "Clean up expired packages",
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
"admin.users.bot": "Bot",
@@ -660,6 +661,9 @@
"repo.settings.display_title": "Display Title",
"repo.settings.display_title_placeholder": "Optional display title for this repository",
"repo.settings.display_title_help": "A custom title shown prominently on the repository page. Leave empty to use the repository name.",
"repo.settings.owner_display_name": "Owner Name",
"repo.settings.owner_display_name_placeholder": "e.g., John Smith, Acme Corp",
"repo.settings.owner_display_name_help": "A display name for the repository owner. Used in licenses, social cards, and other public-facing contexts instead of the username.",
"repo.settings.license": "License",
"repo.settings.license_type": "License Type",
"repo.settings.license_none": "No license selected",
@@ -688,11 +692,41 @@
"repo.settings.pages.saved": "Settings saved successfully",
"repo.settings.pages.brand_name": "Brand Name",
"repo.settings.pages.brand_name_help": "The name displayed on your landing page",
"repo.settings.pages.brand_logo": "Logo",
"repo.settings.pages.brand_logo_url": "Logo URL",
"repo.settings.pages.brand_logo_url_help": "URL to your logo image (SVG or PNG)",
"repo.settings.pages.brand_upload_logo": "Upload Logo",
"repo.settings.pages.brand_delete_logo": "Delete Logo",
"repo.settings.pages.brand_logo_uploaded": "Logo uploaded successfully.",
"repo.settings.pages.brand_logo_deleted": "Logo deleted.",
"repo.settings.pages.brand_upload_btn": "Upload",
"repo.settings.pages.brand_upload_help": "Upload a JPG, PNG, WebP, or GIF image (max 5 MB). Uploaded images take priority over the URL field.",
"repo.settings.pages.brand_upload_error": "Failed to upload image.",
"repo.settings.pages.brand_image_too_large": "Image is too large. Maximum size is 5 MB.",
"repo.settings.pages.brand_not_an_image": "The uploaded file is not a valid image.",
"repo.settings.pages.brand_or": "or use a URL",
"repo.settings.pages.brand_favicon": "Favicon",
"repo.settings.pages.brand_favicon_url": "Favicon URL",
"repo.settings.pages.brand_favicon_url_help": "URL to a custom favicon for your landing page (ICO, PNG, or SVG). Leave blank to use the default.",
"repo.settings.pages.brand_upload_favicon": "Upload Favicon",
"repo.settings.pages.brand_delete_favicon": "Delete Favicon",
"repo.settings.pages.brand_favicon_uploaded": "Favicon uploaded successfully.",
"repo.settings.pages.brand_favicon_deleted": "Favicon deleted.",
"repo.settings.pages.brand_favicon_upload_help": "Upload an ICO, PNG, WebP, or GIF favicon (max 5 MB). Uploaded favicons take priority over the URL field.",
"repo.settings.pages.brand_tagline": "Tagline",
"repo.settings.pages.headline": "Headline",
"repo.settings.pages.subheadline": "Subheadline",
"repo.settings.pages.hero_image": "Hero Image",
"repo.settings.pages.hero_upload": "Upload Image",
"repo.settings.pages.hero_upload_btn": "Upload",
"repo.settings.pages.hero_upload_help": "Upload a JPG, PNG, WebP, or GIF image (max 5 MB). Uploaded images take priority over the URL field.",
"repo.settings.pages.hero_upload_error": "Failed to upload image.",
"repo.settings.pages.hero_image_too_large": "Image is too large. Maximum size is 5 MB.",
"repo.settings.pages.hero_not_an_image": "The uploaded file is not a valid image.",
"repo.settings.pages.hero_image_uploaded": "Hero image uploaded successfully.",
"repo.settings.pages.hero_image_deleted": "Hero image deleted.",
"repo.settings.pages.hero_delete_image": "Delete Image",
"repo.settings.pages.hero_or": "or use a URL",
"repo.settings.pages.image_url": "Hero Image URL",
"repo.settings.pages.video_url": "Demo Video URL",
"repo.settings.pages.code_example": "Code Example",
@@ -703,6 +737,23 @@
"repo.settings.pages.stats": "Stats",
"repo.settings.pages.value_props": "Value Propositions",
"repo.settings.pages.features": "Features",
"repo.settings.pages.gallery_section": "Gallery Section",
"repo.settings.pages.gallery_enabled_desc": "Show a gallery of images from your repository's .gallery folder on the landing page",
"repo.settings.pages.gallery_headline": "Gallery Headline",
"repo.settings.pages.gallery_subheadline": "Gallery Subheadline",
"repo.settings.pages.gallery_max_images": "Maximum Images to Show",
"repo.settings.pages.gallery_columns": "Grid Columns",
"repo.settings.pages.gallery_help_link": "Upload and manage gallery images in Settings > Gallery.",
"repo.settings.pages.comparison_section": "Comparison Section",
"repo.settings.pages.comparison_enabled_desc": "Show a feature comparison table on the landing page",
"repo.settings.pages.comparison_headline": "Headline",
"repo.settings.pages.comparison_subheadline": "Subheadline",
"repo.settings.pages.comparison_columns": "Comparison Columns",
"repo.settings.pages.comparison_columns_help": "Define three columns for your comparison table (e.g., your product vs. two competitors, or three tiers).",
"repo.settings.pages.comparison_highlight": "Highlight",
"repo.settings.pages.comparison_features": "Features & Groups",
"repo.settings.pages.comparison_features_help": "Organize features into optional groups. For each cell, enter \"true\" for a checkmark, \"false\" for an X, or any text.",
"repo.settings.pages.comparison_group_name": "Group Name",
"repo.settings.pages.company_logos": "Company Logos",
"repo.settings.pages.testimonials": "Testimonials",
"repo.settings.pages.pricing_headline": "Pricing Headline",
@@ -724,7 +775,11 @@
"repo.settings.pages.seo_title": "SEO Title",
"repo.settings.pages.seo_description": "Meta Description",
"repo.settings.pages.seo_keywords": "Keywords",
"repo.settings.pages.seo_keywords.placeholder": "Add a keyword...",
"repo.settings.pages.seo_keywords.add": "Add",
"repo.settings.pages.og_image": "Open Graph Image URL",
"repo.settings.pages.use_media_kit_og": "Use Media Kit social card",
"repo.settings.pages.use_media_kit_og_help": "Use the social card from your Media Kit settings as the Open Graph image instead of a custom URL.",
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -2040,6 +2040,7 @@
"repo.blog.subscription_required_desc": "This post is available exclusively to subscribers. Subscribe to unlock the full content.",
"repo.blog.subscribe_to_read": "Subscribe to Read",
"repo.blog.reactions.admin_hint": "Thumbs down counts are only visible to repo admins.",
"repo.blog.views": "views",
"repo.blog.comments": "Comments",
"repo.blog.comments.empty": "No comments yet. Be the first to share your thoughts!",
"repo.blog.comments.disabled": "Comments have been disabled for this post.",
@@ -2114,6 +2115,38 @@
"repo.wishlist.item_closed": "Wishlist item has been closed.",
"repo.wishlist.item_reopened": "Wishlist item has been reopened.",
"repo.wishlist.cannot_vote_closed": "Cannot vote on a closed item.",
"repo.settings.ai": "AI",
"repo.settings.ai.globally_disabled": "AI features are disabled by the system administrator.",
"repo.settings.ai.enable": "Enable AI",
"repo.settings.ai.enable_desc": "Enable AI-powered operations for this repository (code review, issue triage, auto-respond, etc.)",
"repo.settings.ai.not_enabled": "AI is not enabled for this repository. Enable it first.",
"repo.settings.ai.disabled_by_admin": "(disabled by admin)",
"repo.settings.ai.tier1": "Tier 1: Light AI Operations",
"repo.settings.ai.auto_respond_issues": "Automatically respond to new issues with helpful suggestions",
"repo.settings.ai.auto_review_prs": "Automatically review pull requests for code quality and security",
"repo.settings.ai.auto_triage_issues": "Automatically triage and label new issues",
"repo.settings.ai.auto_inspect_workflows": "Automatically inspect workflow changes for issues",
"repo.settings.ai.tier2": "Tier 2: Agent Mode",
"repo.settings.ai.agent_mode": "Enable agent mode (AI can modify code, create branches, and submit PRs)",
"repo.settings.ai.agent_trigger_labels": "Trigger Labels",
"repo.settings.ai.agent_trigger_labels_desc": "Comma-separated list of labels that trigger agent mode when added to an issue (e.g. ai-fix, ai-implement)",
"repo.settings.ai.agent_max_run_minutes": "Max Run Time (minutes)",
"repo.settings.ai.escalation": "Escalation",
"repo.settings.ai.escalate_to_staff": "Escalate to staff when AI confidence is low or agent fails",
"repo.settings.ai.escalation_label": "Escalation Label",
"repo.settings.ai.escalation_assign_team": "Assign to Team",
"repo.settings.ai.provider": "Provider & Model",
"repo.settings.ai.preferred_provider": "Preferred Provider",
"repo.settings.ai.preferred_model": "Preferred Model",
"repo.settings.ai.inherit_default": "Inherit from org/system default",
"repo.settings.ai.resolved_provider": "Currently using",
"repo.settings.ai.instructions": "Custom Instructions",
"repo.settings.ai.system_instructions": "System Instructions",
"repo.settings.ai.system_instructions_desc": "General instructions for all AI operations on this repository",
"repo.settings.ai.review_instructions": "Code Review Instructions",
"repo.settings.ai.review_instructions_desc": "Specific instructions for AI code reviews (focus areas, coding standards, etc.)",
"repo.settings.ai.issue_instructions": "Issue Response Instructions",
"repo.settings.ai.issue_instructions_desc": "Specific instructions for AI issue responses (tone, common answers, project context, etc.)",
"repo.settings.wishlist": "Wishlist",
"repo.settings.wishlist.enable": "Enable Wishlist",
"repo.settings.wishlist.enable_help": "When enabled, a Wishlist tab appears on the repository where users can submit and vote on feature requests.",
@@ -2270,6 +2303,47 @@
"repo.settings.mirror_settings.push_mirror.none": "No push mirrors configured",
"repo.settings.mirror_settings.push_mirror.remote_url": "Git Remote Repository URL",
"repo.settings.mirror_settings.push_mirror.add": "Add Push Mirror",
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files": "Exclude hidden files and folders",
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files_desc": "Do not push files/folders starting with \".\" or marked as hidden to the remote mirror",
"repo.settings.mirror_settings.push_mirror.reset_tooltip": "Reset mirror to a specific commit",
"repo.settings.mirror_settings.push_mirror.reset_title": "Reset push mirror to commit",
"repo.settings.mirror_settings.push_mirror.reset_warning_title": "Destructive action",
"repo.settings.mirror_settings.push_mirror.reset_warning_body": "This will force-push the chosen commit to the remote and rewrite the branch's history there. Auto-sync will be paused so the next scheduled sync does not undo this.",
"repo.settings.mirror_settings.push_mirror.reset_branch": "Branch on remote",
"repo.settings.mirror_settings.push_mirror.reset_branch_help": "The branch on the remote mirror to reset.",
"repo.settings.mirror_settings.push_mirror.reset_commit": "Commit SHA",
"repo.settings.mirror_settings.push_mirror.reset_commit_help": "Full SHA of the commit in this repository to push to the remote.",
"repo.settings.mirror_settings.push_mirror.reset_confirm": "Force-push and pause sync",
"repo.settings.mirror_settings.push_mirror.reset_required": "Branch and commit SHA are required.",
"repo.settings.mirror_settings.push_mirror.reset_failed": "Failed to reset mirror: %s",
"repo.settings.mirror_settings.push_mirror.reset_success": "Mirror reset complete. Auto-sync has been paused; re-enable it via Edit when ready.",
"repo.settings.mirror_settings.push_mirror.reset_all_button": "Reset all mirrors to a commit",
"repo.settings.mirror_settings.push_mirror.reset_all_button_help": "Force-push the same commit to every push mirror at once and pause all auto-sync.",
"repo.settings.mirror_settings.push_mirror.reset_all_title": "Reset all push mirrors to commit",
"repo.settings.mirror_settings.push_mirror.reset_all_warning_title": "Destructive action — affects all mirrors",
"repo.settings.mirror_settings.push_mirror.reset_all_warning_body": "This will force-push the chosen commit to the named branch on every push mirror configured for this repository. Auto-sync will be paused on all of them.",
"repo.settings.mirror_settings.push_mirror.reset_all_confirm": "Force-push all mirrors and pause sync",
"repo.settings.mirror_settings.push_mirror.reset_all_success": "Reset complete. %d mirror(s) updated and auto-sync paused.",
"repo.settings.mirror_settings.push_mirror.reset_all_partial": "Reset partially complete: %d succeeded, %d failed. Errors: %s",
"repo.settings.repo_reset.title": "Reset Repository History",
"repo.settings.repo_reset.desc": "Move a branch's HEAD to a specific commit, dropping later commits, and force-push the change to every push mirror.",
"repo.settings.repo_reset.button": "Reset to Commit",
"repo.settings.repo_reset.warning_title": "This rewrites repository history",
"repo.settings.repo_reset.warning_body": "Performing this action will:",
"repo.settings.repo_reset.warning_branch": "Move the branch's HEAD on this server to the chosen commit. Commits after that point on this branch become unreachable.",
"repo.settings.repo_reset.warning_collaborators": "Anyone who has cloned or pulled the repository will need to re-clone or hard-reset their local copy.",
"repo.settings.repo_reset.warning_mirrors": "All push mirrors for this repository will be force-pushed to match, and their auto-sync will be paused.",
"repo.settings.repo_reset.branch": "Branch to reset",
"repo.settings.repo_reset.commit": "Commit SHA",
"repo.settings.repo_reset.confirm_name": "To confirm, type the repository name: %s",
"repo.settings.repo_reset.confirm": "Reset repository",
"repo.settings.repo_reset.required": "Branch and commit SHA are required.",
"repo.settings.repo_reset.confirm_mismatch": "The repository name confirmation did not match.",
"repo.settings.repo_reset.commit_not_found": "Commit %s was not found in this repository.",
"repo.settings.repo_reset.branch_not_found": "Branch %s does not exist in this repository.",
"repo.settings.repo_reset.failed": "Failed to reset repository: %s",
"repo.settings.repo_reset.success": "Repository reset complete. All push mirrors have been force-pushed and their auto-sync is paused.",
"repo.settings.repo_reset.partial": "Repository was reset locally, but some mirrors failed to update: %s",
"repo.settings.mirror_settings.push_mirror.edit_sync_time": "Edit mirror sync interval",
"repo.settings.sync_mirror": "Synchronize Now",
"repo.settings.pull_mirror_sync_in_progress": "Pulling changes from the remote %s at the moment.",
@@ -2313,6 +2387,7 @@
"repo.settings.pulls.default_delete_branch_after_merge": "Delete pull request branch after merge by default",
"repo.settings.pulls.default_allow_edits_from_maintainers": "Allow edits from maintainers by default",
"repo.settings.releases_desc": "Enable Repository Releases",
"repo.settings.public_release_downloads_desc": "Allow direct release downloads without authentication (for Limited visibility repos)",
"repo.settings.packages_desc": "Enable Repository Packages Registry",
"repo.settings.projects_desc": "Enable Projects",
"repo.settings.projects_mode_desc": "Projects Mode (which kinds of projects to show)",
@@ -3017,6 +3092,20 @@
"org.settings.license_file_found": "Found existing license file in .profile: %s",
"org.settings.license_overwrite_warning": "This will overwrite the existing %s file in your .profile repository.",
"org.settings.license_create_confirm": "This will create a LICENSE.md file in your .profile repository.",
"org.settings.ai": "AI",
"org.settings.ai.provider": "AI Provider & Configuration",
"org.settings.ai.provider_label": "Provider",
"org.settings.ai.model_label": "Model",
"org.settings.ai.api_key": "API Key",
"org.settings.ai.api_key_configured": "An API key is currently configured. Enter a new value to change it.",
"org.settings.ai.rate_limits": "Rate Limits",
"org.settings.ai.max_ops_per_hour": "Max Operations Per Hour",
"org.settings.ai.max_ops_per_hour_desc": "Set to 0 to use the system default. This limit applies per-repository within the organization.",
"org.settings.ai.allowed_ops": "Allowed Operations",
"org.settings.ai.allowed_ops_placeholder": "code-review, issue-triage, issue-response",
"org.settings.ai.allowed_ops_desc": "Comma-separated list of allowed operation types. Leave empty to allow all operations.",
"org.settings.ai.advanced": "Advanced",
"org.settings.ai.agent_mode_allowed": "Allow Agent Mode for repositories in this organization",
"org.settings.homepage_pinning": "Homepage Visibility",
"org.settings.pin_to_homepage": "Pin this organization to the homepage",
"org.settings.pin_to_homepage_help": "When enabled, this organization will be featured on the public homepage. Only administrators can change this setting.",
@@ -3164,6 +3253,7 @@
"admin.dashboard.cleanup_packages": "Clean up expired packages",
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
"admin.dashboard.server_uptime": "Server Uptime",
"admin.dashboard.current_goroutine": "Current Goroutines",
"admin.dashboard.current_memory_usage": "Current Memory Usage",
@@ -3321,6 +3411,17 @@
"admin.packages.bulk.global.partial": "Enabled global access for %d package(s), %d failed (may already exist as global)",
"admin.packages.bulk.automatch.success": "Auto-matched %d package(s) to repositories",
"admin.packages.bulk.automatch.none": "No matching repositories found for selected packages",
"admin.packages.bulk.make_private": "Make Private",
"admin.packages.bulk.make_public": "Make Public",
"admin.packages.bulk.private.enabled": "Made %d package(s) private",
"admin.packages.bulk.private.disabled": "Made %d package(s) public",
"admin.packages.visibility": "Visibility",
"admin.packages.visibility.private": "Private",
"admin.packages.visibility.public": "Public",
"admin.packages.bulk.delete": "Delete Selected",
"admin.packages.bulk.delete.confirm": "Are you sure you want to delete the selected packages and all their versions? This action cannot be undone.",
"admin.packages.bulk.delete.success": "Deleted %d package(s) with %d version(s)",
"admin.packages.bulk.delete.none": "No packages were deleted",
"admin.packages.automatch.button": "Find matching repository",
"admin.packages.automatch.match": "Match",
"admin.packages.automatch.success": "Package linked to matching repository",
@@ -3725,6 +3826,14 @@
"packages.no_metadata": "No metadata.",
"packages.empty.documentation": "For more information on the package registry, see <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the documentation</a>.",
"packages.empty.repo": "Did you upload a package, but it's not shown here? Go to <a href=\"%[1]s\">package settings</a> and link it to this repo.",
"packages.visibility.public": "Public Packages",
"packages.visibility.private": "Private Packages",
"packages.bulk.actions": "Bulk Actions",
"packages.bulk.make_private": "Make Private",
"packages.bulk.make_public": "Make Public",
"packages.bulk.selected": "Selected:",
"packages.bulk.select_all": "Select all",
"packages.bulk.no_selection": "Please select at least one package.",
"packages.registry.documentation": "For more information on the %s registry, see <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the documentation</a>.",
"packages.filter.type": "Type",
"packages.filter.type.all": "All",
@@ -4178,6 +4287,12 @@
"admin.config.enable_blogs_desc": "Enable the Blogs feature across the platform. Repos can publish blog posts visible under Explore > Blogs.",
"admin.config.blogs_in_top_nav": "Blogs in Top Navigation",
"admin.config.blogs_in_top_nav_desc": "Show a Blogs link in the site header navigation bar next to Explore",
"admin.config.show_footer_powered_by": "Show Powered By",
"admin.config.show_footer_powered_by_desc": "Show the \"Powered by GitCaddy Server\" message in the footer",
"admin.config.show_footer_licenses": "Show Licenses Link",
"admin.config.show_footer_licenses_desc": "Show the Licenses link in the footer",
"admin.config.show_footer_api": "Show API Link",
"admin.config.show_footer_api_desc": "Show the API (Swagger) link in the footer",
"admin.config.custom_home_title": "Homepage Title",
"admin.config.custom_home_title_placeholder": "Leave empty to use app name",
"admin.config.custom_home_title_help": "Custom title displayed on the homepage. Leave empty to use the default app name.",
@@ -4213,6 +4328,9 @@
"repo.settings.group_header": "Group Header",
"repo.settings.group_header_placeholder": "e.g., Core Services, Libraries, Tools",
"repo.settings.group_header_help": "Optional header for grouping this repository on the organization page",
"repo.settings.owner_display_name": "Owner Name",
"repo.settings.owner_display_name_placeholder": "e.g., John Smith, Acme Corp",
"repo.settings.owner_display_name_help": "A display name for the repository owner. Used in licenses, social cards, and other public-facing contexts instead of the username.",
"repo.settings.media_kit": "Media Kit",
"repo.settings.media_kit.style": "Social Card Style",
"repo.settings.media_kit.style_help": "Choose the visual style for your repository's share card image used in link previews.",
@@ -4293,10 +4411,21 @@
"repo.settings.subscriptions.no_clients": "No subscribers yet.",
"repo.subscribe.title": "Subscribe to %s",
"repo.subscribe.description": "This repository requires a subscription to access the source code.",
"repo.subscribe.description_with_blogs": "This repository requires a subscription to access the source code and premium blog content.",
"repo.subscribe.buy": "Subscribe",
"repo.subscribe.payment_required": "A subscription is required to view this repository's source code.",
"repo.subscribe.button": "Subscribe for Access",
"repo.subscribe.active": "You have an active subscription",
"repo.subscribe.monthly": "Monthly",
"repo.subscribe.yearly": "Yearly",
"repo.subscribe.lifetime": "Lifetime",
"repo.subscribe.per_month": "month",
"repo.subscribe.per_year": "year",
"repo.subscribe.one_time": "one-time payment",
"repo.subscribe.choose": "Choose Plan",
"repo.subscribe.pay_with_stripe": "Pay with Card",
"repo.subscribe.no_products": "No subscription plans are currently available for this repository.",
"repo.subscribe.success": "Thank you! Your subscription is now active.",
"repo.cross_promoted": "Also Check Out",
"repo.settings.license": "License",
"repo.settings.license_type": "License Type",
@@ -4323,6 +4452,18 @@
"repo.settings.license_apply": "Apply",
"repo.settings.license_overwrite_warning": "This will overwrite the existing %s file.",
"repo.settings.license_create_confirm": "This will create a LICENSE.md file in the repository.",
"repo.settings.license_acl_fields": "Auditable Commercial License Settings",
"repo.settings.license_acl_fields_desc": "These fields are required to customize the Auditable Commercial License (ACL) v1.0 for your project.",
"repo.settings.license_acl_jurisdiction": "Jurisdiction",
"repo.settings.license_acl_jurisdiction_help": "Governing law for Section 8.1 (e.g., \"the State of Delaware, United States\").",
"repo.settings.license_acl_venue": "Venue",
"repo.settings.license_acl_venue_help": "Court venue for disputes (e.g., \"Wilmington, Delaware\").",
"repo.settings.license_acl_contact": "Contact Address (optional)",
"repo.settings.license_acl_contact_help": "Where legal notices should be sent. Adds a Notices section if provided.",
"repo.settings.license_acl_license_url": "License URL (optional)",
"repo.settings.license_acl_tier_url": "Tier Schedule URL (optional)",
"repo.settings.license_acl_required": "Jurisdiction and Venue are required for the Auditable Commercial License.",
"repo.settings.license_copyright_holder": "Copyright holder",
"repo.settings.gallery": "Gallery",
"repo.settings.gallery_help": "Upload images to showcase your project. Images are stored in the .gallery folder.",
"repo.settings.gallery_upload": "Upload Images",
@@ -4348,6 +4489,9 @@
"repo.settings.hidden_folders.already_hidden": "This folder is already hidden.",
"repo.settings.dotfiles": "Dotfiles",
"repo.settings.dotfiles.hide_desc": "Hide files and folders starting with \".\" from the code browser for non-admin users",
"repo.settings.app_integration": "App Integration",
"repo.settings.app_integration.enable": "Allow anonymous issue reporting and update checks",
"repo.settings.app_integration.enable_help": "When enabled, the desktop app can submit issues and check for updates without authentication. Disable this for full access control on private repositories.",
"repo.gallery": "Gallery",
"api": "API",
"admin.config.api_header_url": "API Header Link",
@@ -4368,11 +4512,33 @@
"repo.settings.pages.saved": "Settings saved successfully",
"repo.settings.pages.brand_name": "Brand Name",
"repo.settings.pages.brand_name_help": "The name displayed on your landing page",
"repo.settings.pages.brand_logo": "Logo",
"repo.settings.pages.brand_logo_url": "Logo URL",
"repo.settings.pages.brand_logo_url_help": "URL to your logo image (SVG or PNG)",
"repo.settings.pages.brand_upload_logo": "Upload Logo",
"repo.settings.pages.brand_delete_logo": "Delete Logo",
"repo.settings.pages.brand_logo_uploaded": "Logo uploaded successfully.",
"repo.settings.pages.brand_logo_deleted": "Logo deleted.",
"repo.settings.pages.brand_upload_btn": "Upload",
"repo.settings.pages.brand_upload_help": "Upload a JPG, PNG, WebP, or GIF image (max 5 MB). Uploaded images take priority over the URL field.",
"repo.settings.pages.brand_upload_error": "Failed to upload image.",
"repo.settings.pages.brand_image_too_large": "Image is too large. Maximum size is 5 MB.",
"repo.settings.pages.brand_not_an_image": "The uploaded file is not a valid image.",
"repo.settings.pages.brand_or": "or use a URL",
"repo.settings.pages.brand_tagline": "Tagline",
"repo.settings.pages.headline": "Headline",
"repo.settings.pages.subheadline": "Subheadline",
"repo.settings.pages.hero_image": "Hero Image",
"repo.settings.pages.hero_upload": "Upload Image",
"repo.settings.pages.hero_upload_btn": "Upload",
"repo.settings.pages.hero_upload_help": "Upload a JPG, PNG, WebP, or GIF image (max 5 MB). Uploaded images take priority over the URL field.",
"repo.settings.pages.hero_upload_error": "Failed to upload image.",
"repo.settings.pages.hero_image_too_large": "Image is too large. Maximum size is 5 MB.",
"repo.settings.pages.hero_not_an_image": "The uploaded file is not a valid image.",
"repo.settings.pages.hero_image_uploaded": "Hero image uploaded successfully.",
"repo.settings.pages.hero_image_deleted": "Hero image deleted.",
"repo.settings.pages.hero_delete_image": "Delete Image",
"repo.settings.pages.hero_or": "or use a URL",
"repo.settings.pages.image_url": "Hero Image URL",
"repo.settings.pages.video_url": "Demo Video URL",
"repo.settings.pages.code_example": "Code Example",
@@ -4380,8 +4546,21 @@
"repo.settings.pages.secondary_cta": "Secondary Call to Action",
"repo.settings.pages.cta_label": "Button Label",
"repo.settings.pages.cta_url": "Button URL",
"repo.settings.pages.public_releases": "Public Releases",
"repo.settings.pages.public_releases_desc": "Allow unauthenticated users to download releases. Useful for landing pages on private repositories.",
"repo.settings.pages.hide_mobile_releases_desc": "Hide mobile platform releases (Android, iOS) from the landing page. Use this when mobile releases are distributed via app stores.",
"repo.settings.pages.google_play_id": "Google Play Store ID",
"repo.settings.pages.google_play_id_desc": "Package name from the Play Store URL (e.g. com.example.app)",
"repo.settings.pages.app_store_id": "Apple App Store ID",
"repo.settings.pages.app_store_id_desc": "App ID from the App Store URL (e.g. id123456789)",
"repo.settings.pages.stats": "Stats",
"repo.settings.pages.value_props": "Value Propositions",
"repo.settings.pages.value_props_headline": "Section Headline",
"repo.settings.pages.value_props_headline_help": "Large heading for the value propositions section (e.g., \"Built for makers\"). The small label above it is set in Section Labels.",
"repo.settings.pages.value_props_subheadline": "Section Subheadline",
"repo.settings.pages.features_headline": "Section Headline",
"repo.settings.pages.features_headline_help": "Large heading for the features section (e.g., \"Everything you need\"). The small label above it is set in Section Labels.",
"repo.settings.pages.features_subheadline": "Section Subheadline",
"repo.settings.pages.features": "Features",
"repo.settings.pages.company_logos": "Company Logos",
"repo.settings.pages.testimonials": "Testimonials",
@@ -4404,7 +4583,144 @@
"repo.settings.pages.seo_title": "SEO Title",
"repo.settings.pages.seo_description": "Meta Description",
"repo.settings.pages.seo_keywords": "Keywords",
"repo.settings.pages.seo_keywords.placeholder": "Add a keyword...",
"repo.settings.pages.seo_keywords.add": "Add",
"repo.settings.pages.og_image": "Open Graph Image URL",
"repo.settings.pages.use_media_kit_og": "Use Media Kit social card",
"repo.settings.pages.use_media_kit_og_help": "Use the social card from your Media Kit settings as the Open Graph image instead of a custom URL.",
"repo.settings.pages.brand_favicon": "Favicon",
"repo.settings.pages.brand_favicon_url": "Favicon URL",
"repo.settings.pages.brand_favicon_url_help": "URL to a custom favicon for your landing page (ICO, PNG, or SVG). Leave blank to use the default.",
"repo.settings.pages.brand_upload_favicon": "Upload Favicon",
"repo.settings.pages.brand_delete_favicon": "Delete Favicon",
"repo.settings.pages.brand_favicon_uploaded": "Favicon uploaded successfully.",
"repo.settings.pages.brand_favicon_deleted": "Favicon deleted.",
"repo.settings.pages.brand_favicon_upload_help": "Upload an ICO, PNG, WebP, or GIF favicon (max 5 MB). Uploaded favicons take priority over the URL field.",
"repo.settings.pages.navigation": "Navigation Links",
"repo.settings.pages.navigation_desc": "Control which built-in links appear in the header and footer navigation.",
"repo.settings.pages.nav_show_docs": "Show Docs link (links to wiki)",
"repo.settings.pages.nav_show_api": "Show API link (links to Swagger docs)",
"repo.settings.pages.nav_show_repository": "Show Repository link (View Source button)",
"repo.settings.pages.nav_show_releases": "Show Releases link",
"repo.settings.pages.nav_show_issues": "Show Issues link",
"repo.settings.pages.section_labels": "Section Labels",
"repo.settings.pages.section_labels_desc": "Customize the headings shown on your landing page for each section",
"repo.settings.pages.label_value_props": "Value Props Heading",
"repo.settings.pages.label_value_props_help": "Heading for the value propositions section (e.g., \"Why choose us\")",
"repo.settings.pages.label_features": "Features Heading",
"repo.settings.pages.label_features_help": "Heading for the features section (e.g., \"Capabilities\")",
"repo.settings.pages.blog_section": "Blog Section",
"repo.settings.pages.blog_enabled_desc": "Show recent blog posts on the landing page",
"repo.settings.pages.blog_headline": "Blog Headline",
"repo.settings.pages.blog_subheadline": "Blog Subheadline",
"repo.settings.pages.blog_max_posts": "Maximum Posts to Show",
"repo.settings.pages.gallery_section": "Gallery Section",
"repo.settings.pages.gallery_enabled_desc": "Show a gallery of images from your repository's .gallery folder on the landing page",
"repo.settings.pages.gallery_headline": "Gallery Headline",
"repo.settings.pages.gallery_subheadline": "Gallery Subheadline",
"repo.settings.pages.gallery_max_images": "Maximum Images to Show",
"repo.settings.pages.gallery_columns": "Grid Columns",
"repo.settings.pages.gallery_help_link": "Upload and manage gallery images in Settings > Gallery.",
"repo.settings.pages.comparison": "Comparison",
"repo.settings.pages.cross_promote_section": "Cross-Promote Section",
"repo.settings.pages.cross_promote_enabled_desc": "Show cross-promoted repositories on the landing page (only repos with landing pages enabled are shown)",
"repo.settings.pages.cross_promote_headline": "Section Headline",
"repo.settings.pages.cross_promote_subheadline": "Section Subheadline",
"repo.settings.pages.cross_promote_help": "Configure which repositories to cross-promote in Settings > Cross-Promote. Only repos with landing pages enabled will appear.",
"repo.settings.pages.comparison_section": "Comparison Section",
"repo.settings.pages.comparison_enabled_desc": "Show a feature comparison table on the landing page",
"repo.settings.pages.comparison_headline": "Headline",
"repo.settings.pages.comparison_subheadline": "Subheadline",
"repo.settings.pages.comparison_columns": "Comparison Columns",
"repo.settings.pages.comparison_columns_help": "Define three columns for your comparison table (e.g., your product vs. two competitors, or three tiers).",
"repo.settings.pages.comparison_highlight": "Highlight",
"repo.settings.pages.comparison_features": "Features & Groups",
"repo.settings.pages.comparison_features_help": "Organize features into optional groups. For each cell, enter \"true\" for a checkmark, \"false\" for an X, or any text.",
"repo.settings.pages.comparison_group_name": "Group Name",
"repo.settings.pages.comparison_help_link": "Configure comparison columns and features in Settings > Landing Pages > Comparison.",
"repo.settings.pages.ai_generate": "AI Content Generator",
"repo.settings.pages.ai_generate_desc": "Automatically generate landing page content (headline, features, stats, CTAs) from your repository's README and metadata using AI.",
"repo.settings.pages.ai_generate_button": "Generate Content with AI",
"repo.settings.pages.ai_generating": "Generating content with AI\u2026 This may take a moment.",
"repo.settings.pages.ai_generate_success": "Landing page content has been generated successfully. Review and customize it in the other tabs.",
"repo.settings.pages.ai_generate_failed": "Failed to generate content with AI. Please try again later or configure the content manually.",
"repo.settings.pages.languages": "Languages",
"repo.settings.pages.default_lang": "Default Language",
"repo.settings.pages.default_lang_help": "The primary language of your landing page content",
"repo.settings.pages.enabled_languages": "Enabled Languages",
"repo.settings.pages.enabled_languages_help": "Select which languages your landing page should support. Visitors will see a language switcher in the navigation.",
"repo.settings.pages.save_languages": "Save Language Settings",
"repo.settings.pages.languages_saved": "Language settings saved successfully.",
"repo.settings.pages.translations": "Translations",
"repo.settings.pages.ai_translate": "AI Translate",
"repo.settings.pages.ai_translate_success": "Translation has been generated successfully by AI. Review and edit as needed.",
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
"repo.settings.pages.ai_translating": "Translating with AI\u2026 This may take a moment.",
"repo.settings.pages.delete_translation": "Delete",
"repo.settings.pages.save_translation": "Save Translation",
"repo.settings.pages.translation_saved": "Translation saved successfully.",
"repo.settings.pages.translation_deleted": "Translation deleted.",
"repo.settings.pages.translation_empty": "No translation content provided.",
"repo.settings.pages.trans_headline": "Headline",
"repo.settings.pages.trans_subheadline": "Subheadline",
"repo.settings.pages.trans_primary_cta": "Primary CTA Label",
"repo.settings.pages.trans_secondary_cta": "Secondary CTA Label",
"repo.settings.pages.trans_cta_headline": "CTA Section Headline",
"repo.settings.pages.trans_cta_subheadline": "CTA Section Subheadline",
"repo.settings.pages.trans_cta_button": "CTA Button Label",
"repo.settings.pages.trans_section_brand": "Brand",
"repo.settings.pages.trans_section_hero": "Hero",
"repo.settings.pages.trans_section_stats": "Stats",
"repo.settings.pages.trans_section_value_props": "Value Propositions",
"repo.settings.pages.trans_section_features": "Features",
"repo.settings.pages.trans_section_testimonials": "Testimonials",
"repo.settings.pages.trans_section_pricing": "Pricing",
"repo.settings.pages.trans_section_cta": "Call to Action",
"repo.settings.pages.trans_section_blog": "Blog",
"repo.settings.pages.trans_section_gallery": "Gallery",
"repo.settings.pages.trans_section_comparison": "Comparison",
"repo.settings.pages.trans_section_footer": "Footer",
"repo.settings.pages.trans_section_seo": "SEO",
"repo.settings.pages.trans_section_navigation": "Navigation Labels",
"repo.settings.pages.trans_nav_label": "Label",
"repo.settings.pages.trans_brand_name": "Brand Name",
"repo.settings.pages.trans_brand_tagline": "Tagline",
"repo.settings.pages.trans_stat_value": "Value",
"repo.settings.pages.trans_stat_label": "Label",
"repo.settings.pages.trans_title": "Title",
"repo.settings.pages.trans_description": "Description",
"repo.settings.pages.trans_quote": "Quote",
"repo.settings.pages.trans_role": "Role",
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
"repo.settings.pages.trans_plan_name": "Plan Name",
"repo.settings.pages.trans_plan_period": "Period",
"repo.settings.pages.trans_plan_cta": "Plan Button",
"repo.settings.pages.trans_blog_headline": "Blog Headline",
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
"repo.settings.pages.trans_blog_cta": "Blog Button",
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
"repo.settings.pages.trans_footer_copyright": "Copyright",
"repo.settings.pages.trans_footer_link": "Link Label",
"repo.settings.pages.trans_seo_title": "SEO Title",
"repo.settings.pages.trans_seo_description": "SEO Description",
"repo.settings.pages.advanced": "Advanced",
"repo.settings.pages.static_routes": "Static Routes",
"repo.settings.pages.static_routes_desc": "Serve files directly from your repository at specific URL paths instead of rendering the landing page. Use exact paths (/badge.svg) or glob patterns (/schema/*).",
"repo.settings.pages.add_route": "Add Route",
"repo.settings.pages.redirects": "Redirects",
"repo.settings.pages.redirects_desc": "Redirect specific paths to other URLs. The source path must start with /.",
"repo.settings.pages.add_redirect": "Add Redirect",
"repo.settings.pages.custom_code": "Custom Code",
"repo.settings.pages.custom_css": "Custom CSS",
"repo.settings.pages.custom_head": "Custom Head HTML",
"repo.settings.pages.app_stores": "App Stores",
"repo.settings.pages.hide_mobile_releases": "Hide Mobile Releases",
"repo.vault": "Vault",
"repo.vault.secrets": "Secrets",
"repo.vault.new_secret": "New Secret",
@@ -4539,6 +4855,16 @@
"actions.runners.waiting_jobs": "Waiting Jobs",
"actions.runners.back_to_runners": "Back to Runners",
"actions.runners.no_waiting_jobs": "No jobs waiting for this label",
"admin.ai": "AI Status",
"admin.ai.title": "AI Service Status",
"admin.ai.sidecar_status": "Sidecar Status",
"admin.ai.config": "Configuration",
"admin.ai.stats": "Statistics",
"admin.ai.recent_operations": "Recent Operations",
"admin.ai.total_operations": "Total Operations",
"admin.ai.operations_24h": "Operations (24h)",
"admin.ai.success_rate": "Success Rate",
"admin.ai.tokens_used": "Tokens Used",
"admin.ai_learning": "AI Learning",
"admin.ai_learning.edit": "Edit Pattern",
"admin.ai_learning.total_patterns": "Total Patterns",
@@ -4751,6 +5077,13 @@
"vault.config_error_title": "Vault Not Configured",
"vault.config_error_message": "The vault encryption key has not been configured. Secrets cannot be encrypted or decrypted.",
"vault.config_error_fix": "Add MASTER_KEY to the [vault] section in app.ini or set the GITCADDY_VAULT_KEY environment variable.",
"vault.fallback_key_warning_title": "Vault Using Fallback Encryption Key",
"vault.fallback_key_warning_message": "The vault is currently using Gitea's SECRET_KEY for encryption because no dedicated vault key has been configured. If the SECRET_KEY is ever changed or lost, all vault secrets will become permanently unreadable.",
"vault.fallback_key_warning_fix": "To fix this, copy the current SECRET_KEY value and set it as MASTER_KEY in the [vault] section of app.ini, or set the GITCADDY_VAULT_KEY environment variable. This ensures vault encryption remains stable even if the SECRET_KEY changes.",
"vault.decryption_error_title": "Vault Decryption Failed",
"vault.decryption_error_message": "Unable to decrypt vault secrets. The encryption key may have been changed or is incorrect.",
"vault.decryption_error_fix": "Verify that the MASTER_KEY in the [vault] section of app.ini (or the GITCADDY_VAULT_KEY environment variable) matches the key that was used when the secrets were originally created.",
"vault.encryption_error_message": "Unable to encrypt the secret value. The vault encryption key may not be configured correctly.",
"vault.type_file": "File",
"vault.compare": "Compare",
"vault.compare_version": "Compare this version",

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -15,8 +15,8 @@
"template": "Template",
"language": "Bahasa",
"notifications": "Notifikasi",
"create_new": "Buat…",
"user_profile_and_more": "Profil dan Pengaturan…",
"create_new": "Buat\u00e2\u20ac\u00a6",
"user_profile_and_more": "Profil dan Pengaturan\u00e2\u20ac\u00a6",
"signed_in_as": "Masuk sebagai",
"toc": "Daftar Isi",
"username": "Nama Pengguna",
@@ -28,7 +28,7 @@
"passcode": "Kode Akses",
"webauthn_insert_key": "Masukkan kunci keamanan anda",
"webauthn_sign_in": "Tekan tombol pada kunci keamanan Anda. Jika kunci keamanan Anda tidak memiliki tombol, masukkan kembali.",
"webauthn_press_button": "Silakan tekan tombol pada kunci keamanan Anda…",
"webauthn_press_button": "Silakan tekan tombol pada kunci keamanan Anda\u00e2\u20ac\u00a6",
"webauthn_use_twofa": "Gunakan kode dua faktor dari telepon Anda",
"webauthn_error": "Tidak dapat membaca kunci keamanan Anda.",
"webauthn_unsupported_browser": "Browser Anda saat ini tidak mendukung WebAuthn.",
@@ -90,7 +90,7 @@
"copy_type_unsupported": "Tipe berkas ini tidak dapat disalin",
"write": "Tulis",
"preview": "Pratinjau",
"loading": "Memuat…",
"loading": "Memuat\u00e2\u20ac\u00a6",
"error_title": "Gangguan",
"error404": "Halaman yang akan kamu akses <strong>tidak dapat ditemukan</strong> atau <strong>kamu tidak memiliki akses </strong> untuk melihatnya.",
"go_back": "Kembali",
@@ -175,7 +175,7 @@
"home.password_holder": "Kata Sandi",
"home.switch_dashboard_context": "Alihkan Dasbor Konteks",
"home.my_repos": "Repositori",
"home.show_more_repos": "Tampilkan repositori lainnya…",
"home.show_more_repos": "Tampilkan repositori lainnya\u00e2\u20ac\u00a6",
"home.collaborative_repos": "Repositori Kolaboratif",
"home.my_orgs": "Organisasi Saya",
"home.my_mirrors": "Duplikat Saya",
@@ -295,7 +295,7 @@
"form.invalid_gpg_key": "Tidak dapat memverifikasi kunci GPG Anda: %s",
"form.auth_failed": "Otentikasi gagal: %v",
"form.target_branch_not_exist": "Target cabang tidak ada.",
"user.change_avatar": "Ganti avatar anda…",
"user.change_avatar": "Ganti avatar anda\u00e2\u20ac\u00a6",
"user.repositories": "Repositori",
"user.activity": "Aktivitas Publik",
"user.followers": "Pengikut",
@@ -564,18 +564,18 @@
"repo.editor.fork_before_edit": "Anda harus mencabangkan repositori ini untuk membuat atau mengusulkan perubahan pada berkas ini.",
"repo.editor.delete_this_file": "Hapus Berkas",
"repo.editor.must_have_write_access": "Anda harus punya akses tulis untuk membuat atau mengusulkan perubahan pada berkas ini.",
"repo.editor.name_your_file": "Nama berkas…",
"repo.editor.name_your_file": "Nama berkas\u00e2\u20ac\u00a6",
"repo.editor.filename_help": "Tambahkan direktori dengan mengetikkan nama direktori diikuti dengan garis miring ('/'). Hapus direktori dengan mengetikkan spasi balik pada awal bidang input.",
"repo.editor.or": "atau",
"repo.editor.cancel_lower": "Batalkan",
"repo.editor.commit_changes": "Perubahan komitmen",
"repo.editor.commit_message": "Commit message",
"repo.editor.commit_message_desc": "Tambahkan deskripsi opsional yang panjang…",
"repo.editor.commit_message_desc": "Tambahkan deskripsi opsional yang panjang\u00e2\u20ac\u00a6",
"repo.editor.commit_directly_to_this_branch": "Komitmen langsung ke <strong class=\"branch-name\">%s</strong> cabang.",
"repo.editor.create_new_branch": "Membuat <strong>new branch</strong> untuk tarik komit ini mulai permintaan.",
"repo.editor.create_new_branch_np": "Buat <strong>cabang baru</strong> untuk komit ini.",
"repo.editor.propose_file_change": "Usul perubahan berkas",
"repo.editor.new_branch_name_desc": "Nama branch baru…",
"repo.editor.new_branch_name_desc": "Nama branch baru\u00e2\u20ac\u00a6",
"repo.editor.cancel": "Membatalkan",
"repo.editor.no_changes_to_show": "Tidak ada perubahan untuk ditampilkan.",
"repo.commits.commits": "Melakukan",
@@ -816,7 +816,7 @@
"repo.settings.title": "Judul",
"repo.settings.deploy_key_content": "Konten",
"repo.settings.branches": "Cabang",
"repo.settings.choose_branch": "Pilih branch…",
"repo.settings.choose_branch": "Pilih branch\u00e2\u20ac\u00a6",
"repo.settings.tags": "Tag",
"repo.diff.browse_source": "Telusuri Sumber",
"repo.diff.parent": "orang tua",
@@ -1315,24 +1315,24 @@
"search.search": "Cari",
"search.fuzzy_tooltip": "Include results that closely match the search term",
"search.regexp": "Regexp",
"search.repo_kind": "Search repos",
"search.user_kind": "Search users",
"search.org_kind": "Search orgs",
"search.team_kind": "Search teams",
"search.code_kind": "Search code",
"search.package_kind": "Search packages",
"search.project_kind": "Search projects",
"search.branch_kind": "Search branches",
"search.tag_kind": "Search tags",
"search.commit_kind": "Search commits",
"search.runner_kind": "Search runners",
"search.issue_kind": "Search issues",
"search.pull_kind": "Search pull requests",
"search.repo_kind": "Search repos\u2026",
"search.user_kind": "Search users\u2026",
"search.org_kind": "Search orgs\u2026",
"search.team_kind": "Search teams\u2026",
"search.code_kind": "Search code\u2026",
"search.package_kind": "Search packages\u2026",
"search.project_kind": "Search projects\u2026",
"search.branch_kind": "Search branches\u2026",
"search.tag_kind": "Search tags\u2026",
"search.commit_kind": "Search commits\u2026",
"search.runner_kind": "Search runners\u2026",
"search.issue_kind": "Search issues\u2026",
"search.pull_kind": "Search pull requests\u2026",
"editor.buttons.strikethrough.tooltip": "Add strikethrough text",
"filter.string.asc": "AZ",
"filter.string.desc": "ZA",
"filter.string.asc": "A\u2013Z",
"filter.string.desc": "Z\u2013A",
"install.install": "Installation",
"install.installing_desc": "Installing now, please wait",
"install.installing_desc": "Installing now, please wait\u2026",
"install.host": "Host",
"install.db_schema": "Schema",
"install.ssl_mode": "SSL",
@@ -1409,7 +1409,7 @@
"repo.readme": "README",
"repo.mirror_address_url_invalid": "The provided URL is invalid. Make sure all components of the URL are escaped correctly.",
"repo.mirror_lfs_endpoint_desc": "Sync will attempt to use the clone URL to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">determine the LFS server</a>. You can also specify a custom endpoint if the repository LFS data is stored somewhere else.",
"repo.adopt_search": "Enter username to search for unadopted repositories (leave blank to find all)",
"repo.adopt_search": "Enter username to search for unadopted repositories\u2026 (leave blank to find all)",
"repo.desc.sha256": "SHA256",
"repo.template.webhooks": "Webhooks",
"repo.archive.title": "This repo is archived. You can view files and clone it. You cannot open issues or pull requests or push a commit.",
@@ -1419,7 +1419,7 @@
"repo.migrate_items_labels": "Labels",
"repo.migrate.github_token_desc": "You can put one or more tokens here, separated by commas, to make migrating faster by circumventing the GitHub API rate limit. WARNING: Abusing this feature may violate the service provider's policy and may lead to getting your account(s) blocked.",
"repo.migrate.permission_denied_blocked": "You cannot import from disallowed hosts. Please ask the admin to check ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS settings.",
"repo.migrate.migrating": "Migrating from <b>%s</b>",
"repo.migrate.migrating": "Migrating from <b>%s</b>\u2026",
"repo.migrate.codecommit.aws_access_key_id": "AWS Access Key ID",
"repo.migrate.codecommit.aws_secret_access_key": "AWS Secret Access Key",
"repo.migration_status": "Migration status",
@@ -1468,13 +1468,13 @@
"repo.issues.review.review": "Review",
"repo.issues.assignee.error": "Not all assignees were added, due to an unexpected error.",
"repo.compare.title": "Comparing changes",
"repo.compare.description": "Choose two branches or tags to see whats changed or to start a new pull request.",
"repo.compare.description": "Choose two branches or tags to see what\u2019s changed or to start a new pull request.",
"repo.pulls.new.description": "Discuss and review the changes in this comparison with others.",
"repo.pulls.new.already_existed": "A pull request between these branches already exists",
"repo.pulls.edit.already_changed": "Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes.",
"repo.pulls.select_commit_hold_shift_for_range": "Select commit. Hold Shift and click to select a range.",
"repo.pulls.nothing_to_compare_have_tag": "The selected branches/tags are equal.",
"repo.pulls.is_checking": "Checking for merge conflicts",
"repo.pulls.is_checking": "Checking for merge conflicts\u2026",
"repo.pulls.wrong_commit_id": "commit ID must be a commit ID on the target branch",
"repo.pulls.no_merge_not_ready": "This pull request is not ready to be merged. Check review status and status checks.",
"repo.pulls.rebase_merge_pull_request": "Rebase, then fast-forward",
@@ -1492,7 +1492,7 @@
"repo.pulls.status_checks_need_approvals_helper": "The workflow will only run after approval from the repository maintainer.",
"repo.pulls.cmd_instruction_checkout_title": "Checkout",
"repo.pulls.cmd_instruction_merge_warning": "Warning: This operation cannot merge pull request because \"autodetect manual merge\" is not enabled.",
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By\".",
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By\u2026\".",
"repo.signing.wont_sign.error": "There was an error while checking if the commit could be signed.",
"repo.signing.wont_sign.twofa": "You must have two-factor authentication enabled to have commits signed.",
"repo.wiki": "Wiki",
@@ -1548,8 +1548,8 @@
"repo.settings.visibility.private.bullet_two": "May remove the relationship between it and <strong>forks</strong>, <strong>watchers</strong>, and <strong>stars</strong>.",
"repo.settings.unarchive.text": "Unarchiving the repo will restore its ability to receive commits and pushes, as well as new issues and pull requests.",
"repo.settings.lfs": "LFS",
"repo.settings.lfs_lock_path": "Filepath to lock",
"repo.settings.lfs_pointers.found": "Found %d blob pointer(s) %d associated, %d unassociated (%d missing from store)",
"repo.settings.lfs_lock_path": "Filepath to lock\u2026",
"repo.settings.lfs_pointers.found": "Found %d blob pointer(s) \u2014 %d associated, %d unassociated (%d missing from store)",
"repo.settings.lfs_pointers.sha": "Blob SHA",
"repo.settings.lfs_pointers.oid": "OID",
"repo.settings.pages": "Landing Page",
@@ -1607,7 +1607,7 @@
"repo.branch.commits_divergence_from": "Commit divergence: %[1]d behind and %[2]d ahead of %[3]s",
"repo.branch.commits_no_divergence": "The same as branch %[1]s",
"repo.find_file.follow_symlink": "Follow this symlink to where it is pointing at",
"graphs.component_loading": "Loading %s",
"graphs.component_loading": "Loading %s\u2026",
"org.pinned_repos": "Featured Projects",
"org.public_members": "Public Members",
"org.view_all_members": "View all %d members",
@@ -1615,7 +1615,7 @@
"org.settings.email": "Contact Email Address",
"org.settings.change_visibility": "Change Visibility",
"org.settings.change_visibility_notices_1": "If the organization is converted to private, the repository stars will be removed and cannot be restored.",
"org.settings.change_visibility_notices_2": "Non-members will lose access to the organizations repositories if visibility is changed to private.",
"org.settings.change_visibility_notices_2": "Non-members will lose access to the organization\u2019s repositories if visibility is changed to private.",
"org.settings.change_visibility_success": "The visibility of organization %s has been successfully changed.",
"org.settings.visibility_desc": "Change who can view the organization and its repositories.",
"org.settings.rename": "Rename Organization",
@@ -1652,6 +1652,7 @@
"admin.dashboard.cleanup_packages": "Clean up expired packages",
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
"admin.users.bot": "Bot",
@@ -1669,11 +1670,11 @@
"admin.auths.port": "Port",
"admin.auths.ssh_keys_are_verified": "SSH keys in LDAP are considered as verified",
"admin.auths.helo_hostname": "HELO Hostname",
"admin.auths.oauth2_full_name_claim_name": "Full Name Claim Name. (Optional if set, the user's full name will always be synchronized with this claim)",
"admin.auths.oauth2_full_name_claim_name": "Full Name Claim Name. (Optional \u2014 if set, the user's full name will always be synchronized with this claim)",
"admin.auths.oauth2_ssh_public_key_claim_name": "SSH Public Key Claim Name",
"admin.auths.oauth2_admin_group": "Group Claim value for administrator users. (Optional requires claim name above)",
"admin.auths.oauth2_restricted_group": "Group Claim value for restricted users. (Optional requires claim name above)",
"admin.auths.oauth2_map_group_to_team": "Map claimed groups to Organization teams. (Optional requires claim name above)",
"admin.auths.oauth2_admin_group": "Group Claim value for administrator users. (Optional \u2014 requires claim name above)",
"admin.auths.oauth2_restricted_group": "Group Claim value for restricted users. (Optional \u2014 requires claim name above)",
"admin.auths.oauth2_map_group_to_team": "Map claimed groups to Organization teams. (Optional \u2014 requires claim name above)",
"admin.auths.sspi_auto_create_users_helper": "Allow SSPI auth method to automatically create new accounts for users that log in for the first time",
"admin.auths.sspi_strip_domain_names_helper": "If checked, domain names will be removed from logon names (e.g. \"DOMAIN\\user\" and \"user@example.org\" both will become just \"user\").",
"admin.auths.sspi_separator_replacement_helper": "The character to use to replace the separators of down-level logon names (e.g. the \\ in \"DOMAIN\\user\") and user principal names (e.g. the @ in \"user@example.org\").",
@@ -1783,7 +1784,7 @@
"actions.general.collaborative_owner_not_exist": "The collaborative owner does not exist.",
"actions.general.remove_collaborative_owner": "Remove Collaborative Owner",
"actions.general.remove_collaborative_owner_desc": "Removing a collaborative owner will prevent the repositories of the owner from accessing the actions in this repository. Continue?",
"git.filemode.changed_filemode": "%[1]s %[2]s",
"git.filemode.changed_filemode": "%[1]s \u2192 %[2]s",
"org.pinned_repos_empty_title": "Showcase your best work",
"org.pinned_repos_empty_desc": "Pin up to 6 repositories to highlight your organization's most important projects.",
"org.settings.pinned.manage": "Manage Pins",
@@ -1888,6 +1889,12 @@
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
"admin.config.explore_org_format_list": "List View",
"admin.config.explore_org_format_tiles": "Tile Cards",
"admin.config.show_footer_powered_by": "Tampilkan Powered By",
"admin.config.show_footer_powered_by_desc": "Tampilkan pesan \"Powered by GitCaddy Server\" di footer",
"admin.config.show_footer_licenses": "Tampilkan tautan lisensi",
"admin.config.show_footer_licenses_desc": "Tampilkan tautan lisensi di footer",
"admin.config.show_footer_api": "Tampilkan tautan API",
"admin.config.show_footer_api_desc": "Tampilkan tautan API (Swagger) di footer",
"repo.settings.pages.general": "General",
"repo.settings.pages.brand": "Brand",
"repo.settings.pages.hero": "Hero",
@@ -1936,6 +1943,95 @@
"repo.settings.pages.seo_description": "Meta Description",
"repo.settings.pages.seo_keywords": "Keywords",
"repo.settings.pages.og_image": "Open Graph Image URL",
"repo.settings.pages.public_releases": "Rilis publik",
"repo.settings.pages.public_releases_desc": "Izinkan pengguna yang tidak terautentikasi mengunduh rilis. Berguna untuk halaman arahan repositori privat.",
"repo.settings.pages.brand_favicon_url": "URL Favicon",
"repo.settings.pages.brand_favicon_url_help": "URL favicon kustom untuk halaman arahan Anda (ICO, PNG, atau SVG). Biarkan kosong untuk menggunakan default.",
"repo.settings.pages.navigation": "Tautan navigasi",
"repo.settings.pages.navigation_desc": "Kontrol tautan bawaan mana yang muncul di navigasi header dan footer.",
"repo.settings.pages.nav_show_docs": "Tampilkan tautan Docs (menaut ke wiki)",
"repo.settings.pages.nav_show_api": "Tampilkan tautan API (menaut ke dokumentasi Swagger)",
"repo.settings.pages.nav_show_repository": "Tampilkan tautan repositori (tombol Lihat kode sumber)",
"repo.settings.pages.nav_show_releases": "Tampilkan tautan rilis",
"repo.settings.pages.nav_show_issues": "Tampilkan tautan isu",
"repo.settings.pages.section_labels": "Label bagian",
"repo.settings.pages.section_labels_desc": "Sesuaikan judul yang ditampilkan di halaman arahan untuk setiap bagian",
"repo.settings.pages.label_value_props": "Judul proposisi nilai",
"repo.settings.pages.label_value_props_help": "Judul untuk bagian proposisi nilai (mis., \"Mengapa memilih kami\")",
"repo.settings.pages.label_features": "Judul fitur",
"repo.settings.pages.label_features_help": "Judul untuk bagian fitur (mis., \"Kemampuan\")",
"repo.settings.pages.blog_section": "Bagian blog",
"repo.settings.pages.blog_enabled_desc": "Tampilkan postingan blog terbaru di halaman arahan",
"repo.settings.pages.blog_headline": "Judul blog",
"repo.settings.pages.blog_subheadline": "Subjudul blog",
"repo.settings.pages.blog_max_posts": "Jumlah maksimal postingan yang ditampilkan",
"repo.settings.pages.ai_generate": "Generator konten AI",
"repo.settings.pages.ai_generate_desc": "Buat konten halaman arahan secara otomatis (judul, fitur, statistik, CTA) dari README dan metadata repositori Anda menggunakan AI.",
"repo.settings.pages.ai_generate_button": "Buat konten dengan AI",
"repo.settings.pages.ai_generate_success": "Konten halaman arahan telah berhasil dibuat. Tinjau dan sesuaikan di tab lainnya.",
"repo.settings.pages.ai_generate_failed": "Pembuatan konten AI gagal. Coba lagi nanti atau konfigurasikan konten secara manual.",
"repo.settings.pages.languages": "Bahasa",
"repo.settings.pages.default_lang": "Bahasa default",
"repo.settings.pages.default_lang_help": "Bahasa utama konten halaman arahan Anda",
"repo.settings.pages.enabled_languages": "Bahasa yang diaktifkan",
"repo.settings.pages.enabled_languages_help": "Pilih bahasa yang harus didukung halaman arahan Anda. Pengunjung akan melihat pemilih bahasa di navigasi.",
"repo.settings.pages.save_languages": "Simpan pengaturan bahasa",
"repo.settings.pages.languages_saved": "Pengaturan bahasa berhasil disimpan.",
"repo.settings.pages.translations": "Terjemahan",
"repo.settings.pages.ai_translate": "Terjemahan AI",
"repo.settings.pages.ai_translate_success": "Terjemahan telah berhasil dibuat oleh AI. Tinjau dan edit sesuai kebutuhan.",
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
"repo.settings.pages.delete_translation": "Hapus",
"repo.settings.pages.save_translation": "Simpan terjemahan",
"repo.settings.pages.translation_saved": "Terjemahan berhasil disimpan.",
"repo.settings.pages.translation_deleted": "Terjemahan dihapus.",
"repo.settings.pages.translation_empty": "Tidak ada konten terjemahan yang diberikan.",
"repo.settings.pages.trans_headline": "Judul",
"repo.settings.pages.trans_subheadline": "Subjudul",
"repo.settings.pages.trans_primary_cta": "Label CTA utama",
"repo.settings.pages.trans_secondary_cta": "Label CTA sekunder",
"repo.settings.pages.trans_cta_headline": "Judul bagian CTA",
"repo.settings.pages.trans_cta_subheadline": "Subjudul bagian CTA",
"repo.settings.pages.trans_cta_button": "Label tombol CTA",
"repo.settings.pages.trans_section_brand": "Brand",
"repo.settings.pages.trans_section_hero": "Hero",
"repo.settings.pages.trans_section_stats": "Stats",
"repo.settings.pages.trans_section_value_props": "Value Propositions",
"repo.settings.pages.trans_section_features": "Features",
"repo.settings.pages.trans_section_testimonials": "Testimonials",
"repo.settings.pages.trans_section_pricing": "Pricing",
"repo.settings.pages.trans_section_cta": "Call to Action",
"repo.settings.pages.trans_section_blog": "Blog",
"repo.settings.pages.trans_section_gallery": "Gallery",
"repo.settings.pages.trans_section_comparison": "Comparison",
"repo.settings.pages.trans_section_footer": "Footer",
"repo.settings.pages.trans_section_seo": "SEO",
"repo.settings.pages.trans_brand_name": "Brand Name",
"repo.settings.pages.trans_brand_tagline": "Tagline",
"repo.settings.pages.trans_stat_value": "Value",
"repo.settings.pages.trans_stat_label": "Label",
"repo.settings.pages.trans_title": "Title",
"repo.settings.pages.trans_description": "Description",
"repo.settings.pages.trans_quote": "Quote",
"repo.settings.pages.trans_role": "Role",
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
"repo.settings.pages.trans_plan_name": "Plan Name",
"repo.settings.pages.trans_plan_period": "Period",
"repo.settings.pages.trans_plan_cta": "Plan Button",
"repo.settings.pages.trans_blog_headline": "Blog Headline",
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
"repo.settings.pages.trans_blog_cta": "Blog Button",
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
"repo.settings.pages.trans_footer_copyright": "Copyright",
"repo.settings.pages.trans_footer_link": "Link Label",
"repo.settings.pages.trans_seo_title": "SEO Title",
"repo.settings.pages.trans_seo_description": "SEO Description",
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
@@ -2221,5 +2317,68 @@
"repo.view_file": "View File",
"actions.runners.waiting_jobs": "Pekerjaan Menunggu",
"actions.runners.back_to_runners": "Kembali ke Runner",
"actions.runners.no_waiting_jobs": "Tidak ada pekerjaan yang menunggu untuk label ini"
}
"actions.runners.no_waiting_jobs": "Tidak ada pekerjaan yang menunggu untuk label ini",
"repo.settings.pages.cross_promote_section": "Bagian promosi silang",
"repo.settings.pages.cross_promote_enabled_desc": "Tampilkan repositori promosi silang di halaman arahan",
"repo.settings.pages.cross_promote_headline": "Judul bagian",
"repo.settings.pages.cross_promote_subheadline": "Subjudul bagian",
"repo.settings.pages.cross_promote_help": "Konfigurasikan repositori di Pengaturan > Promosi Silang.",
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files": "Kecualikan file dan folder tersembunyi",
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files_desc": "Jangan dorong file/folder yang dimulai dengan \".\" atau ditandai sebagai tersembunyi ke mirror jarak jauh",
"repo.settings.pages.value_props_headline": "Judul bagian",
"repo.settings.pages.value_props_headline_help": "Judul besar untuk bagian proposisi nilai. Label kecil di atasnya diatur di Label bagian.",
"repo.settings.pages.value_props_subheadline": "Subjudul bagian",
"repo.settings.pages.features_headline": "Judul bagian",
"repo.settings.pages.features_headline_help": "Judul besar untuk bagian fitur. Label kecil di atasnya diatur di Label bagian.",
"repo.settings.pages.features_subheadline": "Subjudul bagian",
"repo.settings.license_acl_fields": "Pengaturan Auditable Commercial License",
"repo.settings.license_acl_fields_desc": "Field-field ini diperlukan untuk menyesuaikan Auditable Commercial License (ACL) v1.0 untuk proyek Anda.",
"repo.settings.license_acl_jurisdiction": "Yurisdiksi",
"repo.settings.license_acl_jurisdiction_help": "Hukum yang berlaku untuk Bagian 8.1.",
"repo.settings.license_acl_venue": "Tempat",
"repo.settings.license_acl_venue_help": "Tempat pengadilan untuk sengketa.",
"repo.settings.license_acl_contact": "Alamat kontak (opsional)",
"repo.settings.license_acl_contact_help": "Ke mana pemberitahuan hukum harus dikirim.",
"repo.settings.license_acl_license_url": "URL lisensi (opsional)",
"repo.settings.license_acl_tier_url": "URL jadwal tingkatan (opsional)",
"repo.settings.license_acl_required": "Yurisdiksi dan Tempat diperlukan untuk Auditable Commercial License.",
"repo.settings.mirror_settings.push_mirror.reset_tooltip": "Reset mirror ke commit tertentu",
"repo.settings.mirror_settings.push_mirror.reset_title": "Reset push mirror ke commit",
"repo.settings.mirror_settings.push_mirror.reset_warning_title": "Tindakan destruktif",
"repo.settings.mirror_settings.push_mirror.reset_warning_body": "Ini akan force-push commit terpilih ke remote dan menulis ulang riwayat branch di sana. Auto-sync akan dijeda.",
"repo.settings.mirror_settings.push_mirror.reset_branch": "Branch di remote",
"repo.settings.mirror_settings.push_mirror.reset_branch_help": "Branch di mirror remote yang akan direset.",
"repo.settings.mirror_settings.push_mirror.reset_commit": "SHA commit",
"repo.settings.mirror_settings.push_mirror.reset_commit_help": "SHA lengkap dari commit di repositori ini untuk dipush ke remote.",
"repo.settings.mirror_settings.push_mirror.reset_confirm": "Force-push dan jeda sinkronisasi",
"repo.settings.mirror_settings.push_mirror.reset_required": "Branch dan SHA commit diperlukan.",
"repo.settings.mirror_settings.push_mirror.reset_failed": "Gagal mereset mirror: %s",
"repo.settings.mirror_settings.push_mirror.reset_success": "Reset mirror selesai. Auto-sync telah dijeda.",
"repo.settings.mirror_settings.push_mirror.reset_all_button": "Reset semua mirror ke commit",
"repo.settings.mirror_settings.push_mirror.reset_all_button_help": "Force-push commit yang sama ke setiap push mirror sekaligus dan jeda semua sinkronisasi otomatis.",
"repo.settings.mirror_settings.push_mirror.reset_all_title": "Reset semua push mirror ke commit",
"repo.settings.mirror_settings.push_mirror.reset_all_warning_title": "Tindakan destruktif \u2014 memengaruhi semua mirror",
"repo.settings.mirror_settings.push_mirror.reset_all_warning_body": "Ini akan force-push commit terpilih ke branch bernama di setiap push mirror.",
"repo.settings.mirror_settings.push_mirror.reset_all_confirm": "Force-push semua mirror dan jeda sinkronisasi",
"repo.settings.mirror_settings.push_mirror.reset_all_success": "Reset selesai. %d mirror diperbarui.",
"repo.settings.mirror_settings.push_mirror.reset_all_partial": "Reset sebagian: %d berhasil, %d gagal. Kesalahan: %s",
"repo.settings.repo_reset.title": "Reset riwayat repositori",
"repo.settings.repo_reset.desc": "Memindahkan HEAD branch ke commit tertentu dan force-push ke semua mirror.",
"repo.settings.repo_reset.button": "Reset ke commit",
"repo.settings.repo_reset.warning_title": "Ini menulis ulang riwayat repositori",
"repo.settings.repo_reset.warning_body": "Tindakan ini akan:",
"repo.settings.repo_reset.warning_branch": "Memindahkan HEAD branch di server ini ke commit terpilih.",
"repo.settings.repo_reset.warning_collaborators": "Siapa pun yang telah meng-clone repositori harus meng-clone ulang.",
"repo.settings.repo_reset.warning_mirrors": "Semua push mirror akan dipaksa diperbarui dan sinkronisasi otomatisnya akan dijeda.",
"repo.settings.repo_reset.branch": "Branch untuk direset",
"repo.settings.repo_reset.commit": "SHA commit",
"repo.settings.repo_reset.confirm_name": "Untuk konfirmasi, ketik nama repositori: %s",
"repo.settings.repo_reset.confirm": "Reset repositori",
"repo.settings.repo_reset.required": "Branch dan SHA commit diperlukan.",
"repo.settings.repo_reset.confirm_mismatch": "Konfirmasi nama repositori tidak cocok.",
"repo.settings.repo_reset.commit_not_found": "Commit %s tidak ditemukan.",
"repo.settings.repo_reset.branch_not_found": "Branch %s tidak ada.",
"repo.settings.repo_reset.failed": "Reset gagal: %s",
"repo.settings.repo_reset.success": "Reset selesai.",
"repo.settings.repo_reset.partial": "Repositori direset secara lokal, tetapi beberapa mirror gagal: %s"
}

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -14,8 +14,8 @@
"language": "Taal",
"notifications": "Meldingen",
"active_stopwatch": "Actieve Tijd Tracker",
"create_new": "Maken…",
"user_profile_and_more": "Profiel en instellingen…",
"create_new": "Maken\u00e2\u20ac\u00a6",
"user_profile_and_more": "Profiel en instellingen\u00e2\u20ac\u00a6",
"signed_in_as": "Aangemeld als",
"toc": "Inhoudsopgave",
"licenses": "Licenties",
@@ -30,7 +30,7 @@
"passcode": "PIN",
"webauthn_insert_key": "Voer uw beveiligingssleutel in",
"webauthn_sign_in": "Druk op de knop van uw beveiligingssleutel. Als uw beveiligingssleutel geen knop heeft, voeg deze dan opnieuw in.",
"webauthn_press_button": "Druk alstublieft op de knop van uw beveiligingssleutel…",
"webauthn_press_button": "Druk alstublieft op de knop van uw beveiligingssleutel\u00e2\u20ac\u00a6",
"webauthn_use_twofa": "Gebruik een twee-factor code van uw telefoon",
"webauthn_error": "Kon uw beveiligingssleutel niet lezen.",
"webauthn_unsupported_browser": "Uw browser ondersteunt momenteel geen WebAuthn.",
@@ -73,11 +73,11 @@
"edit": "Bewerken",
"enabled": "Ingeschakeld",
"disabled": "Uitgeschakeld",
"copy": "Kopiëren",
"copy": "Kopi\u00ebren",
"copy_url": "Kopieer URL",
"copy_branch": "Kopieer branchnaam",
"copy_success": "Gekopieerd!",
"copy_error": "Kopiëren mislukt",
"copy_error": "Kopi\u00c3\u00abren mislukt",
"write": "Schrijven",
"preview": "Voorbeeld",
"loading": "Laden...",
@@ -96,12 +96,12 @@
"error.occurred": "Er is een fout opgetreden",
"error.not_found": "Het doel kon niet worden gevonden.",
"error.network_error": "Netwerk fout",
"startpage.app_desc": "Geïntegreerd in je workflow",
"startpage.app_desc": "Ge\u00c3\u00afntegreerd in je workflow",
"startpage.install": "Overal Implementeren",
"startpage.lightweight": "Razendsnel",
"startpage.lightweight_desc": "Minimale voetafdruk, maximale prestaties. GitCaddy draait efficiënt op alles, van Raspberry Pi tot enterprise servers.",
"startpage.lightweight_desc": "Minimale voetafdruk, maximale prestaties. GitCaddy draait effici\u00c3\u00abnt op alles, van Raspberry Pi tot enterprise servers.",
"install.install": "Installatie",
"install.title": "Initiële configuratie",
"install.title": "Initi\u00c3\u00able configuratie",
"install.docker_helper": "Als je GitCaddy draait in Docker, Lees eerst de <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">documentatie</a> voordat je een instelling aanpast.",
"install.require_db_desc": "GitCaddy vereist MySQL, PostgreSQL, MSSQL, SQLite3 of TiDB (MySQL protocol).",
"install.db_title": "Database-instellingen",
@@ -162,7 +162,7 @@
"install.enable_captcha": "Registratie CAPTCHA inschakelen",
"install.enable_captcha_popup": "Vereis captcha validatie voor zelf-registratie van gebruiker.",
"install.require_sign_in_view": "Vereis inloggen om pagina's te kunnen bekijken",
"install.admin_setting_desc": "Het creëren van een administrator-account is optioneel. De eerste geregistreerde gebruiker wordt automatisch de beheerder.",
"install.admin_setting_desc": "Het cre\u00c3\u00abren van een administrator-account is optioneel. De eerste geregistreerde gebruiker wordt automatisch de beheerder.",
"install.admin_title": "Instellingen beheerdersaccount",
"install.admin_name": "Admin gebruikersnaam",
"install.admin_password": "Wachtwoord",
@@ -170,7 +170,7 @@
"install.admin_email": "E-mailadres",
"install.install_btn_confirm": "Installeer GitCaddy",
"install.test_git_failed": "Git test niet gelukt: 'git' commando %v",
"install.sqlite3_not_available": "Deze GitCaddy-versie biedt geen ondersteuning voor SQLite3. Download de officiële build van %s (niet de versie van de 'gobuild').",
"install.sqlite3_not_available": "Deze GitCaddy-versie biedt geen ondersteuning voor SQLite3. Download de offici\u00c3\u00able build van %s (niet de versie van de 'gobuild').",
"install.invalid_db_setting": "De database instelling zijn niet correct: %v",
"install.invalid_repo_path": "Het pad van de hoofdmap van de repository is ongeldig: %v",
"install.invalid_app_data_path": "Ongeldig app-gegevenspad: %v",
@@ -192,7 +192,7 @@
"home.uname_holder": "Gebruikersnaam of e-mailadres",
"home.password_holder": "Wachtwoord",
"home.switch_dashboard_context": "Wissel voorpaginacontext",
"home.show_more_repos": "Toon meer repositories…",
"home.show_more_repos": "Toon meer repositories\u00e2\u20ac\u00a6",
"home.collaborative_repos": "Gedeelde repositories",
"home.my_orgs": "Mijn organisaties",
"home.my_mirrors": "Mijn spiegels",
@@ -204,16 +204,16 @@
"home.show_both_archived_unarchived": "Toont zowel gearchiveerd als niet-gearchiveerd",
"home.show_only_archived": "Toon alleen gearchiveerd",
"home.show_only_unarchived": "Toon alleen niet gearchiveerd",
"home.show_private": "Privé",
"home.show_both_private_public": "Toon zowel openbaar als privé",
"home.show_only_private": "Toon alleen privé",
"home.show_private": "Priv\u00c3\u00a9",
"home.show_both_private_public": "Toon zowel openbaar als priv\u00c3\u00a9",
"home.show_only_private": "Toon alleen priv\u00c3\u00a9",
"home.show_only_public": "Toon alleen opbenbaar",
"home.issues.in_your_repos": "In uw repositories",
"explore.users": "Gebruikers",
"explore.organizations": "Organisaties",
"explore.packages": "Pakketten",
"explore.packages.empty.description": "Er zijn nog geen openbare of globale pakketten beschikbaar.",
"explore.code_last_indexed_at": "Laatst geïndexeerd %s",
"explore.code_last_indexed_at": "Laatst ge\u00c3\u00afndexeerd %s",
"auth.create_new_account": "Account registreren",
"auth.disable_register_prompt": "Registratie is uitgeschakeld. Neem alstublieft contact op met de pagina beheerder.",
"auth.disable_register_mail": "E-mailbevestiging voor registratie is uitgeschakeld.",
@@ -236,7 +236,7 @@
"auth.reset_password_helper": "Account herstellen",
"auth.password_too_short": "De lengte van uw wachtwoord moet tenminste %d karakters zijn.",
"auth.non_local_account": "Non-lokale gebruikers mogen hun wachtwoord niet wijzigen via de webinterface.",
"auth.verify": "Verifiëren",
"auth.verify": "Verifi\u00c3\u00abren",
"auth.scratch_code": "Eenmalige code",
"auth.use_scratch_code": "Gebruik een eenmalige code",
"auth.twofa_scratch_used": "Je hebt je eenmalige code gebruikt. Je wordt omgeleid naar de tweeledige-authenticatie instellingen pagina zodat je de inschrijving van het apparaat kan verwijderen of een nieuwe eenmalige code kan genereren.",
@@ -265,7 +265,7 @@
"auth.sspi_auth_failed": "SSPI-authenticatie mislukt",
"auth.password_pwned_err": "Kan het verzoek om HaveIBeenPwned niet voltooien",
"mail.view_it_on": "Bekijk het op %s",
"mail.link_not_working_do_paste": "Werkt dit niet? Probeer het te kopiëren en te plakken naar uw browser.",
"mail.link_not_working_do_paste": "Werkt dit niet? Probeer het te kopi\u00c3\u00abren en te plakken naar uw browser.",
"mail.hi_user_x": "Hoi <b>%s</b>,",
"mail.activate_account": "Activeer uw account",
"mail.activate_account.title": "%s, activeer alstublieft uw account",
@@ -344,36 +344,36 @@
"form.username_been_taken": "Deze naam is al in gebruik.",
"form.username_change_not_local_user": "Niet-lokale gebruikers mogen hun gebruikersnaam niet wijzigen.",
"form.repo_name_been_taken": "De repository-naam wordt al gebruikt.",
"form.repository_force_private": "Forceer privé is ingeschakeld: privé repositories kunnen niet openbaar worden gemaakt.",
"form.repository_force_private": "Forceer priv\u00c3\u00a9 is ingeschakeld: priv\u00c3\u00a9 repositories kunnen niet openbaar worden gemaakt.",
"form.repository_files_already_exist": "Er bestaan al bestanden voor deze repository. Neem contact op met de systeembeheerder.",
"form.repository_files_already_exist.delete": "Er bestaan al bestanden voor deze repository. U moet deze verwijderen.",
"form.repository_files_already_exist.adopt_or_delete": "Er bestaan al bestanden voor deze repository. Adopteer of verwijder deze.",
"form.visit_rate_limit": "Bezoeklimiet op afstand gerichter.",
"form.org_name_been_taken": "Naam van de organisatie wordt al gebruikt.",
"form.team_name_been_taken": "De teamnaam is al in gebruik.",
"form.team_no_units_error": "Toegang verlenen tot ten minste één repository sectie.",
"form.team_no_units_error": "Toegang verlenen tot ten minste \u00c3\u00a9\u00c3\u00a9n repository sectie.",
"form.email_been_used": "Het emailadres is al in gebruik.",
"form.email_invalid": "Het e-mailadres is ongeldig.",
"form.username_password_incorrect": "Gebruikersnaam of wachtwoord is onjuist.",
"form.password_complexity": "Wachtwoord voldoet niet aan complexiteit eisen:",
"form.password_lowercase_one": "Minstens één kleine letter",
"form.password_uppercase_one": "Minstens één hoofdletter",
"form.password_digit_one": "Minstens één cijfer",
"form.password_special_one": "Minstens één speciaal teken (interpunctie, haakjes, aanhalingstekens, etc.)",
"form.password_lowercase_one": "Minstens \u00c3\u00a9\u00c3\u00a9n kleine letter",
"form.password_uppercase_one": "Minstens \u00c3\u00a9\u00c3\u00a9n hoofdletter",
"form.password_digit_one": "Minstens \u00c3\u00a9\u00c3\u00a9n cijfer",
"form.password_special_one": "Minstens \u00c3\u00a9\u00c3\u00a9n speciaal teken (interpunctie, haakjes, aanhalingstekens, etc.)",
"form.enterred_invalid_repo_name": "De repository-naam die u hebt ingevoerd is niet correct.",
"form.enterred_invalid_org_name": "De organizatienaam die u hebt ingevoerd is niet correct.",
"form.enterred_invalid_owner_name": "De nieuwe eigenaarnaam is niet geldig.",
"form.enterred_invalid_password": "Het ingevoerde wachtwoord is onjuist.",
"form.user_not_exist": "De gebruiker bestaat niet.",
"form.team_not_exist": "Dit team bestaat niet.",
"form.last_org_owner": "Je kunt de laatste eigenaar van een organisatie niet verwijderen. Er moet er minimaal één eigenaar in een organisatie zitten.",
"form.last_org_owner": "Je kunt de laatste eigenaar van een organisatie niet verwijderen. Er moet er minimaal \u00c3\u00a9\u00c3\u00a9n eigenaar in een organisatie zitten.",
"form.cannot_add_org_to_team": "Een organisatie kan niet worden toegevoegd als een teamlid.",
"form.invalid_ssh_key": "Kan de SSH-sleutel niet verifiëren: %s",
"form.invalid_gpg_key": "Kan de GPG-sleutel niet verifiëren: %s",
"form.invalid_ssh_key": "Kan de SSH-sleutel niet verifi\u00c3\u00abren: %s",
"form.invalid_gpg_key": "Kan de GPG-sleutel niet verifi\u00c3\u00abren: %s",
"form.invalid_ssh_principal": "Ongeldige verantwoordelijke: %s",
"form.auth_failed": "Verificatie mislukt: %v",
"form.target_branch_not_exist": "Doel branch bestaat niet",
"user.change_avatar": "Wijzig je profielfoto…",
"user.change_avatar": "Wijzig je profielfoto\u00e2\u20ac\u00a6",
"user.repositories": "Repository's",
"user.activity": "Activiteit",
"user.followers": "Volgers",
@@ -430,7 +430,7 @@
"settings.enable_custom_avatar": "Aangepaste avatar inschakelen",
"settings.choose_new_avatar": "Kies een nieuwe avatar",
"settings.delete_current_avatar": "Verwijder huidige avatar",
"settings.uploaded_avatar_not_a_image": "Het geüploade bestand is geen afbeelding.",
"settings.uploaded_avatar_not_a_image": "Het ge\u00c3\u00bcploade bestand is geen afbeelding.",
"settings.update_avatar_success": "Je avatar is bijgewerkt.",
"settings.update_user_avatar_success": "De avatar van de gebruiker is bijgewerkt.",
"settings.change_password": "Wachtwoord bijwerken",
@@ -473,7 +473,7 @@
"settings.add_key": "Sleutel toevoegen",
"settings.ssh_desc": "Deze publieke SSH sleutels worden geassocieerd met uw account. De bijbehorende private sleutels geven volledige toegang toe tot je repositories.",
"settings.principal_desc": "Deze SSH-certificaatverantwoordelijken zijn gekoppeld aan uw account en geven volledige toegang tot uw repositories.",
"settings.gpg_desc": "Deze publieke GPG-sleutels zijn verbonden met je account. Houd je privé-sleutels veilig, omdat hiermee commits kunnen worden ondertekend.",
"settings.gpg_desc": "Deze publieke GPG-sleutels zijn verbonden met je account. Houd je priv\u00c3\u00a9-sleutels veilig, omdat hiermee commits kunnen worden ondertekend.",
"settings.ssh_helper": "<strong>Weet u niet hoe?</strong> Lees dan onze handleiding voor het <a href=\"%s\"> genereren van SSH sleutels</a> of voor <a href=\"%s\"> algemene SSH</a> problemen.",
"settings.gpg_helper": "<strong>Hulp nodig?</strong> Neem een kijkje op de GitHub handleiding <a href=\"%s\">over GPG</a>.",
"settings.add_new_key": "SSH sleutel toevoegen",
@@ -489,15 +489,15 @@
"settings.gpg_key_matched_identities": "Overeenkomende identiteiten:",
"settings.gpg_key_matched_identities_long": "De ingesloten identiteiten in deze sleutel komen overeen met de geactiveerde e-mailadressen voor deze gebruiker. Commits die overeenkomen met deze e-mailadressen kunnen worden geverifieerd met deze sleutel.",
"settings.gpg_key_verified": "Geverifieerde sleutel",
"settings.gpg_key_verified_long": "Sleutel is geverifieerd met een token en kan worden gebruikt om commits te verifiëren die overeenkomen met alle geactiveerde e-mailadressen voor deze gebruiker naast de bijbehorende identiteiten voor deze sleutel.",
"settings.gpg_key_verify": "Verifiëren",
"settings.gpg_key_verified_long": "Sleutel is geverifieerd met een token en kan worden gebruikt om commits te verifi\u00c3\u00abren die overeenkomen met alle geactiveerde e-mailadressen voor deze gebruiker naast de bijbehorende identiteiten voor deze sleutel.",
"settings.gpg_key_verify": "Verifi\u00c3\u00abren",
"settings.gpg_token_required": "U moet een handtekening opgeven voor de onderstaande token",
"settings.gpg_token_help": "U kunt een handtekening genereren met:",
"settings.gpg_token_signature": "Gepantserde GPG-handtekening",
"settings.key_signature_gpg_placeholder": "Begint met '-----BEGIN PGP SIGNATURE-----'",
"settings.ssh_key_verified": "Geverifieerde sleutel",
"settings.ssh_key_verified_long": "Sleutel is geverifieerd met een token en kan worden gebruikt om commits te verifiëren die overeenkomen met alle geactiveerde e-mailadressen voor deze gebruiker.",
"settings.ssh_key_verify": "Verifiëren",
"settings.ssh_key_verified_long": "Sleutel is geverifieerd met een token en kan worden gebruikt om commits te verifi\u00c3\u00abren die overeenkomen met alle geactiveerde e-mailadressen voor deze gebruiker.",
"settings.ssh_key_verify": "Verifi\u00c3\u00abren",
"settings.ssh_token_required": "U moet een handtekening opgeven voor het onderstaande token",
"settings.ssh_token_help": "U kunt een handtekening genereren door het volgende:",
"settings.ssh_token_signature": "Gepantserde SSH handtekening",
@@ -597,7 +597,7 @@
"settings.visibility": "Gebruiker zichtbaarheid",
"settings.visibility.public": "Openbaar",
"settings.visibility.limited": "Beperkt",
"settings.visibility.private": "Privé",
"settings.visibility.private": "Priv\u00c3\u00a9",
"repo.owner": "Eigenaar",
"repo.owner_helper": "Sommige organisaties kunnen niet worden weergegeven in de dropdown vanwege een limiet op het maximale aantal repositories.",
"repo.repo_name": "Naam van repository",
@@ -608,7 +608,7 @@
"repo.template_description": "Sjabloon repositories laten gebruikers nieuwe repositories genereren met dezelfde directory structuur, bestanden en optionele instellingen.",
"repo.visibility": "Zichtbaarheid",
"repo.visibility_description": "Alleen de eigenaar of de organisatielid kan het zien als ze rechten hebben.",
"repo.visibility_helper_forced": "De sitebeheerder verplicht alle repositories om privé te zijn.",
"repo.visibility_helper_forced": "De sitebeheerder verplicht alle repositories om priv\u00c3\u00a9 te zijn.",
"repo.visibility_fork_helper": "(Verandering van deze waarde zal van invloed zijn op alle forks)",
"repo.clone_helper": "Heb je hulp nodig om te clonen? Bekijk dan de <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">handleiding</a>.",
"repo.fork_repo": "Repository forken",
@@ -671,18 +671,18 @@
"repo.blame_prior": "Bekijk de schuld voorafgaand aan deze verandering",
"repo.transfer.accept": "Accepteer overdracht",
"repo.transfer.reject": "Overdracht afwijzen",
"repo.desc.private": "Privé",
"repo.desc.private": "Priv\u00c3\u00a9",
"repo.desc.public": "Openbaar",
"repo.desc.template": "Sjabloon",
"repo.desc.internal": "Interne",
"repo.desc.archived": "Gearchiveerd",
"repo.template.items": "Sjabloon items",
"repo.template.git_content": "Git inhoud (standaard Branch)",
"repo.template.git_hooks_tooltip": "Je bent momenteel niet in staat om Git Hooks één keer te wijzigen of te verwijderen. Selecteer deze optie alleen als je de sjabloonrepository vertrouwt.",
"repo.template.git_hooks_tooltip": "Je bent momenteel niet in staat om Git Hooks \u00c3\u00a9\u00c3\u00a9n keer te wijzigen of te verwijderen. Selecteer deze optie alleen als je de sjabloonrepository vertrouwt.",
"repo.template.topics": "Onderwerpen",
"repo.template.avatar": "Profielfoto",
"repo.template.issue_labels": "Issuelabels",
"repo.template.one_item": "Moet ten minste één sjabloon selecteren",
"repo.template.one_item": "Moet ten minste \u00c3\u00a9\u00c3\u00a9n sjabloon selecteren",
"repo.template.invalid": "Moet een sjabloon repository selecteren",
"repo.archive.issue.nocomment": "Deze repo is gearchiveerd. U kunt niet reageren op problemen.",
"repo.archive.pull.nocomment": "Deze repo is gearchiveerd. U kunt niet reageren op pull requests.",
@@ -773,7 +773,7 @@
"repo.invisible_runes_line": "Deze lijn heeft onzichtbare unicode karakters",
"repo.ambiguous_runes_line": "Deze lijn heeft dubbelzinnige unicode karakters",
"repo.unescape_control_characters": "Onescape",
"repo.file_copy_permalink": "Permalink kopiëren",
"repo.file_copy_permalink": "Permalink kopi\u00c3\u00abren",
"repo.view_git_blame": "Bekijk Git Blame",
"repo.video_not_supported_in_browser": "Je browser ondersteunt de HTML5 'video'-tag niet.",
"repo.audio_not_supported_in_browser": "Je browser ondersteunt de HTML5 'audio'-tag niet.",
@@ -799,7 +799,7 @@
"repo.editor.fork_before_edit": "Je moet deze repository forken om veranderingen te maken of voor te stellen.",
"repo.editor.delete_this_file": "Verwijder bestand",
"repo.editor.must_have_write_access": "U moet schrijftoegang hebben om aanpassingen te maken of voor te stellen in dit bestand.",
"repo.editor.name_your_file": "Bestandsnaam…",
"repo.editor.name_your_file": "Bestandsnaam\u00e2\u20ac\u00a6",
"repo.editor.filename_help": "Voeg een map toe door zijn naam te typen, gevolgd door een slash ('/'). Verwijder een map door op backspace te drukken aan het begin van het tekstveld.",
"repo.editor.or": "of",
"repo.editor.cancel_lower": "Annuleer",
@@ -809,13 +809,13 @@
"repo.editor.patching": "Patchen:",
"repo.editor.new_patch": "Nieuwe Patch",
"repo.editor.commit_message": "Commit-bericht",
"repo.editor.commit_message_desc": "Voeg een optionele uitgebreide omschrijving toe…",
"repo.editor.commit_message_desc": "Voeg een optionele uitgebreide omschrijving toe\u00e2\u20ac\u00a6",
"repo.editor.signoff_desc": "Voeg een Signed-off-by toe aan het einde van het commit logbericht.",
"repo.editor.commit_directly_to_this_branch": "Commit direct naar de branch '<strong class=\"branch-name\">%s</strong>'.",
"repo.editor.create_new_branch": "Maak een <strong>nieuwe branch</strong> voor deze commit en start van een pull-aanvraag.",
"repo.editor.create_new_branch_np": "Maak een <strong>nieuwe branch</strong> voor deze commit.",
"repo.editor.propose_file_change": "Stel bestandswijziging voor",
"repo.editor.new_branch_name_desc": "Nieuwe branch naam…",
"repo.editor.new_branch_name_desc": "Nieuwe branch naam\u00e2\u20ac\u00a6",
"repo.editor.cancel": "Annuleer",
"repo.editor.filename_cannot_be_empty": "Bestandsnaam mag niet leeg zijn.",
"repo.editor.file_changed_while_editing": "De bestandsinhoud is veranderd sinds je bent begonnen met bewerken. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">Klik hier</a> om ze te zien, of <strong>commit de veranderingen opnieuw</strong> om ze te overschrijven.",
@@ -825,7 +825,7 @@
"repo.editor.push_rejected_no_message": "De wijziging is afgewezen door de server zonder bericht. Controleer de Git Hooks alsjeblieft.",
"repo.editor.push_rejected": "De wijziging is afgewezen door de server. Controleer Controleer de Git Hooks alsjeblieft.",
"repo.editor.push_rejected_summary": "Volledig afwijzingsbericht:",
"repo.editor.add_subdir": "Een map toevoegen…",
"repo.editor.add_subdir": "Een map toevoegen\u00e2\u20ac\u00a6",
"repo.editor.no_commit_to_branch": "Kan niet rechtstreeks naar branch committen omdat:",
"repo.editor.user_no_push_to_branch": "Gebruiker kan niet pushen naar branch",
"repo.editor.require_signed_commit": "Branch vereist een ondertekende commit",
@@ -857,7 +857,7 @@
"repo.projects.create": "Project aanmaken",
"repo.projects.title": "Titel",
"repo.projects.new": "Nieuw project",
"repo.projects.new_subheader": "Coördineer, track en update uw werk op één plek, dus projecten blijven transparant en op schema.",
"repo.projects.new_subheader": "Co\u00c3\u00b6rdineer, track en update uw werk op \u00c3\u00a9\u00c3\u00a9n plek, dus projecten blijven transparant en op schema.",
"repo.projects.deletion": "Project verwijderen",
"repo.projects.deletion_desc": "Als een project wordt verwijdert, wordt deze van alle gerelateerde kwesties verwijderd. Doorgaan?",
"repo.projects.deletion_success": "Het project is verwijderd.",
@@ -971,7 +971,7 @@
"repo.issues.num_comments": "%d opmerkingen",
"repo.issues.commented_at": "reageerde <a href=\"#%s\">%s</a>",
"repo.issues.delete_comment_confirm": "Weet u zeker dat u deze reactie wilt verwijderen?",
"repo.issues.context.copy_link": "Link kopiëren",
"repo.issues.context.copy_link": "Link kopi\u00c3\u00abren",
"repo.issues.context.quote_reply": "Citeer antwoord",
"repo.issues.context.reference_issue": "Verwijs in nieuw issue",
"repo.issues.context.edit": "Bewerken",
@@ -1071,7 +1071,7 @@
"repo.issues.dependency.title": "Afhankelijkheden",
"repo.issues.dependency.issue_no_dependencies": "Geen afhankelijkheden ingesteld.",
"repo.issues.dependency.pr_no_dependencies": "Geen afhankelijkheden ingesteld.",
"repo.issues.dependency.add": "Voeg afhankelijkheid toe…",
"repo.issues.dependency.add": "Voeg afhankelijkheid toe\u00e2\u20ac\u00a6",
"repo.issues.dependency.cancel": "Annuleer",
"repo.issues.dependency.remove": "Verwijder",
"repo.issues.dependency.remove_info": "Verwijder afhankelijkheid",
@@ -1115,7 +1115,7 @@
"repo.issues.reference_issue.body": "Inhoud",
"repo.issues.content_history.deleted": "verwijderd",
"repo.issues.content_history.edited": "bewerkt",
"repo.issues.content_history.created": "gecreëerd",
"repo.issues.content_history.created": "gecre\u00c3\u00aberd",
"repo.issues.content_history.delete_from_history": "Uit geschiedenis verwijderen",
"repo.issues.content_history.delete_from_history_confirm": "Uit geschiedenis verwijderen?",
"repo.issues.content_history.options": "Opties",
@@ -1251,7 +1251,7 @@
"repo.wiki.back_to_wiki": "Terug naar wiki-pagina",
"repo.wiki.delete_page_button": "Verwijder pagina",
"repo.wiki.page_already_exists": "Er bestaat al een wiki-pagina met deze naam.",
"repo.wiki.pages": "Pagina’s",
"repo.wiki.pages": "Pagina\u00e2\u20ac\u2122s",
"repo.wiki.last_updated": "Laatst bijgewerkt: %s",
"repo.wiki.page_name_desc": "Voer een naam in voor deze Wiki pagina. Sommige speciale namen zijn: 'Home', '_Sidebar' en '_Footer'.",
"repo.activity": "Activiteit",
@@ -1411,9 +1411,9 @@
"repo.settings.discord_icon_url": "Icoon URL",
"repo.settings.event_desc": "Trigger op:",
"repo.settings.event_send_everything": "Alle gebeurtenissen",
"repo.settings.event_choose": "Aangepaste gebeurtenissen…",
"repo.settings.event_choose": "Aangepaste gebeurtenissen\u00e2\u20ac\u00a6",
"repo.settings.event_header_repository": "Repository gebeurtenissen",
"repo.settings.event_create": "Creëer",
"repo.settings.event_create": "Cre\u00c3\u00aber",
"repo.settings.event_create_desc": "Branch, of tag aangemaakt.",
"repo.settings.event_delete": "Verwijder",
"repo.settings.event_delete_desc": "Branch of tag verwijderd.",
@@ -1487,13 +1487,13 @@
"repo.settings.require_signed_commits_desc": "Weiger pushes naar deze branch als deze niet ondertekend of niet verifieerbaar is.",
"repo.settings.protected_branch_deletion_desc": "Branch bescherming uitschakelen zorgt ervoor dat gebruikers met schrijfrechten naar de branch kunnen pushen. Doorgaan?",
"repo.settings.block_rejected_reviews": "Samenvoegen van afgewezen beoordelingen blokkeren",
"repo.settings.block_rejected_reviews_desc": "Samenvoegen zal niet mogelijk zijn wanneer er wijzigingen worden aangevraagd door officiële beoordelaars, zelfs niet als er genoeg goedkeuringen zijn.",
"repo.settings.block_on_official_review_requests": "Blokkeer de samenvoeging van officiële beoordelingsverzoeken",
"repo.settings.block_on_official_review_requests_desc": "Samenvoegen is niet mogelijk wanneer het officiële herzieningsverzoeken heeft, ook al zijn er genoeg goedkeuringen.",
"repo.settings.block_rejected_reviews_desc": "Samenvoegen zal niet mogelijk zijn wanneer er wijzigingen worden aangevraagd door offici\u00c3\u00able beoordelaars, zelfs niet als er genoeg goedkeuringen zijn.",
"repo.settings.block_on_official_review_requests": "Blokkeer de samenvoeging van offici\u00c3\u00able beoordelingsverzoeken",
"repo.settings.block_on_official_review_requests_desc": "Samenvoegen is niet mogelijk wanneer het offici\u00c3\u00able herzieningsverzoeken heeft, ook al zijn er genoeg goedkeuringen.",
"repo.settings.block_outdated_branch": "Samenvoegen blokkeren als pull request verouderd is",
"repo.settings.block_outdated_branch_desc": "Samenvoegen is niet mogelijk als de hoofd branch achter loop op de basis branch.",
"repo.settings.default_branch_desc": "Selecteer een standaard repository branch voor pull requests en code commits:",
"repo.settings.choose_branch": "Kies een branch…",
"repo.settings.choose_branch": "Kies een branch\u00e2\u20ac\u00a6",
"repo.settings.no_protected_branch": "Er zijn geen beschermde branches.",
"repo.settings.edit_protected_branch": "Bewerken",
"repo.settings.protected_branch_required_approvals_min": "Vereiste goedkeuringen kunnen niet negatief zijn.",
@@ -1572,7 +1572,7 @@
"repo.diff.load": "Laad Diff",
"repo.diff.generated": "gegenereerd",
"repo.diff.comment.placeholder": "Opmerking toevoegen",
"repo.diff.comment.add_single_comment": "Één reactie toevoegen",
"repo.diff.comment.add_single_comment": "\u00c3\u2030\u00c3\u00a9n reactie toevoegen",
"repo.diff.comment.add_review_comment": "Voeg commentaar toe",
"repo.diff.comment.start_review": "Review starten",
"repo.diff.comment.reply": "Reageer",
@@ -1600,7 +1600,7 @@
"repo.release.source_code": "Broncode",
"repo.release.tag_name": "Tagnaam",
"repo.release.target": "Doel",
"repo.release.tag_helper": "Kies een bestaande tag, of creëer een nieuwe tag bij publiceren.",
"repo.release.tag_helper": "Kies een bestaande tag, of cre\u00c3\u00aber een nieuwe tag bij publiceren.",
"repo.release.prerelease_desc": "Markeren als voorlopige versie",
"repo.release.prerelease_helper": "Markeer deze release als ongeschikt voor productiedoeleinden.",
"repo.release.cancel": "Annuleren",
@@ -1652,8 +1652,8 @@
"org.settings.visibility": "Zichtbaarheid",
"org.settings.visibility.public": "Publiek",
"org.settings.visibility.limited_shortname": "Beperkt",
"org.settings.visibility.private": "Privé (alleen zichtbaar voor organisatieleden)",
"org.settings.visibility.private_shortname": "Privé",
"org.settings.visibility.private": "Priv\u00c3\u00a9 (alleen zichtbaar voor organisatieleden)",
"org.settings.visibility.private_shortname": "Priv\u00c3\u00a9",
"org.settings.update_settings": "Instellingen bijwerken",
"org.settings.update_avatar_success": "De avatar van de organisatie is aangepast.",
"org.settings.delete": "Verwijder organisatie",
@@ -1807,7 +1807,7 @@
"admin.users.allow_create_organization": "Mag organisaties aanmaken",
"admin.users.update_profile": "Update gebruikers account",
"admin.users.delete_account": "Verwijder gebruikers account",
"admin.users.still_own_repo": "Deze gebruiker is nog steeds eigenaar van één of meerdere repositories. Verwijder of draag eerst deze repositories over.",
"admin.users.still_own_repo": "Deze gebruiker is nog steeds eigenaar van \u00c3\u00a9\u00c3\u00a9n of meerdere repositories. Verwijder of draag eerst deze repositories over.",
"admin.users.still_has_org": "Deze gebruiker is lid van een organisatie. Verwijder de gebruiker eerst uit alle organisaties.",
"admin.users.deletion_success": "De gebruiker is verwijderd.",
"admin.users.list_status_filter.is_active": "Actief",
@@ -2224,24 +2224,24 @@
"search.search": "Zoeken",
"search.fuzzy_tooltip": "Include results that closely match the search term",
"search.regexp": "Regexp",
"search.repo_kind": "Search repos",
"search.user_kind": "Search users",
"search.org_kind": "Search orgs",
"search.team_kind": "Search teams",
"search.code_kind": "Search code",
"search.package_kind": "Search packages",
"search.project_kind": "Search projects",
"search.branch_kind": "Search branches",
"search.tag_kind": "Search tags",
"search.commit_kind": "Search commits",
"search.runner_kind": "Search runners",
"search.issue_kind": "Search issues",
"search.pull_kind": "Search pull requests",
"search.repo_kind": "Search repos\u2026",
"search.user_kind": "Search users\u2026",
"search.org_kind": "Search orgs\u2026",
"search.team_kind": "Search teams\u2026",
"search.code_kind": "Search code\u2026",
"search.package_kind": "Search packages\u2026",
"search.project_kind": "Search projects\u2026",
"search.branch_kind": "Search branches\u2026",
"search.tag_kind": "Search tags\u2026",
"search.commit_kind": "Search commits\u2026",
"search.runner_kind": "Search runners\u2026",
"search.issue_kind": "Search issues\u2026",
"search.pull_kind": "Search pull requests\u2026",
"aria.footer.links": "Links",
"editor.buttons.strikethrough.tooltip": "Add strikethrough text",
"filter.string.asc": "AZ",
"filter.string.desc": "ZA",
"install.installing_desc": "Installing now, please wait",
"filter.string.asc": "A\u2013Z",
"filter.string.desc": "Z\u2013A",
"install.installing_desc": "Installing now, please wait\u2026",
"install.db_schema": "Schema",
"install.ssl_mode": "SSL",
"install.reinstall_confirm_check_1": "The data encrypted by the SECRET_KEY in app.ini may be lost: users may not be able to log in with 2FA/OTP and mirrors may not function correctly. By checking this box, you confirm that the current app.ini file contains the correct SECRET_KEY.",
@@ -2322,7 +2322,7 @@
"repo.mirror_address_url_invalid": "The provided URL is invalid. Make sure all components of the URL are escaped correctly.",
"repo.mirror_lfs_endpoint_desc": "Sync will attempt to use the clone URL to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">determine the LFS server</a>. You can also specify a custom endpoint if the repository LFS data is stored somewhere else.",
"repo.forks": "Forks",
"repo.adopt_search": "Enter username to search for unadopted repositories (leave blank to find all)",
"repo.adopt_search": "Enter username to search for unadopted repositories\u2026 (leave blank to find all)",
"repo.desc.sha256": "SHA256",
"repo.template.webhooks": "Webhooks",
"repo.archive.title": "This repo is archived. You can view files and clone it. You cannot open issues or pull requests or push a commit.",
@@ -2333,7 +2333,7 @@
"repo.migrate_items_releases": "Releases",
"repo.migrate.github_token_desc": "You can put one or more tokens here, separated by commas, to make migrating faster by circumventing the GitHub API rate limit. WARNING: Abusing this feature may violate the service provider's policy and may lead to getting your account(s) blocked.",
"repo.migrate.permission_denied_blocked": "You cannot import from disallowed hosts. Please ask the admin to check ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS settings.",
"repo.migrate.migrating": "Migrating from <b>%s</b>",
"repo.migrate.migrating": "Migrating from <b>%s</b>\u2026",
"repo.migrate.codecommit.aws_access_key_id": "AWS Access Key ID",
"repo.migrate.codecommit.aws_secret_access_key": "AWS Secret Access Key",
"repo.migration_status": "Migration status",
@@ -2388,14 +2388,14 @@
"repo.issues.review.review": "Review",
"repo.issues.assignee.error": "Not all assignees were added, due to an unexpected error.",
"repo.compare.title": "Comparing changes",
"repo.compare.description": "Choose two branches or tags to see whats changed or to start a new pull request.",
"repo.compare.description": "Choose two branches or tags to see what\u2019s changed or to start a new pull request.",
"repo.pulls.new.description": "Discuss and review the changes in this comparison with others.",
"repo.pulls.new.already_existed": "A pull request between these branches already exists",
"repo.pulls.edit.already_changed": "Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes.",
"repo.pulls.select_commit_hold_shift_for_range": "Select commit. Hold Shift and click to select a range.",
"repo.pulls.nothing_to_compare_have_tag": "The selected branches/tags are equal.",
"repo.pulls.tab_commits": "Commits",
"repo.pulls.is_checking": "Checking for merge conflicts",
"repo.pulls.is_checking": "Checking for merge conflicts\u2026",
"repo.pulls.wrong_commit_id": "commit ID must be a commit ID on the target branch",
"repo.pulls.no_merge_not_ready": "This pull request is not ready to be merged. Check review status and status checks.",
"repo.pulls.rebase_merge_pull_request": "Rebase, then fast-forward",
@@ -2413,7 +2413,7 @@
"repo.pulls.status_checks_need_approvals_helper": "The workflow will only run after approval from the repository maintainer.",
"repo.pulls.cmd_instruction_checkout_title": "Checkout",
"repo.pulls.cmd_instruction_merge_warning": "Warning: This operation cannot merge pull request because \"autodetect manual merge\" is not enabled.",
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By\".",
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By\u2026\".",
"repo.signing.wont_sign.error": "There was an error while checking if the commit could be signed.",
"repo.signing.wont_sign.twofa": "You must have two-factor authentication enabled to have commits signed.",
"repo.wiki": "Wiki",
@@ -2474,8 +2474,8 @@
"repo.settings.visibility.private.bullet_two": "May remove the relationship between it and <strong>forks</strong>, <strong>watchers</strong>, and <strong>stars</strong>.",
"repo.settings.unarchive.text": "Unarchiving the repo will restore its ability to receive commits and pushes, as well as new issues and pull requests.",
"repo.settings.lfs": "LFS",
"repo.settings.lfs_lock_path": "Filepath to lock",
"repo.settings.lfs_pointers.found": "Found %d blob pointer(s) %d associated, %d unassociated (%d missing from store)",
"repo.settings.lfs_lock_path": "Filepath to lock\u2026",
"repo.settings.lfs_pointers.found": "Found %d blob pointer(s) \u2014 %d associated, %d unassociated (%d missing from store)",
"repo.settings.lfs_pointers.sha": "Blob SHA",
"repo.settings.lfs_pointers.oid": "OID",
"repo.settings.pages": "Pagina's",
@@ -2535,7 +2535,7 @@
"repo.branch.commits_divergence_from": "Commit divergence: %[1]d behind and %[2]d ahead of %[3]s",
"repo.branch.commits_no_divergence": "The same as branch %[1]s",
"repo.find_file.follow_symlink": "Follow this symlink to where it is pointing at",
"graphs.component_loading": "Loading %s",
"graphs.component_loading": "Loading %s\u2026",
"org.teams": "Teams",
"org.pinned_repos": "Featured Projects",
"org.public_members": "Public Members",
@@ -2544,7 +2544,7 @@
"org.settings.email": "Contact Email Address",
"org.settings.change_visibility": "Change Visibility",
"org.settings.change_visibility_notices_1": "If the organization is converted to private, the repository stars will be removed and cannot be restored.",
"org.settings.change_visibility_notices_2": "Non-members will lose access to the organizations repositories if visibility is changed to private.",
"org.settings.change_visibility_notices_2": "Non-members will lose access to the organization\u2019s repositories if visibility is changed to private.",
"org.settings.change_visibility_success": "The visibility of organization %s has been successfully changed.",
"org.settings.visibility_desc": "Change who can view the organization and its repositories.",
"org.settings.rename": "Rename Organization",
@@ -2582,6 +2582,7 @@
"admin.dashboard.cleanup_packages": "Pakketten opschonen",
"admin.dashboard.cleanup_actions": "Acties opschonen",
"admin.dashboard.cleanup_expired_upload_sessions": "Verlopen uploads opschonen",
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
"admin.dashboard.delete_old_actions.started": "Verwijderen oude acties gestart",
"admin.dashboard.gc_lfs": "LFS-garbage collection",
"admin.users.bot": "Bot",
@@ -2659,12 +2660,12 @@
"packages.settings.global_access.disabled": "Globale toegang tot pakket is uitgeschakeld.",
"packages.settings.global_access.error": "Kon globale toegangsinstelling niet bijwerken.",
"packages.visibility": "Zichtbaarheid",
"packages.settings.visibility.private.text": "Dit pakket is momenteel privé. Maak het openbaar om iedereen toegang te geven.",
"packages.settings.visibility.private.button": "Privé maken",
"packages.settings.visibility.private.bullet_title": "U staat op het punt dit pakket privé te maken.",
"packages.settings.visibility.private.text": "Dit pakket is momenteel priv\u00e9. Maak het openbaar om iedereen toegang te geven.",
"packages.settings.visibility.private.button": "Priv\u00e9 maken",
"packages.settings.visibility.private.bullet_title": "U staat op het punt dit pakket priv\u00e9 te maken.",
"packages.settings.visibility.private.bullet_one": "Alleen gebruikers met de juiste rechten kunnen dit pakket openen.",
"packages.settings.visibility.private.success": "Pakket is nu privé.",
"packages.settings.visibility.public.text": "Dit pakket is momenteel openbaar. Maak het privé om de toegang te beperken.",
"packages.settings.visibility.private.success": "Pakket is nu priv\u00e9.",
"packages.settings.visibility.public.text": "Dit pakket is momenteel openbaar. Maak het priv\u00e9 om de toegang te beperken.",
"packages.settings.visibility.public.button": "Openbaar maken",
"packages.settings.visibility.public.bullet_title": "U staat op het punt dit pakket openbaar te maken.",
"packages.settings.visibility.public.bullet_one": "Iedereen kan dit pakket openen en downloaden.",
@@ -2713,7 +2714,7 @@
"actions.general.collaborative_owner_not_exist": "Collaboratieve eigenaar bestaat niet",
"actions.general.remove_collaborative_owner": "Collaboratieve eigenaar verwijderen",
"actions.general.remove_collaborative_owner_desc": "De eigenaar verliest toegang tot runnerbeheer",
"git.filemode.changed_filemode": "%[1]s %[2]s",
"git.filemode.changed_filemode": "%[1]s \u2192 %[2]s",
"org.pinned_repos_empty_title": "Showcase your best work",
"org.pinned_repos_empty_desc": "Pin up to 6 repositories to highlight your organization's most important projects.",
"org.settings.pinned.manage": "Manage Pins",
@@ -2780,7 +2781,7 @@
"admin.config.pinned_org_format_condensed": "Compact",
"admin.config.pinned_org_format_regular": "Normaal",
"admin.config.pinned_org_display_format_help": "Weergaveformaat voor vastgepinde organisaties",
"admin.config.logo_upload_success": "Logo geüpload",
"admin.config.logo_upload_success": "Logo ge\u00fcpload",
"admin.config.logo_url_success": "Logo-URL ingesteld",
"admin.config.logo_reset_success": "Logo gereset",
"admin.config.logo_invalid_type": "Ongeldig logo-bestandstype",
@@ -2791,7 +2792,7 @@
"admin.config.reset_icon": "Pictogram resetten",
"admin.config.current_icon": "Huidig pictogram",
"admin.config.icon_url": "Pictogram-URL",
"admin.config.icon_upload_success": "Pictogram geüpload",
"admin.config.icon_upload_success": "Pictogram ge\u00fcpload",
"admin.config.icon_url_success": "Pictogram-URL ingesteld",
"admin.config.icon_reset_success": "Pictogram gereset",
"admin.config.icon_invalid_type": "Ongeldig bestandstype. Toegestaan: SVG, PNG, ICO",
@@ -2818,6 +2819,12 @@
"admin.config.explore_org_display_format_help": "Weergaveformaat voor organisaties",
"admin.config.explore_org_format_list": "Lijst",
"admin.config.explore_org_format_tiles": "Tegels",
"admin.config.show_footer_powered_by": "Powered By weergeven",
"admin.config.show_footer_powered_by_desc": "Het bericht \"Powered by GitCaddy Server\" weergeven in de voettekst",
"admin.config.show_footer_licenses": "Licenties-link weergeven",
"admin.config.show_footer_licenses_desc": "De licenties-link weergeven in de voettekst",
"admin.config.show_footer_api": "API-link weergeven",
"admin.config.show_footer_api_desc": "De API (Swagger)-link weergeven in de voettekst",
"repo.settings.pages.general": "General",
"repo.settings.pages.brand": "Brand",
"repo.settings.pages.hero": "Hero",
@@ -2866,6 +2873,95 @@
"repo.settings.pages.seo_description": "Meta Description",
"repo.settings.pages.seo_keywords": "Keywords",
"repo.settings.pages.og_image": "Open Graph Image URL",
"repo.settings.pages.public_releases": "Openbare releases",
"repo.settings.pages.public_releases_desc": "Niet-geauthenticeerde gebruikers toestaan releases te downloaden. Handig voor landingspagina's van priv\u00e9repository's.",
"repo.settings.pages.brand_favicon_url": "Favicon-URL",
"repo.settings.pages.brand_favicon_url_help": "URL naar een aangepast favicon voor uw landingspagina (ICO, PNG of SVG). Laat leeg om de standaard te gebruiken.",
"repo.settings.pages.navigation": "Navigatielinks",
"repo.settings.pages.navigation_desc": "Bepaal welke ingebouwde links verschijnen in de kop- en voettekstnavigatie.",
"repo.settings.pages.nav_show_docs": "Docs-link tonen (linkt naar wiki)",
"repo.settings.pages.nav_show_api": "API-link tonen (linkt naar Swagger-documentatie)",
"repo.settings.pages.nav_show_repository": "Repository-link tonen (Broncode bekijken-knop)",
"repo.settings.pages.nav_show_releases": "Releases-link tonen",
"repo.settings.pages.nav_show_issues": "Issues-link tonen",
"repo.settings.pages.section_labels": "Sectielabels",
"repo.settings.pages.section_labels_desc": "Pas de koppen aan die op uw landingspagina worden weergegeven voor elke sectie",
"repo.settings.pages.label_value_props": "Kop waardeproposities",
"repo.settings.pages.label_value_props_help": "Kop voor de sectie waardeproposities (bijv., \"Waarom voor ons kiezen\")",
"repo.settings.pages.label_features": "Kop functies",
"repo.settings.pages.label_features_help": "Kop voor de sectie functies (bijv., \"Mogelijkheden\")",
"repo.settings.pages.blog_section": "Blogsectie",
"repo.settings.pages.blog_enabled_desc": "Toon recente blogberichten op de landingspagina",
"repo.settings.pages.blog_headline": "Blogtitel",
"repo.settings.pages.blog_subheadline": "Blog-ondertitel",
"repo.settings.pages.blog_max_posts": "Maximaal aantal berichten om te tonen",
"repo.settings.pages.ai_generate": "AI-inhoudsgenerator",
"repo.settings.pages.ai_generate_desc": "Genereer automatisch landingspagina-inhoud (kop, functies, statistieken, CTA's) uit de README en metadata van uw repository met AI.",
"repo.settings.pages.ai_generate_button": "Inhoud genereren met AI",
"repo.settings.pages.ai_generate_success": "Landingspagina-inhoud is succesvol gegenereerd. Bekijk en pas aan in de andere tabbladen.",
"repo.settings.pages.ai_generate_failed": "AI-generatie mislukt. Probeer het later opnieuw of configureer de inhoud handmatig.",
"repo.settings.pages.languages": "Talen",
"repo.settings.pages.default_lang": "Standaardtaal",
"repo.settings.pages.default_lang_help": "De hoofdtaal van uw landingspagina-inhoud",
"repo.settings.pages.enabled_languages": "Ingeschakelde talen",
"repo.settings.pages.enabled_languages_help": "Selecteer welke talen uw landingspagina moet ondersteunen. Bezoekers zien een taalwisselaar in de navigatie.",
"repo.settings.pages.save_languages": "Taalinstellingen opslaan",
"repo.settings.pages.languages_saved": "Taalinstellingen succesvol opgeslagen.",
"repo.settings.pages.translations": "Vertalingen",
"repo.settings.pages.ai_translate": "AI-vertaling",
"repo.settings.pages.ai_translate_success": "Vertaling is succesvol gegenereerd door AI. Controleer en bewerk indien nodig.",
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
"repo.settings.pages.delete_translation": "Verwijderen",
"repo.settings.pages.save_translation": "Vertaling opslaan",
"repo.settings.pages.translation_saved": "Vertaling succesvol opgeslagen.",
"repo.settings.pages.translation_deleted": "Vertaling verwijderd.",
"repo.settings.pages.translation_empty": "Geen vertaalinhoud opgegeven.",
"repo.settings.pages.trans_headline": "Kop",
"repo.settings.pages.trans_subheadline": "Ondertitel",
"repo.settings.pages.trans_primary_cta": "Primaire CTA-label",
"repo.settings.pages.trans_secondary_cta": "Secundaire CTA-label",
"repo.settings.pages.trans_cta_headline": "CTA-sectietitel",
"repo.settings.pages.trans_cta_subheadline": "CTA-sectie ondertitel",
"repo.settings.pages.trans_cta_button": "CTA-knoplabel",
"repo.settings.pages.trans_section_brand": "Brand",
"repo.settings.pages.trans_section_hero": "Hero",
"repo.settings.pages.trans_section_stats": "Stats",
"repo.settings.pages.trans_section_value_props": "Value Propositions",
"repo.settings.pages.trans_section_features": "Features",
"repo.settings.pages.trans_section_testimonials": "Testimonials",
"repo.settings.pages.trans_section_pricing": "Pricing",
"repo.settings.pages.trans_section_cta": "Call to Action",
"repo.settings.pages.trans_section_blog": "Blog",
"repo.settings.pages.trans_section_gallery": "Gallery",
"repo.settings.pages.trans_section_comparison": "Comparison",
"repo.settings.pages.trans_section_footer": "Footer",
"repo.settings.pages.trans_section_seo": "SEO",
"repo.settings.pages.trans_brand_name": "Brand Name",
"repo.settings.pages.trans_brand_tagline": "Tagline",
"repo.settings.pages.trans_stat_value": "Value",
"repo.settings.pages.trans_stat_label": "Label",
"repo.settings.pages.trans_title": "Title",
"repo.settings.pages.trans_description": "Description",
"repo.settings.pages.trans_quote": "Quote",
"repo.settings.pages.trans_role": "Role",
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
"repo.settings.pages.trans_plan_name": "Plan Name",
"repo.settings.pages.trans_plan_period": "Period",
"repo.settings.pages.trans_plan_cta": "Plan Button",
"repo.settings.pages.trans_blog_headline": "Blog Headline",
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
"repo.settings.pages.trans_blog_cta": "Blog Button",
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
"repo.settings.pages.trans_footer_copyright": "Copyright",
"repo.settings.pages.trans_footer_link": "Link Label",
"repo.settings.pages.trans_seo_title": "SEO Title",
"repo.settings.pages.trans_seo_description": "SEO Description",
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
@@ -2984,7 +3080,7 @@
"repo.settings.gallery_upload": "Afbeeldingen uploaden",
"repo.settings.gallery_current": "Huidige afbeeldingen",
"repo.settings.gallery_empty": "Nog geen galerij-afbeeldingen. Upload er enkele om uw project te tonen!",
"repo.settings.gallery_uploaded": "Afbeelding succesvol geüpload.",
"repo.settings.gallery_uploaded": "Afbeelding succesvol ge\u00fcpload.",
"repo.settings.gallery_deleted": "Afbeelding succesvol verwijderd.",
"repo.settings.gallery_caption_saved": "Bijschrift succesvol opgeslagen.",
"repo.settings.gallery_caption_placeholder": "Voeg een bijschrift toe...",
@@ -3041,12 +3137,12 @@
"vault.back_to_secret": "Terug naar Geheim",
"vault.back_to_versions": "Terug naar Versies",
"vault.show_value": "Waarde tonen",
"vault.copy_value": "Kopiëren naar klembord",
"vault.copy_value": "Kopi\u00ebren naar klembord",
"vault.view_hidden": "Verborgen bekijken",
"vault.view_raw": "Ruwe waarde bekijken",
"vault.hidden": "Verborgen",
"vault.raw": "Ruw",
"vault.copy": "Kopiëren",
"vault.copy": "Kopi\u00ebren",
"vault.copied": "Gekopieerd!",
"vault.multiline_hint": "Ondersteunt meerregelige inhoud (env-bestanden, certificaten, enz.)",
"vault.rollback": "Terugdraaien",
@@ -3164,5 +3260,68 @@
"repo.view_file": "Bestand bekijken",
"actions.runners.waiting_jobs": "Wachtende taken",
"actions.runners.back_to_runners": "Terug naar runners",
"actions.runners.no_waiting_jobs": "Geen taken wachten op dit label"
}
"actions.runners.no_waiting_jobs": "Geen taken wachten op dit label",
"repo.settings.pages.cross_promote_section": "Kruispromotie-sectie",
"repo.settings.pages.cross_promote_enabled_desc": "Toon cross-gepromote repositories op de landingspagina",
"repo.settings.pages.cross_promote_headline": "Sectiekop",
"repo.settings.pages.cross_promote_subheadline": "Sectie-ondertitel",
"repo.settings.pages.cross_promote_help": "Configureer de repositories in Instellingen > Kruispromotie.",
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files": "Verborgen bestanden en mappen uitsluiten",
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files_desc": "Bestanden/mappen die beginnen met \".\" of als verborgen gemarkeerd zijn niet naar de remote mirror pushen",
"repo.settings.pages.value_props_headline": "Sectiekop",
"repo.settings.pages.value_props_headline_help": "Grote kop voor de sectie waardeproposities. Het kleine label erboven wordt ingesteld in Sectielabels.",
"repo.settings.pages.value_props_subheadline": "Sectie-ondertitel",
"repo.settings.pages.features_headline": "Sectiekop",
"repo.settings.pages.features_headline_help": "Grote kop voor de sectie functies. Het kleine label erboven wordt ingesteld in Sectielabels.",
"repo.settings.pages.features_subheadline": "Sectie-ondertitel",
"repo.settings.license_acl_fields": "Auditable Commercial License instellingen",
"repo.settings.license_acl_fields_desc": "Deze velden zijn vereist om de Auditable Commercial License (ACL) v1.0 voor uw project aan te passen.",
"repo.settings.license_acl_jurisdiction": "Rechtsgebied",
"repo.settings.license_acl_jurisdiction_help": "Toepasselijk recht voor Sectie 8.1 (bijv., \"the State of Delaware, United States\").",
"repo.settings.license_acl_venue": "Locatie",
"repo.settings.license_acl_venue_help": "Rechtbank locatie voor geschillen (bijv., \"Wilmington, Delaware\").",
"repo.settings.license_acl_contact": "Contactadres (optioneel)",
"repo.settings.license_acl_contact_help": "Waar juridische kennisgevingen heen moeten. Voegt een Notices-sectie toe indien opgegeven.",
"repo.settings.license_acl_license_url": "Licentie URL (optioneel)",
"repo.settings.license_acl_tier_url": "Tier schema URL (optioneel)",
"repo.settings.license_acl_required": "Rechtsgebied en Locatie zijn vereist voor de Auditable Commercial License.",
"repo.settings.mirror_settings.push_mirror.reset_tooltip": "Mirror resetten naar specifieke commit",
"repo.settings.mirror_settings.push_mirror.reset_title": "Push-mirror resetten naar commit",
"repo.settings.mirror_settings.push_mirror.reset_warning_title": "Destructieve actie",
"repo.settings.mirror_settings.push_mirror.reset_warning_body": "Dit zal de gekozen commit afdwingen op de remote en de branchgeschiedenis daar herschrijven. Auto-sync wordt gepauzeerd.",
"repo.settings.mirror_settings.push_mirror.reset_branch": "Branch op remote",
"repo.settings.mirror_settings.push_mirror.reset_branch_help": "De branch op de remote mirror om te resetten.",
"repo.settings.mirror_settings.push_mirror.reset_commit": "Commit SHA",
"repo.settings.mirror_settings.push_mirror.reset_commit_help": "Volledige SHA van de commit in deze repository om naar de remote te pushen.",
"repo.settings.mirror_settings.push_mirror.reset_confirm": "Force-push en pauzeer sync",
"repo.settings.mirror_settings.push_mirror.reset_required": "Branch en commit SHA zijn vereist.",
"repo.settings.mirror_settings.push_mirror.reset_failed": "Mirror reset mislukt: %s",
"repo.settings.mirror_settings.push_mirror.reset_success": "Mirror reset voltooid. Auto-sync is gepauzeerd.",
"repo.settings.mirror_settings.push_mirror.reset_all_button": "Reset alle mirrors naar een commit",
"repo.settings.mirror_settings.push_mirror.reset_all_button_help": "Force-push dezelfde commit naar elke push-mirror tegelijk en pauzeer alle auto-sync.",
"repo.settings.mirror_settings.push_mirror.reset_all_title": "Reset alle push-mirrors naar commit",
"repo.settings.mirror_settings.push_mirror.reset_all_warning_title": "Destructieve actie \u2014 be\u00efnvloedt alle mirrors",
"repo.settings.mirror_settings.push_mirror.reset_all_warning_body": "Dit force-pusht de gekozen commit naar de genoemde branch op elke geconfigureerde push-mirror.",
"repo.settings.mirror_settings.push_mirror.reset_all_confirm": "Force-push alle mirrors en pauzeer sync",
"repo.settings.mirror_settings.push_mirror.reset_all_success": "Reset voltooid. %d mirror(s) bijgewerkt.",
"repo.settings.mirror_settings.push_mirror.reset_all_partial": "Gedeeltelijke reset: %d geslaagd, %d mislukt. Fouten: %s",
"repo.settings.repo_reset.title": "Repository-geschiedenis resetten",
"repo.settings.repo_reset.desc": "Verplaatst de HEAD van een branch naar een specifieke commit en force-pusht naar alle mirrors.",
"repo.settings.repo_reset.button": "Reset naar commit",
"repo.settings.repo_reset.warning_title": "Dit herschrijft de repository-geschiedenis",
"repo.settings.repo_reset.warning_body": "Deze actie zal:",
"repo.settings.repo_reset.warning_branch": "De HEAD van de branch op deze server verplaatsen naar de gekozen commit.",
"repo.settings.repo_reset.warning_collaborators": "Iedereen die de repository heeft gekloond of getrokken moet opnieuw klonen.",
"repo.settings.repo_reset.warning_mirrors": "Alle push-mirrors worden geforceerd bijgewerkt en hun auto-sync wordt gepauzeerd.",
"repo.settings.repo_reset.branch": "Branch om te resetten",
"repo.settings.repo_reset.commit": "Commit SHA",
"repo.settings.repo_reset.confirm_name": "Typ ter bevestiging de repository-naam: %s",
"repo.settings.repo_reset.confirm": "Repository resetten",
"repo.settings.repo_reset.required": "Branch en commit SHA zijn vereist.",
"repo.settings.repo_reset.confirm_mismatch": "Bevestiging van repository-naam komt niet overeen.",
"repo.settings.repo_reset.commit_not_found": "Commit %s niet gevonden.",
"repo.settings.repo_reset.branch_not_found": "Branch %s bestaat niet.",
"repo.settings.repo_reset.failed": "Reset mislukt: %s",
"repo.settings.repo_reset.success": "Reset voltooid.",
"repo.settings.repo_reset.partial": "Repository lokaal gereset, maar sommige mirrors mislukten: %s"
}

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
--------------------------------------------------------------------------------
GitCaddy Server - Third Party Licenses
Copyright (c) 2024-2026 MarketAlly Inc.
Copyright (c) 2010-2026 MarketAlly Inc.
https://marketally.com
GitCaddy is built on Gitea (https://gitea.com) and includes the following

View File

@@ -57,7 +57,7 @@ func GetOverview(ctx *context.APIContext) {
}
// Get stats
stats, err := org_service.GetOrgOverviewStats(ctx, org.ID)
stats, err := org_service.GetOrgOverviewStats(ctx, org.ID, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return

View File

@@ -72,7 +72,7 @@ func AIReviewPullRequest(ctx *context.APIContext) {
return
}
review, err := ai_service.ReviewPullRequest(ctx, pr)
review, err := ai_service.ReviewPullRequest(ctx, pr, nil)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
@@ -141,7 +141,7 @@ func AITriageIssue(ctx *context.APIContext) {
return
}
triage, err := ai_service.TriageIssue(ctx, issue)
triage, err := ai_service.TriageIssue(ctx, issue, nil)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
@@ -203,7 +203,7 @@ func AISuggestLabels(ctx *context.APIContext) {
return
}
suggestions, err := ai_service.SuggestLabels(ctx, issue)
suggestions, err := ai_service.SuggestLabels(ctx, issue, nil)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
@@ -279,7 +279,7 @@ func AIExplainCode(ctx *context.APIContext) {
return
}
explanation, err := ai_service.ExplainCode(ctx, ctx.Repo.Repository, req.FilePath, req.Code, req.StartLine, req.EndLine, req.Question)
explanation, err := ai_service.ExplainCode(ctx, ctx.Repo.Repository, req.FilePath, req.Code, req.StartLine, req.EndLine, req.Question, nil)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
@@ -355,7 +355,7 @@ func AIGenerateDocumentation(ctx *context.APIContext) {
return
}
docs, err := ai_service.GenerateDocumentation(ctx, ctx.Repo.Repository, req.FilePath, req.Code, req.DocType, req.Language, req.Style)
docs, err := ai_service.GenerateDocumentation(ctx, ctx.Repo.Repository, req.FilePath, req.Code, req.DocType, req.Language, req.Style, nil)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),

View File

@@ -0,0 +1,184 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v2
import (
"bufio"
"io"
"strings"
actions_model "code.gitcaddy.com/server/v3/models/actions"
"code.gitcaddy.com/server/v3/modules/actions"
"code.gitcaddy.com/server/v3/modules/gitrepo"
"code.gitcaddy.com/server/v3/modules/log"
api "code.gitcaddy.com/server/v3/modules/structs"
"code.gitcaddy.com/server/v3/services/context"
"code.gitcaddy.com/server/v3/services/convert"
)
const maxLogTailLines = 200
// GetRunFailureLog returns a structured failure summary for a workflow run.
// It includes failed jobs, their failed step names, extracted log tails,
// and the workflow YAML — all in a single response.
func GetRunFailureLog(ctx *context.APIContext) {
runID := ctx.PathParamInt64("run_id")
repo := ctx.Repo.Repository
// 1. Load the run
run, err := actions_model.GetRunByID(ctx, runID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if run == nil || run.RepoID != repo.ID {
ctx.APIErrorNotFound("run not found")
return
}
runStatus, runConclusion := convert.ToActionsStatus(run.Status)
// 2. Load all jobs for this run
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
// 3. Filter to failed jobs; if none explicitly failed, include all
var targetJobs []*actions_model.ActionRunJob
for _, job := range jobs {
if job.Status.IsFailure() {
targetJobs = append(targetJobs, job)
}
}
if len(targetJobs) == 0 {
targetJobs = jobs
}
// 4. For each target job, load steps and log tail
failedJobs := make([]*api.ActionJobFailureDetail, 0, len(targetJobs))
for _, job := range targetJobs {
jobStatus, jobConclusion := convert.ToActionsStatus(job.Status)
detail := &api.ActionJobFailureDetail{
JobID: job.ID,
JobName: job.Name,
Status: jobStatus,
Conclusion: jobConclusion,
}
// Load steps to find failed step names
if job.TaskID > 0 {
steps, err := actions_model.GetTaskStepsByTaskID(ctx, job.TaskID)
if err != nil {
log.Error("GetTaskStepsByTaskID(%d): %v", job.TaskID, err)
} else {
for _, step := range steps {
if step.Status.IsFailure() {
detail.FailedSteps = append(detail.FailedSteps, step.Name)
}
}
}
// Read log tail from the task
detail.Log = readLogTail(ctx, job.TaskID)
}
failedJobs = append(failedJobs, detail)
}
// 5. Read workflow YAML from the git repo
workflowYAML := readWorkflowYAML(ctx, run.WorkflowID)
ctx.JSON(200, &api.ActionRunFailureLog{
RunID: run.ID,
Status: runStatus,
Conclusion: runConclusion,
WorkflowID: run.WorkflowID,
WorkflowYAML: workflowYAML,
FailedJobs: failedJobs,
})
}
// readLogTail reads the last maxLogTailLines from a task's log, stripping timestamps.
func readLogTail(ctx *context.APIContext, taskID int64) string {
task, err := actions_model.GetTaskByID(ctx, taskID)
if err != nil {
log.Error("GetTaskByID(%d): %v", taskID, err)
return ""
}
f, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
if err != nil {
log.Error("OpenLogs(%s): %v", task.LogFilename, err)
return ""
}
defer f.Close()
content, err := io.ReadAll(f)
if err != nil {
log.Error("ReadAll log: %v", err)
return ""
}
// Split into lines, take last N
scanner := bufio.NewScanner(strings.NewReader(string(content)))
var allLines []string
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
// Strip timestamp prefix
_, parsed, parseErr := actions.ParseLog(line)
if parseErr == nil {
allLines = append(allLines, parsed)
} else {
allLines = append(allLines, line)
}
}
if len(allLines) > maxLogTailLines {
allLines = allLines[len(allLines)-maxLogTailLines:]
}
return strings.Join(allLines, "\n")
}
// readWorkflowYAML reads the workflow file content from the repo's default branch.
func readWorkflowYAML(ctx *context.APIContext, workflowID string) string {
gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
if err != nil {
log.Error("OpenRepository: %v", err)
return ""
}
defer gitRepo.Close()
commit, err := gitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
log.Error("GetBranchCommit(%s): %v", ctx.Repo.Repository.DefaultBranch, err)
return ""
}
// Try .gitea/workflows/ first, then .github/workflows/
for _, prefix := range []string{".gitea/workflows/", ".github/workflows/"} {
blob, err := commit.GetBlobByPath(prefix + workflowID)
if err != nil {
continue
}
reader, err := blob.DataAsync()
if err != nil {
continue
}
defer reader.Close()
data, err := io.ReadAll(reader)
if err != nil {
continue
}
return string(data)
}
return ""
}

View File

@@ -0,0 +1,46 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v2
import (
actions_model "code.gitcaddy.com/server/v3/models/actions"
"code.gitcaddy.com/server/v3/modules/git"
api "code.gitcaddy.com/server/v3/modules/structs"
"code.gitcaddy.com/server/v3/services/context"
"code.gitcaddy.com/server/v3/services/convert"
)
// ListWorkflowStatuses returns the latest run status for each workflow in the repository.
// This is a batch endpoint that replaces the need to fetch all runs and filter client-side.
func ListWorkflowStatuses(ctx *context.APIContext) {
repo := ctx.Repo.Repository
runs, err := actions_model.GetLatestRunPerWorkflow(ctx, repo.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
workflows := make([]*api.ActionWorkflowStatus, 0, len(runs))
for _, run := range runs {
status, conclusion := convert.ToActionsStatus(run.Status)
workflows = append(workflows, &api.ActionWorkflowStatus{
WorkflowID: run.WorkflowID,
WorkflowName: run.Title,
Status: status,
Conclusion: conclusion,
RunID: run.ID,
RunNumber: run.Index,
Event: string(run.Event),
HeadBranch: git.RefName(run.Ref).BranchName(),
StartedAt: run.Started.AsLocalTime(),
CompletedAt: run.Stopped.AsLocalTime(),
HTMLURL: run.HTMLURL(),
})
}
ctx.JSON(200, &api.ActionWorkflowStatusResponse{
Workflows: workflows,
})
}

View File

@@ -0,0 +1,479 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v2
import (
"net/http"
ai_model "code.gitcaddy.com/server/v3/models/ai"
"code.gitcaddy.com/server/v3/models/db"
issues_model "code.gitcaddy.com/server/v3/models/issues"
repo_model "code.gitcaddy.com/server/v3/models/repo"
"code.gitcaddy.com/server/v3/models/unit"
ai_module "code.gitcaddy.com/server/v3/modules/ai"
apierrors "code.gitcaddy.com/server/v3/modules/errors"
"code.gitcaddy.com/server/v3/modules/setting"
api "code.gitcaddy.com/server/v3/modules/structs"
"code.gitcaddy.com/server/v3/modules/web"
"code.gitcaddy.com/server/v3/services/ai"
"code.gitcaddy.com/server/v3/services/context"
)
// getRepoAIConfig is a helper that loads the AI config for the repo in ctx,
// returning nil and writing an error response if AI is not available.
func getRepoAIConfig(ctx *context.APIContext) *repo_model.AIConfig {
if !setting.AI.Enabled {
ctx.APIErrorWithCode(apierrors.AIDisabled)
return nil
}
aiUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeAI)
if err != nil {
ctx.APIErrorWithCode(apierrors.AIUnitNotEnabled)
return nil
}
return aiUnit.AIConfig()
}
// resolveProviderConfig builds a ProviderConfig from the repo/org/system cascade.
func resolveProviderConfig(ctx *context.APIContext) *ai_module.ProviderConfig {
var orgID int64
if ctx.Repo.Repository.Owner.IsOrganization() {
orgID = ctx.Repo.Repository.OwnerID
}
var repoProvider, repoModel string
if aiUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeAI); err == nil {
cfg := aiUnit.AIConfig()
repoProvider = cfg.PreferredProvider
repoModel = cfg.PreferredModel
}
provider := ai_model.ResolveProvider(ctx, orgID, repoProvider)
model := ai_model.ResolveModel(ctx, orgID, repoModel)
apiKey := ai_model.ResolveAPIKey(ctx, orgID, provider)
return &ai_module.ProviderConfig{
Provider: provider,
Model: model,
APIKey: apiKey,
}
}
func toAIOperationV2(op *ai_model.OperationLog) *api.AIOperationV2 {
return &api.AIOperationV2{
ID: op.ID,
RepoID: op.RepoID,
Operation: op.Operation,
Tier: op.Tier,
TriggerEvent: op.TriggerEvent,
TriggerUserID: op.TriggerUserID,
TargetID: op.TargetID,
TargetType: op.TargetType,
Provider: op.Provider,
Model: op.Model,
InputTokens: op.InputTokens,
OutputTokens: op.OutputTokens,
Status: op.Status,
ResultCommentID: op.ResultCommentID,
ActionRunID: op.ActionRunID,
ErrorMessage: op.ErrorMessage,
DurationMs: op.DurationMs,
CreatedAt: op.CreatedUnix.AsTime(),
}
}
// GetRepoAISettings returns the AI settings for a repository
func GetRepoAISettings(ctx *context.APIContext) {
aiCfg := getRepoAIConfig(ctx)
if aiCfg == nil {
return
}
// Resolve cascade values for display
var orgID int64
if ctx.Repo.Repository.Owner.IsOrganization() {
orgID = ctx.Repo.Repository.OwnerID
}
ctx.JSON(http.StatusOK, &api.AISettingsV2{
AutoRespondToIssues: aiCfg.AutoRespondToIssues,
AutoReviewPRs: aiCfg.AutoReviewPRs,
AutoInspectWorkflows: aiCfg.AutoInspectWorkflows,
AutoTriageIssues: aiCfg.AutoTriageIssues,
AgentModeEnabled: aiCfg.AgentModeEnabled,
AgentTriggerLabels: aiCfg.AgentTriggerLabels,
AgentMaxRunMinutes: aiCfg.AgentMaxRunMinutes,
EscalateToStaff: aiCfg.EscalateToStaff,
EscalationLabel: aiCfg.EscalationLabel,
EscalationAssignTeam: aiCfg.EscalationAssignTeam,
PreferredProvider: aiCfg.PreferredProvider,
PreferredModel: aiCfg.PreferredModel,
SystemInstructions: aiCfg.SystemInstructions,
ReviewInstructions: aiCfg.ReviewInstructions,
IssueInstructions: aiCfg.IssueInstructions,
ResolvedProvider: ai_model.ResolveProvider(ctx, orgID, aiCfg.PreferredProvider),
ResolvedModel: ai_model.ResolveModel(ctx, orgID, aiCfg.PreferredModel),
})
}
// UpdateRepoAISettings updates the AI settings for a repository
func UpdateRepoAISettings(ctx *context.APIContext) {
if !setting.AI.Enabled {
ctx.APIErrorWithCode(apierrors.AIDisabled)
return
}
if !ctx.Repo.Permission.IsAdmin() {
ctx.APIErrorWithCode(apierrors.PermRepoAdminRequired)
return
}
form := web.GetForm(ctx).(*api.UpdateAISettingsOption)
// Get existing AI unit, or create it
aiUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeAI)
if err != nil {
// AI unit doesn't exist yet — create it
aiUnit = &repo_model.RepoUnit{
RepoID: ctx.Repo.Repository.ID,
Type: unit.TypeAI,
Config: &repo_model.AIConfig{},
}
}
cfg := aiUnit.AIConfig()
// Apply updates using optional fields (nil = don't change)
if form.AutoRespondToIssues != nil {
cfg.AutoRespondToIssues = *form.AutoRespondToIssues
}
if form.AutoReviewPRs != nil {
cfg.AutoReviewPRs = *form.AutoReviewPRs
}
if form.AutoInspectWorkflows != nil {
cfg.AutoInspectWorkflows = *form.AutoInspectWorkflows
}
if form.AutoTriageIssues != nil {
cfg.AutoTriageIssues = *form.AutoTriageIssues
}
if form.AgentModeEnabled != nil {
if *form.AgentModeEnabled && !setting.AI.AllowAgentMode {
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
"detail": "Agent mode is disabled by the system administrator",
})
return
}
cfg.AgentModeEnabled = *form.AgentModeEnabled
}
if form.AgentTriggerLabels != nil {
cfg.AgentTriggerLabels = form.AgentTriggerLabels
}
if form.AgentMaxRunMinutes != nil {
cfg.AgentMaxRunMinutes = *form.AgentMaxRunMinutes
}
if form.EscalateToStaff != nil {
cfg.EscalateToStaff = *form.EscalateToStaff
}
if form.EscalationLabel != nil {
cfg.EscalationLabel = *form.EscalationLabel
}
if form.EscalationAssignTeam != nil {
cfg.EscalationAssignTeam = *form.EscalationAssignTeam
}
if form.PreferredProvider != nil {
cfg.PreferredProvider = *form.PreferredProvider
}
if form.PreferredModel != nil {
cfg.PreferredModel = *form.PreferredModel
}
if form.SystemInstructions != nil {
cfg.SystemInstructions = *form.SystemInstructions
}
if form.ReviewInstructions != nil {
cfg.ReviewInstructions = *form.ReviewInstructions
}
if form.IssueInstructions != nil {
cfg.IssueInstructions = *form.IssueInstructions
}
aiUnit.Config = cfg
if aiUnit.ID == 0 {
if err := db.Insert(ctx, aiUnit); err != nil {
ctx.APIErrorInternal(err)
return
}
} else {
if err := repo_model.UpdateRepoUnit(ctx, aiUnit); err != nil {
ctx.APIErrorInternal(err)
return
}
}
ctx.JSON(http.StatusOK, cfg)
}
// ListAIOperations returns the AI operation log for a repository
func ListAIOperations(ctx *context.APIContext) {
if !setting.AI.Enabled {
ctx.APIErrorWithCode(apierrors.AIDisabled)
return
}
page := max(ctx.FormInt("page"), 1)
limit := ctx.FormInt("limit")
if limit <= 0 || limit > 50 {
limit = 20
}
opts := ai_model.FindOperationLogsOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: limit,
},
RepoID: ctx.Repo.Repository.ID,
Operation: ctx.FormString("operation"),
Status: ctx.FormString("status"),
Tier: ctx.FormInt("tier"),
}
ops, err := db.Find[ai_model.OperationLog](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
count, err := db.Count[ai_model.OperationLog](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*api.AIOperationV2, 0, len(ops))
for _, op := range ops {
result = append(result, toAIOperationV2(op))
}
ctx.JSON(http.StatusOK, &api.AIOperationListV2{
Operations: result,
TotalCount: count,
})
}
// GetAIOperation returns a single AI operation by ID
func GetAIOperation(ctx *context.APIContext) {
if !setting.AI.Enabled {
ctx.APIErrorWithCode(apierrors.AIDisabled)
return
}
id := ctx.PathParamInt64("id")
op, err := ai_model.GetOperationLog(ctx, id)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if op == nil || op.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorWithCode(apierrors.AIOperationNotFound)
return
}
ctx.JSON(http.StatusOK, toAIOperationV2(op))
}
// TriggerAIReview manually triggers an AI code review for a pull request
func TriggerAIReview(ctx *context.APIContext) {
if getRepoAIConfig(ctx) == nil {
return
}
if !setting.AI.EnableCodeReview {
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
"detail": "Code review is disabled by the system administrator",
})
return
}
pullIndex := ctx.PathParamInt64("pull")
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, pullIndex)
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorWithCode(apierrors.PRNotFound)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !issue.IsPull {
ctx.APIErrorWithCode(apierrors.PRNotFound)
return
}
if err := ai.EnqueueOperation(&ai.OperationRequest{
RepoID: ctx.Repo.Repository.ID,
Operation: "code-review",
Tier: 1,
TriggerEvent: "api.manual",
TriggerUserID: ctx.Doer.ID,
TargetID: issue.ID,
TargetType: "pull",
}); err != nil {
ctx.APIErrorWithCode(apierrors.AIServiceError, map[string]any{
"detail": err.Error(),
})
return
}
ctx.JSON(http.StatusAccepted, map[string]any{
"message": "AI code review has been queued",
"issue_id": issue.ID,
})
}
// triggerIssueAIOp is a shared helper for issue-targeted AI operations (respond, triage).
func triggerIssueAIOp(ctx *context.APIContext, operation, successMsg string) {
issueIndex := ctx.PathParamInt64("issue")
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, issueIndex)
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorWithCode(apierrors.IssueNotFound)
} else {
ctx.APIErrorInternal(err)
}
return
}
if err := ai.EnqueueOperation(&ai.OperationRequest{
RepoID: ctx.Repo.Repository.ID,
Operation: operation,
Tier: 1,
TriggerEvent: "api.manual",
TriggerUserID: ctx.Doer.ID,
TargetID: issue.ID,
TargetType: "issue",
}); err != nil {
ctx.APIErrorWithCode(apierrors.AIServiceError, map[string]any{
"detail": err.Error(),
})
return
}
ctx.JSON(http.StatusAccepted, map[string]any{
"message": successMsg,
"issue_id": issue.ID,
})
}
// TriggerAIRespond manually triggers an AI response to an issue
func TriggerAIRespond(ctx *context.APIContext) {
if getRepoAIConfig(ctx) == nil {
return
}
if !setting.AI.AllowAutoRespond {
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
"detail": "Auto-respond is disabled by the system administrator",
})
return
}
triggerIssueAIOp(ctx, "issue-response", "AI response has been queued")
}
// TriggerAITriage manually triggers AI triage for an issue
func TriggerAITriage(ctx *context.APIContext) {
if getRepoAIConfig(ctx) == nil {
return
}
if !setting.AI.EnableIssueTriage {
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
"detail": "Issue triage is disabled by the system administrator",
})
return
}
triggerIssueAIOp(ctx, "issue-triage", "AI triage has been queued")
}
// TriggerAIExplain triggers an AI explanation of code
func TriggerAIExplain(ctx *context.APIContext) {
if !setting.AI.Enabled {
ctx.APIErrorWithCode(apierrors.AIDisabled)
return
}
if !setting.AI.EnableExplainCode {
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
"detail": "Code explanation is disabled by the system administrator",
})
return
}
form := web.GetForm(ctx).(*api.AIExplainRequest)
providerCfg := resolveProviderConfig(ctx)
resp, err := ai.ExplainCode(ctx, ctx.Repo.Repository, form.FilePath, "", form.StartLine, form.EndLine, form.Question, providerCfg)
if err != nil {
ctx.APIErrorWithCode(apierrors.AIServiceError, map[string]any{
"detail": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, resp)
}
// TriggerAIFix triggers a Tier 2 agent fix for an issue
func TriggerAIFix(ctx *context.APIContext) {
aiCfg := getRepoAIConfig(ctx)
if aiCfg == nil {
return
}
if !setting.AI.AllowAgentMode {
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
"detail": "Agent mode is disabled by the system administrator",
})
return
}
if !aiCfg.AgentModeEnabled {
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
"detail": "Agent mode is not enabled for this repository",
})
return
}
issueIndex := ctx.PathParamInt64("issue")
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, issueIndex)
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorWithCode(apierrors.IssueNotFound)
} else {
ctx.APIErrorInternal(err)
}
return
}
if err := ai.EnqueueOperation(&ai.OperationRequest{
RepoID: ctx.Repo.Repository.ID,
Operation: "agent-fix",
Tier: 2,
TriggerEvent: "api.manual",
TriggerUserID: ctx.Doer.ID,
TargetID: issue.ID,
TargetType: "issue",
}); err != nil {
ctx.APIErrorWithCode(apierrors.AIServiceError, map[string]any{
"detail": err.Error(),
})
return
}
ctx.JSON(http.StatusAccepted, map[string]any{
"message": "AI agent fix has been queued",
"issue_id": issue.ID,
"tier": 2,
})
}

View File

@@ -0,0 +1,242 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v2
import (
"net/http"
ai_model "code.gitcaddy.com/server/v3/models/ai"
"code.gitcaddy.com/server/v3/models/db"
"code.gitcaddy.com/server/v3/models/organization"
ai_module "code.gitcaddy.com/server/v3/modules/ai"
apierrors "code.gitcaddy.com/server/v3/modules/errors"
"code.gitcaddy.com/server/v3/modules/setting"
api "code.gitcaddy.com/server/v3/modules/structs"
"code.gitcaddy.com/server/v3/modules/web"
"code.gitcaddy.com/server/v3/services/context"
)
// GetOrgAISettingsV2 returns the AI settings for an organization
func GetOrgAISettingsV2(ctx *context.APIContext) {
if !setting.AI.Enabled {
ctx.APIErrorWithCode(apierrors.AIDisabled)
return
}
orgName := ctx.PathParam("org")
org, err := organization.GetOrgByName(ctx, orgName)
if err != nil {
if organization.IsErrOrgNotExist(err) {
ctx.APIErrorWithCode(apierrors.OrgNotFound)
} else {
ctx.APIErrorInternal(err)
}
return
}
// Require org admin
if !ctx.Doer.IsAdmin {
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !isOwner {
ctx.APIErrorWithCode(apierrors.PermOrgAdminRequired)
return
}
}
settings, err := ai_model.GetOrgAISettings(ctx, org.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if settings == nil {
// Return empty defaults
ctx.JSON(http.StatusOK, &api.OrgAISettingsV2{})
return
}
ctx.JSON(http.StatusOK, &api.OrgAISettingsV2{
Provider: settings.Provider,
Model: settings.Model,
HasAPIKey: settings.APIKeyEncrypted != "",
MaxOpsPerHour: settings.MaxOpsPerHour,
AllowedOps: settings.AllowedOps,
AgentModeAllowed: settings.AgentModeAllowed,
})
}
// UpdateOrgAISettingsV2 updates the AI settings for an organization
func UpdateOrgAISettingsV2(ctx *context.APIContext) {
if !setting.AI.Enabled {
ctx.APIErrorWithCode(apierrors.AIDisabled)
return
}
orgName := ctx.PathParam("org")
org, err := organization.GetOrgByName(ctx, orgName)
if err != nil {
if organization.IsErrOrgNotExist(err) {
ctx.APIErrorWithCode(apierrors.OrgNotFound)
} else {
ctx.APIErrorInternal(err)
}
return
}
// Require org admin
if !ctx.Doer.IsAdmin {
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !isOwner {
ctx.APIErrorWithCode(apierrors.PermOrgAdminRequired)
return
}
}
form := web.GetForm(ctx).(*api.UpdateOrgAISettingsOption)
// Load existing or create new
settings, err := ai_model.GetOrgAISettings(ctx, org.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if settings == nil {
settings = &ai_model.OrgAISettings{OrgID: org.ID}
}
if form.Provider != nil {
settings.Provider = *form.Provider
}
if form.Model != nil {
settings.Model = *form.Model
}
if form.APIKey != nil {
if err := settings.SetAPIKey(*form.APIKey); err != nil {
ctx.APIErrorInternal(err)
return
}
}
if form.MaxOpsPerHour != nil {
settings.MaxOpsPerHour = *form.MaxOpsPerHour
}
if form.AllowedOps != nil {
settings.AllowedOps = *form.AllowedOps
}
if form.AgentModeAllowed != nil {
if *form.AgentModeAllowed && !setting.AI.AllowAgentMode {
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
"detail": "Agent mode is disabled by the system administrator",
})
return
}
settings.AgentModeAllowed = *form.AgentModeAllowed
}
if err := ai_model.CreateOrUpdateOrgAISettings(ctx, settings); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, &api.OrgAISettingsV2{
Provider: settings.Provider,
Model: settings.Model,
HasAPIKey: settings.APIKeyEncrypted != "",
MaxOpsPerHour: settings.MaxOpsPerHour,
AllowedOps: settings.AllowedOps,
AgentModeAllowed: settings.AgentModeAllowed,
})
}
// GetAIServiceStatus returns the AI service health and global stats (admin only)
func GetAIServiceStatus(ctx *context.APIContext) {
if !ctx.Doer.IsAdmin {
ctx.APIErrorWithCode(apierrors.PermActionDenied)
return
}
resp := &api.AIServiceStatusV2{
Enabled: setting.AI.Enabled,
ServiceURL: setting.AI.ServiceURL,
}
if !setting.AI.Enabled {
ctx.JSON(http.StatusOK, resp)
return
}
// Check AI service health
client := ai_module.GetClient()
health, err := client.CheckHealth(ctx)
if err == nil && health != nil {
resp.Healthy = health.Healthy
resp.Version = health.Version
resp.ProviderStatus = health.ProviderStatus
}
ctx.JSON(http.StatusOK, resp)
}
// ListAllAIOperations returns the global AI operation log (admin only)
func ListAllAIOperations(ctx *context.APIContext) {
if !ctx.Doer.IsAdmin {
ctx.APIErrorWithCode(apierrors.PermActionDenied)
return
}
if !setting.AI.Enabled {
ctx.APIErrorWithCode(apierrors.AIDisabled)
return
}
page := max(ctx.FormInt("page"), 1)
limit := ctx.FormInt("limit")
if limit <= 0 || limit > 100 {
limit = 50
}
opts := ai_model.FindOperationLogsOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: limit,
},
Operation: ctx.FormString("operation"),
Status: ctx.FormString("status"),
Tier: ctx.FormInt("tier"),
}
// Allow filtering by repo
if repoID := ctx.FormInt64("repo_id"); repoID > 0 {
opts.RepoID = repoID
}
ops, err := db.Find[ai_model.OperationLog](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
count, err := db.Count[ai_model.OperationLog](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*api.AIOperationV2, 0, len(ops))
for _, op := range ops {
result = append(result, toAIOperationV2(op))
}
ctx.JSON(http.StatusOK, &api.AIOperationListV2{
Operations: result,
TotalCount: count,
})
}

View File

@@ -146,6 +146,8 @@ func Routes() *web.Router {
m.Get("/runners/status", repoAssignment(), ListRunnersStatus)
m.Get("/runners/{runner_id}/status", repoAssignment(), GetRunnerStatus)
m.Post("/workflows/validate", repoAssignment(), reqToken(), web.Bind(api.WorkflowValidationRequest{}), ValidateWorkflow)
m.Get("/workflows/status", repoAssignment(), ListWorkflowStatuses)
m.Get("/runs/{run_id}/failure-log", repoAssignment(), GetRunFailureLog)
})
// Releases v2 API - Enhanced releases with app update support
@@ -169,10 +171,25 @@ func Routes() *web.Router {
// Upload instructions endpoint
m.Get("/upload/instructions", GetUploadInstructions)
// Public landing page API - for private repos with public_landing enabled
// Landing page API
m.Group("/repos/{owner}/{repo}/pages", func() {
// Public read endpoints
m.Get("/config", repoAssignmentWithPublicAccess(), GetPagesConfig)
m.Get("/content", repoAssignmentWithPublicAccess(), GetPagesContent)
// Write endpoints (require auth + repo admin)
m.Group("/config", func() {
m.Put("", web.Bind(api.UpdatePagesConfigOption{}), UpdatePagesConfig)
m.Patch("", web.Bind(api.UpdatePagesConfigOption{}), PatchPagesConfig)
m.Put("/brand", web.Bind(api.UpdatePagesBrandOption{}), UpdatePagesBrand)
m.Put("/hero", web.Bind(api.UpdatePagesHeroOption{}), UpdatePagesHero)
m.Put("/content", web.Bind(api.UpdatePagesContentOption{}), UpdatePagesContentSection)
m.Put("/comparison", web.Bind(api.UpdatePagesComparisonOption{}), UpdatePagesComparison)
m.Put("/social", web.Bind(api.UpdatePagesSocialOption{}), UpdatePagesSocial)
m.Put("/pricing", web.Bind(api.UpdatePagesPricingOption{}), UpdatePagesPricing)
m.Put("/footer", web.Bind(api.UpdatePagesFooterOption{}), UpdatePagesFooter)
m.Put("/theme", web.Bind(api.UpdatePagesThemeOption{}), UpdatePagesTheme)
}, repoAssignment(), reqToken())
})
// Blog v2 API - repository blog endpoints
@@ -214,6 +231,54 @@ func Routes() *web.Router {
m.Delete("", web.Bind(api.HiddenFolderOptionV2{}), RemoveHiddenFolderV2)
}, repoAssignment(), reqToken())
})
// AI operations API - repo-scoped AI operations and settings
m.Group("/repos/{owner}/{repo}/ai", func() {
m.Get("/settings", GetRepoAISettings)
m.Get("/operations", ListAIOperations)
m.Get("/operations/{id}", GetAIOperation)
m.Group("", func() {
m.Put("/settings", web.Bind(api.UpdateAISettingsOption{}), UpdateRepoAISettings)
m.Post("/review/{pull}", TriggerAIReview)
m.Post("/respond/{issue}", TriggerAIRespond)
m.Post("/triage/{issue}", TriggerAITriage)
m.Post("/explain", web.Bind(api.AIExplainRequest{}), TriggerAIExplain)
m.Post("/fix/{issue}", TriggerAIFix)
}, reqToken())
}, repoAssignment())
// Organization v2 API - org overview, repos, profile, pinned repos
m.Group("/user/orgs", func() {
m.Get("", ListUserOrgsV2)
}, reqToken())
m.Group("/orgs/{org}", func() {
// Public read endpoints
m.Get("/overview", GetOrgOverviewV2)
m.Get("/repos", ListOrgReposV2)
m.Get("/profile/readme", GetOrgProfileReadmeV2)
// Write endpoints require authentication + org ownership (checked in handler)
m.Group("", func() {
m.Patch("", web.Bind(UpdateOrgV2Option{}), UpdateOrgV2)
m.Put("/profile/readme", web.Bind(UpdateOrgProfileReadmeOption{}), UpdateOrgProfileReadmeV2)
m.Post("/pinned", web.Bind(PinOrgRepoV2Option{}), PinOrgRepoV2)
m.Delete("/pinned/{repo}", UnpinOrgRepoV2)
}, reqToken())
})
// AI settings API - org-scoped AI settings
m.Group("/orgs/{org}/ai", func() {
m.Get("/settings", GetOrgAISettingsV2)
m.Put("/settings", web.Bind(api.UpdateOrgAISettingsOption{}), UpdateOrgAISettingsV2)
}, reqToken())
// AI admin API - site admin AI management
m.Group("/admin/ai", func() {
m.Get("/status", GetAIServiceStatus)
m.Get("/operations", ListAllAIOperations)
}, reqToken())
})
return m

View File

@@ -16,6 +16,7 @@ import (
actions_model "code.gitcaddy.com/server/v3/models/actions"
"code.gitcaddy.com/server/v3/models/db"
issue_model "code.gitcaddy.com/server/v3/models/issues"
packages_model "code.gitcaddy.com/server/v3/models/packages"
repo_model "code.gitcaddy.com/server/v3/models/repo"
secret_model "code.gitcaddy.com/server/v3/models/secret"
@@ -572,6 +573,59 @@ var mcpTools = []MCPTool{
"required": []string{"owner"},
},
},
{
Name: "list_issues",
Description: "List issues for a repository with pagination. Returns issue number, title, state, labels, poster, timestamps, and total count.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"owner": map[string]any{
"type": "string",
"description": "Repository owner",
},
"repo": map[string]any{
"type": "string",
"description": "Repository name",
},
"state": map[string]any{
"type": "string",
"description": "Filter by state: open, closed, or all (default: open)",
"enum": []string{"open", "closed", "all"},
},
"page": map[string]any{
"type": "integer",
"description": "Page number (default 1)",
},
"limit": map[string]any{
"type": "integer",
"description": "Results per page (default 20, max 100)",
},
},
"required": []string{"owner", "repo"},
},
},
{
Name: "get_issue",
Description: "Get detailed information about a specific issue including body content and comments.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"owner": map[string]any{
"type": "string",
"description": "Repository owner",
},
"repo": map[string]any{
"type": "string",
"description": "Repository name",
},
"number": map[string]any{
"type": "integer",
"description": "Issue number",
},
},
"required": []string{"owner", "repo", "number"},
},
},
}
// MCPHandler handles MCP protocol requests
@@ -631,9 +685,11 @@ func handleInitialize(ctx *context_service.APIContext, req *MCPRequest) {
}
func handleToolsList(ctx *context_service.APIContext, req *MCPRequest) {
allTools := make([]MCPTool, 0, len(mcpTools)+len(mcpAITools))
allTools := make([]MCPTool, 0, len(mcpTools)+len(mcpAITools)+len(mcpPagesTools)+len(mcpOrgTools))
allTools = append(allTools, mcpTools...)
allTools = append(allTools, mcpAITools...)
allTools = append(allTools, mcpPagesTools...)
allTools = append(allTools, mcpOrgTools...)
result := MCPToolsListResult{Tools: allTools}
sendMCPResult(ctx, req.ID, result)
}
@@ -701,6 +757,56 @@ func handleToolsCall(ctx *context_service.APIContext, req *MCPRequest) {
result, err = toolGetPackageDefaults(ctx, params.Arguments)
case "list_repos":
result, err = toolListRepos(ctx, params.Arguments)
case "list_issues":
result, err = toolListIssues(ctx, params.Arguments)
case "get_issue":
result, err = toolGetIssue(ctx, params.Arguments)
// Landing Pages tools
case "get_landing_config":
result, err = toolGetLandingConfig(ctx, params.Arguments)
case "list_landing_templates":
result, err = toolListLandingTemplates(ctx, params.Arguments)
case "enable_landing_page":
result, err = toolEnableLandingPage(ctx, params.Arguments)
case "update_landing_brand":
result, err = toolUpdateLandingBrand(ctx, params.Arguments)
case "update_landing_hero":
result, err = toolUpdateLandingHero(ctx, params.Arguments)
case "update_landing_pricing":
result, err = toolUpdateLandingPricing(ctx, params.Arguments)
case "update_landing_comparison":
result, err = toolUpdateLandingComparison(ctx, params.Arguments)
case "update_landing_features":
result, err = toolUpdateLandingFeatures(ctx, params.Arguments)
case "update_landing_social_proof":
result, err = toolUpdateLandingSocialProof(ctx, params.Arguments)
case "update_landing_seo":
result, err = toolUpdateLandingSEO(ctx, params.Arguments)
case "update_landing_theme":
result, err = toolUpdateLandingTheme(ctx, params.Arguments)
case "update_landing_stats":
result, err = toolUpdateLandingStats(ctx, params.Arguments)
case "update_landing_value_props":
result, err = toolUpdateLandingValueProps(ctx, params.Arguments)
case "update_landing_cta":
result, err = toolUpdateLandingCTA(ctx, params.Arguments)
// Organization tools
case "list_orgs":
result, err = toolListOrgs(ctx, params.Arguments)
case "get_org_overview":
result, err = toolGetOrgOverview(ctx, params.Arguments)
case "update_org":
result, err = toolUpdateOrg(ctx, params.Arguments)
case "list_org_repos":
result, err = toolListOrgRepos(ctx, params.Arguments)
case "get_org_profile_readme":
result, err = toolGetOrgProfileReadme(ctx, params.Arguments)
case "update_org_profile_readme":
result, err = toolUpdateOrgProfileReadme(ctx, params.Arguments)
case "pin_org_repo":
result, err = toolPinOrgRepo(ctx, params.Arguments)
case "unpin_org_repo":
result, err = toolUnpinOrgRepo(ctx, params.Arguments)
default:
sendMCPError(ctx, req.ID, -32602, "Unknown tool", params.Name)
return
@@ -2196,8 +2302,9 @@ func toolListRepos(ctx *context_service.APIContext, args map[string]any) (any, e
Page: 1,
PageSize: limit,
},
Actor: ctx.Doer,
OwnerID: ownerUser.ID,
Private: true,
Private: ctx.Doer != nil,
OrderBy: db.SearchOrderByAlphabetically,
Archived: optional.Some(false),
})
@@ -2229,6 +2336,201 @@ func toolListRepos(ctx *context_service.APIContext, args map[string]any) (any, e
}, nil
}
func toolListIssues(ctx *context_service.APIContext, args map[string]any) (any, error) {
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
if owner == "" || repo == "" {
return nil, errors.New("owner and repo are required")
}
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
if err != nil {
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
}
page := 1
if p, ok := args["page"].(float64); ok && p > 0 {
page = int(p)
}
limit := 20
if l, ok := args["limit"].(float64); ok && l > 0 {
limit = min(int(l), 100)
}
opts := &issue_model.IssuesOptions{
Paginator: &db.ListOptions{
Page: page,
PageSize: limit,
},
RepoIDs: []int64{repository.ID},
IsPull: optional.Some(false),
SortType: "newest",
}
state, _ := args["state"].(string)
switch state {
case "closed":
opts.IsClosed = optional.Some(true)
case "all":
// no filter
default:
opts.IsClosed = optional.Some(false)
}
issues, err := issue_model.Issues(ctx, opts)
if err != nil {
return nil, fmt.Errorf("failed to list issues: %w", err)
}
totalCount, err := issue_model.CountIssues(ctx, opts)
if err != nil {
return nil, fmt.Errorf("failed to count issues: %w", err)
}
if err := issues.LoadPosters(ctx); err != nil {
return nil, fmt.Errorf("failed to load posters: %w", err)
}
if err := issues.LoadLabels(ctx); err != nil {
return nil, fmt.Errorf("failed to load labels: %w", err)
}
items := make([]map[string]any, 0, len(issues))
for _, issue := range issues {
labels := make([]string, 0, len(issue.Labels))
for _, l := range issue.Labels {
labels = append(labels, l.Name)
}
posterName := ""
if issue.Poster != nil {
posterName = issue.Poster.Name
}
issueState := "open"
if issue.IsClosed {
issueState = "closed"
}
items = append(items, map[string]any{
"number": issue.Index,
"title": issue.Title,
"state": issueState,
"poster": posterName,
"labels": labels,
"num_comments": issue.NumComments,
"created_at": issue.CreatedUnix.AsTime().Format(time.RFC3339),
"updated_at": issue.UpdatedUnix.AsTime().Format(time.RFC3339),
"url": fmt.Sprintf("%s%s/%s/issues/%d", setting.AppURL, owner, repo, issue.Index),
})
}
totalPages := (int(totalCount) + limit - 1) / limit
return map[string]any{
"repository": fmt.Sprintf("%s/%s", owner, repo),
"total_count": totalCount,
"page": page,
"page_size": limit,
"total_pages": totalPages,
"issues": items,
}, nil
}
func toolGetIssue(ctx *context_service.APIContext, args map[string]any) (any, error) {
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
if owner == "" || repo == "" {
return nil, errors.New("owner and repo are required")
}
number, ok := args["number"].(float64)
if !ok || number < 1 {
return nil, errors.New("number is required and must be a positive integer")
}
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
if err != nil {
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
}
issue, err := issue_model.GetIssueByIndex(ctx, repository.ID, int64(number))
if err != nil {
return nil, fmt.Errorf("issue #%d not found", int64(number))
}
if err := issue.LoadPoster(ctx); err != nil {
return nil, fmt.Errorf("failed to load poster: %w", err)
}
if err := issue.LoadLabels(ctx); err != nil {
return nil, fmt.Errorf("failed to load labels: %w", err)
}
labels := make([]string, 0, len(issue.Labels))
for _, l := range issue.Labels {
labels = append(labels, l.Name)
}
posterName := ""
if issue.Poster != nil {
posterName = issue.Poster.Name
}
state := "open"
if issue.IsClosed {
state = "closed"
}
result := map[string]any{
"number": issue.Index,
"title": issue.Title,
"state": state,
"body": issue.Content,
"poster": posterName,
"labels": labels,
"num_comments": issue.NumComments,
"created_at": issue.CreatedUnix.AsTime().Format(time.RFC3339),
"updated_at": issue.UpdatedUnix.AsTime().Format(time.RFC3339),
"url": fmt.Sprintf("%s%s/%s/issues/%d", setting.AppURL, owner, repo, issue.Index),
}
if issue.IsClosed && !issue.ClosedUnix.IsZero() {
result["closed_at"] = issue.ClosedUnix.AsTime().Format(time.RFC3339)
}
// Load comments
comments, err := issue_model.FindComments(ctx, &issue_model.FindCommentsOptions{
IssueID: issue.ID,
Type: issue_model.CommentTypeComment,
})
if err != nil {
return nil, fmt.Errorf("failed to load comments: %w", err)
}
if len(comments) > 0 {
if err := comments.LoadPosters(ctx); err != nil {
return nil, fmt.Errorf("failed to load comment posters: %w", err)
}
commentItems := make([]map[string]any, 0, len(comments))
for _, c := range comments {
cPoster := ""
if c.Poster != nil {
cPoster = c.Poster.Name
}
commentItems = append(commentItems, map[string]any{
"id": c.ID,
"body": c.Content,
"poster": cPoster,
"created_at": c.CreatedUnix.AsTime().Format(time.RFC3339),
})
}
result["comments"] = commentItems
}
return result, nil
}
// validateWorkflowContent runs YAML and workflow-structural validation on raw workflow content.
// Returns a list of error strings (empty means valid) and a list of warning strings.
func validateWorkflowContent(content []byte) (errs, warnings []string) {

Some files were not shown because too many files have changed in this diff Show More