feat(packages): add package defaults configuration for orgs
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Failing after 3m5s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m59s
Build and Release / Lint (push) Successful in 5m6s
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
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Failing after 3m5s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m59s
Build and Release / Lint (push) Successful in 5m6s
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 ability for organizations to preconfigure default package metadata (authors, company, copyright, icon) that AI tools can use when building packages. Includes database model, org settings UI with icon upload, MCP tool for retrieving defaults with repo-specific URLs, and localization strings.
This commit is contained in:
56
models/packages/package_defaults.go
Normal file
56
models/packages/package_defaults.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright 2026 The GitCaddy Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(PackageDefaults))
|
||||
}
|
||||
|
||||
// PackageDefaults stores preconfigured package metadata defaults for an organization or user.
|
||||
// These values are used by AI tools when building packages (e.g., NuGet .csproj metadata).
|
||||
type PackageDefaults struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerID int64 `xorm:"UNIQUE INDEX NOT NULL"`
|
||||
Authors string `xorm:"TEXT"`
|
||||
Company string `xorm:"VARCHAR(255)"`
|
||||
Copyright string `xorm:"VARCHAR(512)"`
|
||||
IconPath string `xorm:"VARCHAR(255)"` // relative path in avatar storage (package-icons/{ownerID})
|
||||
}
|
||||
|
||||
// GetPackageDefaultsByOwnerID returns the package defaults for the given owner.
|
||||
// Returns an empty PackageDefaults (with OwnerID set) if none exist yet.
|
||||
func GetPackageDefaultsByOwnerID(ctx context.Context, ownerID int64) (*PackageDefaults, error) {
|
||||
defaults := &PackageDefaults{OwnerID: ownerID}
|
||||
has, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Get(defaults)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return &PackageDefaults{OwnerID: ownerID}, nil
|
||||
}
|
||||
return defaults, nil
|
||||
}
|
||||
|
||||
// CreateOrUpdatePackageDefaults creates or updates the package defaults for the given owner.
|
||||
func CreateOrUpdatePackageDefaults(ctx context.Context, defaults *PackageDefaults) error {
|
||||
existing := &PackageDefaults{}
|
||||
has, err := db.GetEngine(ctx).Where("owner_id = ?", defaults.OwnerID).Get(existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if has {
|
||||
defaults.ID = existing.ID
|
||||
_, err = db.GetEngine(ctx).ID(defaults.ID).AllCols().Update(defaults)
|
||||
} else {
|
||||
_, err = db.GetEngine(ctx).Insert(defaults)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -504,6 +504,19 @@
|
||||
"packages.settings.unlink.error": "Failed to remove repository link.",
|
||||
"packages.settings.unlink.success": "Repository link was successfully removed.",
|
||||
"packages.settings.global_access.url": "Global URL",
|
||||
"packages.settings.preconfigure": "Preconfigure Defaults",
|
||||
"packages.settings.preconfigure.description": "Set default package metadata for this organization. These values are used by AI tools when building packages.",
|
||||
"packages.settings.preconfigure.authors": "Authors",
|
||||
"packages.settings.preconfigure.authors.placeholder": "e.g. David H. Friedel Jr",
|
||||
"packages.settings.preconfigure.company": "Company",
|
||||
"packages.settings.preconfigure.company.placeholder": "e.g. MarketAlly",
|
||||
"packages.settings.preconfigure.copyright": "Copyright",
|
||||
"packages.settings.preconfigure.copyright.placeholder": "e.g. Copyright © 2026 MarketAlly",
|
||||
"packages.settings.preconfigure.icon": "Default Package Icon",
|
||||
"packages.settings.preconfigure.icon.upload": "Upload Icon",
|
||||
"packages.settings.preconfigure.saved": "Package defaults saved successfully.",
|
||||
"packages.settings.preconfigure.icon.saved": "Package icon uploaded successfully.",
|
||||
"packages.settings.preconfigure.icon.error": "Failed to upload package icon.",
|
||||
"packages.owner.settings.cargo.rebuild.success": "The Cargo index was successfully rebuilt.",
|
||||
"secrets.secrets": "Secrets",
|
||||
"actions.actions": "Actions",
|
||||
|
||||
@@ -3682,6 +3682,19 @@
|
||||
"packages.settings.global_access.disabled": "Package global access has been disabled.",
|
||||
"packages.settings.global_access.error": "Failed to update global access setting.",
|
||||
"packages.settings.global_access.url": "Global URL",
|
||||
"packages.settings.preconfigure": "Preconfigure Defaults",
|
||||
"packages.settings.preconfigure.description": "Set default package metadata for this organization. These values are used by AI tools when building packages.",
|
||||
"packages.settings.preconfigure.authors": "Authors",
|
||||
"packages.settings.preconfigure.authors.placeholder": "e.g. David H. Friedel Jr",
|
||||
"packages.settings.preconfigure.company": "Company",
|
||||
"packages.settings.preconfigure.company.placeholder": "e.g. MarketAlly",
|
||||
"packages.settings.preconfigure.copyright": "Copyright",
|
||||
"packages.settings.preconfigure.copyright.placeholder": "e.g. Copyright © 2026 MarketAlly",
|
||||
"packages.settings.preconfigure.icon": "Default Package Icon",
|
||||
"packages.settings.preconfigure.icon.upload": "Upload Icon",
|
||||
"packages.settings.preconfigure.saved": "Package defaults saved successfully.",
|
||||
"packages.settings.preconfigure.icon.saved": "Package icon uploaded successfully.",
|
||||
"packages.settings.preconfigure.icon.error": "Failed to upload package icon.",
|
||||
"packages.visibility": "Visibility",
|
||||
"packages.settings.visibility.private.text": "This package is currently private. Make it public to allow anyone to access it.",
|
||||
"packages.settings.visibility.private.button": "Make Private",
|
||||
|
||||
@@ -500,6 +500,24 @@ var mcpTools = []MCPTool{
|
||||
"required": []string{"owner", "repo", "run_id", "artifact_name"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_package_defaults",
|
||||
Description: "Get preconfigured package defaults for an organization, with repo-specific URLs filled in. Returns authors, company, copyright, icon URL, and repository URLs for building valid package metadata.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Organization or user name",
|
||||
},
|
||||
"repo": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository name (optional, for generating repo-specific URLs)",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// MCPHandler handles MCP protocol requests
|
||||
@@ -623,6 +641,8 @@ func handleToolsCall(ctx *context_service.APIContext, req *MCPRequest) {
|
||||
result, err = toolGetCompatibilityMatrix(ctx, params.Arguments)
|
||||
case "diagnose_job_failure":
|
||||
result, err = toolDiagnoseJobFailure(ctx, params.Arguments)
|
||||
case "get_package_defaults":
|
||||
result, err = toolGetPackageDefaults(ctx, params.Arguments)
|
||||
default:
|
||||
sendMCPError(ctx, req.ID, -32602, "Unknown tool", params.Name)
|
||||
return
|
||||
@@ -2004,3 +2024,43 @@ func toolGetArtifactDownloadURL(ctx *context_service.APIContext, args map[string
|
||||
"download_url": downloadURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolGetPackageDefaults(ctx *context_service.APIContext, args map[string]any) (any, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
|
||||
if owner == "" {
|
||||
return nil, errors.New("owner is required")
|
||||
}
|
||||
|
||||
ownerUser, err := user_model.GetUserByName(ctx, owner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("owner not found: %s", owner)
|
||||
}
|
||||
|
||||
defaults, err := packages_model.GetPackageDefaultsByOwnerID(ctx, ownerUser.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get package defaults: %w", err)
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"owner": owner,
|
||||
"authors": defaults.Authors,
|
||||
"company": defaults.Company,
|
||||
"copyright": defaults.Copyright,
|
||||
}
|
||||
|
||||
// Fill in repo-specific URLs if repo is provided
|
||||
if repo != "" {
|
||||
baseURL := setting.AppURL + owner + "/" + repo
|
||||
result["repository_url"] = baseURL
|
||||
result["package_project_url"] = baseURL
|
||||
}
|
||||
|
||||
// Include icon URL if an icon has been uploaded
|
||||
if defaults.IconPath != "" {
|
||||
result["icon_url"] = setting.AppURL + owner + "/-/package-icon"
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -5,10 +5,15 @@ package org
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
packages_model "code.gitcaddy.com/server/v3/models/packages"
|
||||
"code.gitcaddy.com/server/v3/modules/avatar"
|
||||
"code.gitcaddy.com/server/v3/modules/setting"
|
||||
"code.gitcaddy.com/server/v3/modules/storage"
|
||||
"code.gitcaddy.com/server/v3/modules/templates"
|
||||
"code.gitcaddy.com/server/v3/modules/typesniffer"
|
||||
shared "code.gitcaddy.com/server/v3/routers/web/shared/packages"
|
||||
shared_user "code.gitcaddy.com/server/v3/routers/web/shared/user"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
@@ -30,11 +35,99 @@ func Packages(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
defaults, err := packages_model.GetPackageDefaultsByOwnerID(ctx, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetPackageDefaultsByOwnerID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["PackageDefaults"] = defaults
|
||||
|
||||
shared.SetPackagesContext(ctx, ctx.ContextUser)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsPackages)
|
||||
}
|
||||
|
||||
func PackageDefaultsPost(ctx *context.Context) {
|
||||
defaults, err := packages_model.GetPackageDefaultsByOwnerID(ctx, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetPackageDefaultsByOwnerID", err)
|
||||
return
|
||||
}
|
||||
|
||||
defaults.Authors = ctx.FormString("authors")
|
||||
defaults.Company = ctx.FormString("company")
|
||||
defaults.Copyright = ctx.FormString("copyright")
|
||||
|
||||
if err := packages_model.CreateOrUpdatePackageDefaults(ctx, defaults); err != nil {
|
||||
ctx.ServerError("CreateOrUpdatePackageDefaults", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("packages.settings.preconfigure.saved"))
|
||||
ctx.Redirect(fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name))
|
||||
}
|
||||
|
||||
func PackageDefaultsIconPost(ctx *context.Context) {
|
||||
file, header, err := ctx.Req.FormFile("icon")
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("packages.settings.preconfigure.icon.error"))
|
||||
ctx.Redirect(fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name))
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if header.Size > setting.Avatar.MaxFileSize {
|
||||
ctx.Flash.Error(ctx.Tr("settings.uploaded_avatar_is_too_big", header.Size/1024, setting.Avatar.MaxFileSize/1024))
|
||||
ctx.Redirect(fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name))
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
ctx.ServerError("io.ReadAll", err)
|
||||
return
|
||||
}
|
||||
|
||||
st := typesniffer.DetectContentType(data)
|
||||
if !(st.IsImage() && !st.IsSvgImage()) {
|
||||
ctx.Flash.Error(ctx.Tr("settings.uploaded_avatar_not_a_image"))
|
||||
ctx.Redirect(fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name))
|
||||
return
|
||||
}
|
||||
|
||||
// Process/resize the image
|
||||
processedData, err := avatar.ProcessAvatarImage(data)
|
||||
if err != nil {
|
||||
ctx.ServerError("ProcessAvatarImage", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Save to storage
|
||||
iconPath := fmt.Sprintf("package-icons/%d", ctx.ContextUser.ID)
|
||||
if err := storage.SaveFrom(storage.Avatars, iconPath, func(w io.Writer) error {
|
||||
_, err := w.Write(processedData)
|
||||
return err
|
||||
}); err != nil {
|
||||
ctx.ServerError("storage.SaveFrom", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update database
|
||||
defaults, err := packages_model.GetPackageDefaultsByOwnerID(ctx, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetPackageDefaultsByOwnerID", err)
|
||||
return
|
||||
}
|
||||
defaults.IconPath = iconPath
|
||||
if err := packages_model.CreateOrUpdatePackageDefaults(ctx, defaults); err != nil {
|
||||
ctx.ServerError("CreateOrUpdatePackageDefaults", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("packages.settings.preconfigure.icon.saved"))
|
||||
ctx.Redirect(fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name))
|
||||
}
|
||||
|
||||
func PackagesRuleAdd(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("packages.title")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
|
||||
45
routers/web/user/package_icon.go
Normal file
45
routers/web/user/package_icon.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2026 The GitCaddy Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
packages_model "code.gitcaddy.com/server/v3/models/packages"
|
||||
"code.gitcaddy.com/server/v3/modules/httpcache"
|
||||
"code.gitcaddy.com/server/v3/modules/storage"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
)
|
||||
|
||||
// PackageIcon serves the default package icon for an organization or user.
|
||||
func PackageIcon(ctx *context.Context) {
|
||||
defaults, err := packages_model.GetPackageDefaultsByOwnerID(ctx, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetPackageDefaultsByOwnerID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if defaults.IconPath == "" {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
f, err := storage.Avatars.Open(defaults.IconPath)
|
||||
if err != nil {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
ctx.ServerError("Stat", err)
|
||||
return
|
||||
}
|
||||
|
||||
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{MaxAge: 5 * time.Minute})
|
||||
ctx.Resp.Header().Set("Content-Type", "image/png")
|
||||
http.ServeContent(ctx.Resp, ctx.Req, "package-icon.png", info.ModTime(), f)
|
||||
}
|
||||
@@ -1080,6 +1080,10 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/initialize", org.InitializeCargoIndex)
|
||||
m.Post("/rebuild", org.RebuildCargoIndex)
|
||||
})
|
||||
m.Group("/defaults", func() {
|
||||
m.Post("", org.PackageDefaultsPost)
|
||||
m.Post("/icon", org.PackageDefaultsIconPost)
|
||||
})
|
||||
}, packagesEnabled)
|
||||
|
||||
m.Group("/blocked_users", func() {
|
||||
@@ -1118,6 +1122,7 @@ func registerWebRoutes(m *web.Router) {
|
||||
})
|
||||
})
|
||||
}, context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
|
||||
m.Get("/package-icon", user.PackageIcon)
|
||||
}
|
||||
|
||||
m.Get("/repositories", org.Repositories)
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings packages")}}
|
||||
<div class="org-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "packages.settings.preconfigure"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p>{{ctx.Locale.Tr "packages.settings.preconfigure.description"}}</p>
|
||||
<form class="ui form" action="{{.OrgLink}}/settings/packages/defaults" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "packages.settings.preconfigure.authors"}}</label>
|
||||
<input name="authors" value="{{.PackageDefaults.Authors}}" placeholder="{{ctx.Locale.Tr "packages.settings.preconfigure.authors.placeholder"}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "packages.settings.preconfigure.company"}}</label>
|
||||
<input name="company" value="{{.PackageDefaults.Company}}" placeholder="{{ctx.Locale.Tr "packages.settings.preconfigure.company.placeholder"}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "packages.settings.preconfigure.copyright"}}</label>
|
||||
<input name="copyright" value="{{.PackageDefaults.Copyright}}" placeholder="{{ctx.Locale.Tr "packages.settings.preconfigure.copyright.placeholder"}}">
|
||||
</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
<div class="ui divider"></div>
|
||||
<h5>{{ctx.Locale.Tr "packages.settings.preconfigure.icon"}}</h5>
|
||||
<div class="tw-flex tw-items-center tw-gap-4">
|
||||
{{if .PackageDefaults.IconPath}}
|
||||
<img class="ui tiny image" src="{{AppSubUrl}}/{{.ContextUser.Name}}/-/package-icon" width="64" height="64">
|
||||
{{end}}
|
||||
<form class="ui form" action="{{.OrgLink}}/settings/packages/defaults/icon" method="post" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input name="icon" type="file" accept="image/png,image/jpeg,image/gif">
|
||||
<button class="ui primary button tw-mt-2">{{ctx.Locale.Tr "packages.settings.preconfigure.icon.upload"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{template "package/shared/cleanup_rules/list" .}}
|
||||
{{template "package/shared/cargo" .}}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user