feat: add deploy tokens (#37306)

Deploy keys only work over SSH. A deploy token is their counterpart for HTTPS: a repository scoped credential, used as the password of a Git request, with read or read and write access. It covers Git operations and LFS, and can be regenerated in place.

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Claude Mythos <noreply@anthropic.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
ToastyTheBot
2026-08-27 03:32:44 +08:00
committed by GitHub
parent 3c4d5a6a5c
commit 646ea0f253
76 changed files with 1594 additions and 831 deletions
+13 -10
View File
@@ -24,20 +24,23 @@ const (
// HookOptions represents the options for the Hook calls
type HookOptions struct {
OldCommitIDs []string
NewCommitIDs []string
RefFullNames []git.RefName
UserID int64
UserName string
IsWiki bool
OldCommitIDs []string
NewCommitIDs []string
RefFullNames []git.RefName
GitObjectDirectory string
GitAlternativeObjectDirectories string
GitQuarantinePath string
GitPushOptions GitPushOptions
PullRequestID int64
PushTrigger repository.PushTrigger
DeployKeyID int64 // if the pusher is a DeployKey, then UserID is the repo's org user.
IsWiki bool
ActionsTaskID int64 // if the pusher is an Actions user, the task ID
PullRequestID int64
PushTrigger repository.PushTrigger
UserID int64
UserName string
UserExtDoerData string
}
// SSHLogOption ssh log options
+12 -10
View File
@@ -33,16 +33,18 @@ func ServNoCommand(ctx context.Context, keyID int64) (*asymkey_model.PublicKey,
// ServCommandResults are the results of a call to the private route serv
type ServCommandResults struct {
IsWiki bool
DeployKeyID int64
KeyID int64 // public key
KeyName string // this field is ambiguous, it can be the name of DeployKey, or the name of the PublicKey
UserName string
UserEmail string
UserID int64
OwnerName string
RepoName string
RepoID int64
IsWiki bool
OwnerName string
RepoName string
RepoID int64
PublicKeyID int64
UserName string
UserEmail string
UserID int64
UserExtDoerData string
RepoStoragePath string
}
+19 -17
View File
@@ -16,21 +16,23 @@ import (
// env keys for git hooks need
const (
EnvRepoName = "GITEA_REPO_NAME"
EnvRepoUsername = "GITEA_REPO_USER_NAME"
EnvRepoID = "GITEA_REPO_ID"
EnvRepoIsWiki = "GITEA_REPO_IS_WIKI"
EnvPusherName = "GITEA_PUSHER_NAME"
EnvPusherEmail = "GITEA_PUSHER_EMAIL"
EnvPusherID = "GITEA_PUSHER_ID"
EnvKeyID = "GITEA_KEY_ID" // public key ID
EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID"
EnvPRID = "GITEA_PR_ID"
EnvPRIndex = "GITEA_PR_INDEX" // not used by Gitea at the moment, it is for custom git hooks
EnvPushTrigger = "GITEA_PUSH_TRIGGER"
EnvIsInternal = "GITEA_INTERNAL_PUSH"
EnvAppURL = "GITEA_ROOT_URL"
EnvActionsTaskID = "GITEA_ACTIONS_TASK_ID"
EnvRepoName = "GITEA_REPO_NAME"
EnvRepoUsername = "GITEA_REPO_USER_NAME" // owner name
EnvRepoID = "GITEA_REPO_ID"
EnvRepoIsWiki = "GITEA_REPO_IS_WIKI"
EnvKeyID = "GITEA_KEY_ID" // public key ID
EnvPusherName = "GITEA_PUSHER_NAME"
EnvPusherEmail = "GITEA_PUSHER_EMAIL"
EnvPusherID = "GITEA_PUSHER_ID"
EnvPusherExtDoerData = "GITEA_PUSHER_EXT_DOER_DATA"
EnvPRID = "GITEA_PR_ID"
EnvPRIndex = "GITEA_PR_INDEX" // not used by Gitea at the moment, it is for custom git hooks
EnvPushTrigger = "GITEA_PUSH_TRIGGER"
EnvIsInternal = "GITEA_INTERNAL_PUSH"
EnvAppURL = "GITEA_ROOT_URL"
)
type PushTrigger string
@@ -68,8 +70,8 @@ func DoerPushingEnvironment(doer *user_model.User, repo *repo_model.Repository,
if !doer.KeepEmailPrivate {
env = append(env, EnvPusherEmail+"="+doer.Email)
}
if taskID, isActionsUser := user_model.GetActionsUserTaskID(doer); isActionsUser {
env = append(env, EnvActionsTaskID+"="+strconv.FormatInt(taskID, 10))
if doer.ExtDoerData != nil {
env = append(env, EnvPusherExtDoerData+"="+doer.ExtDoerData.EncodeToString())
}
return env
}
+11 -4
View File
@@ -146,8 +146,15 @@ func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Co
}
}
// NewRequestContextForTest creates a new RequestContext for testing purposes
// It doesn't add the context to the process manager, nor do cleanup
func NewRequestContextForTest(parentCtx context.Context) RequestContext {
return &requestContext{Context: parentCtx, RequestDataStore: &requestDataStore{values: make(map[any]any)}}
type TestingT interface {
Cleanup(func())
Context() context.Context
}
// NewRequestContextForTest creates a new RequestContext for testing purposes
func NewRequestContextForTest(t TestingT) RequestContext {
store := &requestDataStore{values: make(map[any]any)}
ret := &requestContext{Context: t.Context(), RequestDataStore: store}
t.Cleanup(store.cleanUp)
return ret
}
+19 -8
View File
@@ -7,30 +7,33 @@ import (
"time"
)
// DeployKey a deploy key
type DeployKey struct {
// ID is the unique identifier for the deploy key
// ID is the unique identifier for the deploy-key
ID int64 `json:"id"`
// Type tells whether the key authenticates over SSH or with a token over HTTPS
// enum: ssh,token
KeyType string `json:"key_type"`
// KeyID is the associated public key ID
KeyID int64 `json:"key_id"`
// Key contains the actual SSH key content
Key string `json:"key"`
// URL is the API URL for this deploy key
// URL is the API URL for this deploy-key
URL string `json:"url"`
// Title is the human-readable name for the key
Title string `json:"title"`
// Fingerprint is the key's fingerprint
Fingerprint string `json:"fingerprint"`
// Token is the plaintext token of an HTTPS key, only returned when it is created
Token string `json:"token,omitempty"`
// swagger:strfmt date-time
// Created is the time when the deploy key was added
// Created is the time when the deploy-key was added
Created time.Time `json:"created_at"`
// ReadOnly indicates if the key has read-only access
ReadOnly bool `json:"read_only"`
// Repository is the repository this deploy key belongs to
// Repository is the repository this deploy-key belongs to
Repository *Repository `json:"repository,omitempty"`
}
// CreateKeyOption options when creating a key
type CreateKeyOption struct {
// Title of the key to add
//
@@ -43,7 +46,15 @@ type CreateKeyOption struct {
// unique: true
Key string `json:"key" binding:"Required"`
// Describe if the key has only read access or read/write
//
// required: false
ReadOnly bool `json:"read_only"`
}
type CreateDeployKeyTokenOption struct {
// Title of the token to add
//
// required: true
// unique: true
Title string `json:"title" binding:"Required;MaxSize(50)"`
// Describe if the token has only read access or read/write
ReadOnly bool `json:"read_only"`
}
@@ -15,7 +15,7 @@ import (
)
func TestRenderTimelineEventComment(t *testing.T) {
ctx := reqctx.NewRequestContextForTest(t.Context())
ctx := reqctx.NewRequestContextForTest(t)
ctx.SetContextValue(translation.ContextKey, &translation.MockLocale{})
ut := &RenderUtils{ctx: ctx}
var createdStr template.HTML = "(created-at)"
+1 -1
View File
@@ -62,7 +62,7 @@ func TestMain(m *testing.M) {
}
func newTestRenderUtils(t *testing.T) *RenderUtils {
ctx := reqctx.NewRequestContextForTest(t.Context())
ctx := reqctx.NewRequestContextForTest(t)
ctx.SetContextValue(translation.ContextKey, &translation.MockLocale{})
return NewRenderUtils(ctx)
}