feat: Merge main branch

This commit is contained in:
Pascal Zimmermann
2026-04-11 23:31:01 +02:00
1185 changed files with 39208 additions and 23019 deletions
+190 -417
View File
@@ -3,8 +3,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
// protoc v4.25.2
// protoc-gen-go v1.36.11
// protoc v7.34.0
// source: artifact.proto
package actions
@@ -12,6 +12,7 @@ package actions
import (
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
@@ -27,24 +28,22 @@ const (
)
type CreateArtifactRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
Version int32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
Version int32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"`
MimeType *wrapperspb.StringValue `protobuf:"bytes,6,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateArtifactRequest) Reset() {
*x = CreateArtifactRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_artifact_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_artifact_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateArtifactRequest) String() string {
@@ -55,7 +54,7 @@ func (*CreateArtifactRequest) ProtoMessage() {}
func (x *CreateArtifactRequest) ProtoReflect() protoreflect.Message {
mi := &file_artifact_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -105,22 +104,26 @@ func (x *CreateArtifactRequest) GetVersion() int32 {
return 0
}
type CreateArtifactResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
func (x *CreateArtifactRequest) GetMimeType() *wrapperspb.StringValue {
if x != nil {
return x.MimeType
}
return nil
}
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"`
type CreateArtifactResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateArtifactResponse) Reset() {
*x = CreateArtifactResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_artifact_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_artifact_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateArtifactResponse) String() string {
@@ -131,7 +134,7 @@ func (*CreateArtifactResponse) ProtoMessage() {}
func (x *CreateArtifactResponse) ProtoReflect() protoreflect.Message {
mi := &file_artifact_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -161,24 +164,21 @@ func (x *CreateArtifactResponse) GetSignedUploadUrl() string {
}
type FinalizeArtifactRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState `protogen:"open.v1"`
WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"`
Hash *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=hash,proto3" json:"hash,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *FinalizeArtifactRequest) Reset() {
*x = FinalizeArtifactRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_artifact_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_artifact_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FinalizeArtifactRequest) String() string {
@@ -189,7 +189,7 @@ func (*FinalizeArtifactRequest) ProtoMessage() {}
func (x *FinalizeArtifactRequest) ProtoReflect() protoreflect.Message {
mi := &file_artifact_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -240,21 +240,18 @@ func (x *FinalizeArtifactRequest) GetHash() *wrapperspb.StringValue {
}
type FinalizeArtifactResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"`
unknownFields protoimpl.UnknownFields
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *FinalizeArtifactResponse) Reset() {
*x = FinalizeArtifactResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_artifact_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_artifact_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FinalizeArtifactResponse) String() string {
@@ -265,7 +262,7 @@ func (*FinalizeArtifactResponse) ProtoMessage() {}
func (x *FinalizeArtifactResponse) ProtoReflect() protoreflect.Message {
mi := &file_artifact_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -295,23 +292,20 @@ func (x *FinalizeArtifactResponse) GetArtifactId() int64 {
}
type ListArtifactsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState `protogen:"open.v1"`
WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
NameFilter *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=name_filter,json=nameFilter,proto3" json:"name_filter,omitempty"`
IdFilter *wrapperspb.Int64Value `protobuf:"bytes,4,opt,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListArtifactsRequest) Reset() {
*x = ListArtifactsRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_artifact_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_artifact_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListArtifactsRequest) String() string {
@@ -322,7 +316,7 @@ func (*ListArtifactsRequest) ProtoMessage() {}
func (x *ListArtifactsRequest) ProtoReflect() protoreflect.Message {
mi := &file_artifact_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -366,20 +360,17 @@ func (x *ListArtifactsRequest) GetIdFilter() *wrapperspb.Int64Value {
}
type ListArtifactsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"`
unknownFields protoimpl.UnknownFields
Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ListArtifactsResponse) Reset() {
*x = ListArtifactsResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_artifact_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_artifact_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListArtifactsResponse) String() string {
@@ -390,7 +381,7 @@ func (*ListArtifactsResponse) ProtoMessage() {}
func (x *ListArtifactsResponse) ProtoReflect() protoreflect.Message {
mi := &file_artifact_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -413,25 +404,22 @@ func (x *ListArtifactsResponse) GetArtifacts() []*ListArtifactsResponse_Monolith
}
type ListArtifactsResponse_MonolithArtifact struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState `protogen:"open.v1"`
WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
DatabaseId int64 `protobuf:"varint,3,opt,name=database_id,json=databaseId,proto3" json:"database_id,omitempty"`
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListArtifactsResponse_MonolithArtifact) Reset() {
*x = ListArtifactsResponse_MonolithArtifact{}
if protoimpl.UnsafeEnabled {
mi := &file_artifact_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_artifact_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListArtifactsResponse_MonolithArtifact) String() string {
@@ -442,7 +430,7 @@ func (*ListArtifactsResponse_MonolithArtifact) ProtoMessage() {}
func (x *ListArtifactsResponse_MonolithArtifact) ProtoReflect() protoreflect.Message {
mi := &file_artifact_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -500,22 +488,19 @@ func (x *ListArtifactsResponse_MonolithArtifact) GetCreatedAt() *timestamppb.Tim
}
type GetSignedArtifactURLRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetSignedArtifactURLRequest) Reset() {
*x = GetSignedArtifactURLRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_artifact_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_artifact_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetSignedArtifactURLRequest) String() string {
@@ -526,7 +511,7 @@ func (*GetSignedArtifactURLRequest) ProtoMessage() {}
func (x *GetSignedArtifactURLRequest) ProtoReflect() protoreflect.Message {
mi := &file_artifact_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -563,20 +548,17 @@ func (x *GetSignedArtifactURLRequest) GetName() string {
}
type GetSignedArtifactURLResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"`
unknownFields protoimpl.UnknownFields
SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *GetSignedArtifactURLResponse) Reset() {
*x = GetSignedArtifactURLResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_artifact_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_artifact_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetSignedArtifactURLResponse) String() string {
@@ -587,7 +569,7 @@ func (*GetSignedArtifactURLResponse) ProtoMessage() {}
func (x *GetSignedArtifactURLResponse) ProtoReflect() protoreflect.Message {
mi := &file_artifact_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -610,22 +592,19 @@ func (x *GetSignedArtifactURLResponse) GetSignedUrl() string {
}
type DeleteArtifactRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteArtifactRequest) Reset() {
*x = DeleteArtifactRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_artifact_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_artifact_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteArtifactRequest) String() string {
@@ -636,7 +615,7 @@ func (*DeleteArtifactRequest) ProtoMessage() {}
func (x *DeleteArtifactRequest) ProtoReflect() protoreflect.Message {
mi := &file_artifact_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -673,21 +652,18 @@ func (x *DeleteArtifactRequest) GetName() string {
}
type DeleteArtifactResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"`
unknownFields protoimpl.UnknownFields
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *DeleteArtifactResponse) Reset() {
*x = DeleteArtifactResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_artifact_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_artifact_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteArtifactResponse) String() string {
@@ -698,7 +674,7 @@ func (*DeleteArtifactResponse) ProtoMessage() {}
func (x *DeleteArtifactResponse) ProtoReflect() protoreflect.Message {
mi := &file_artifact_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -729,173 +705,105 @@ func (x *DeleteArtifactResponse) GetArtifactId() int64 {
var File_artifact_proto protoreflect.FileDescriptor
var file_artifact_proto_rawDesc = []byte{
0x0a, 0x0e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x12, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73,
0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a,
0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x22, 0xf5, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66,
0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f,
0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72,
0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49,
0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f,
0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77,
0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61,
0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18,
0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52,
0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61,
0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02,
0x6f, 0x6b, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x75, 0x70, 0x6c,
0x6f, 0x61, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73,
0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xe8,
0x01, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66,
0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f,
0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72,
0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49,
0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f,
0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77,
0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18,
0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61,
0x6c, 0x75, 0x65, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x4b, 0x0a, 0x18, 0x46, 0x69, 0x6e,
0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28,
0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63,
0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69,
0x66, 0x61, 0x63, 0x74, 0x49, 0x64, 0x22, 0x84, 0x02, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41,
0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f,
0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72,
0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65,
0x6e, 0x64, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x69, 0x6c,
0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69,
0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x69, 0x6c,
0x74, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72,
0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61,
0x6c, 0x75, 0x65, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x7c, 0x0a,
0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61,
0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c,
0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72,
0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f,
0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74,
0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x26,
0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72,
0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f,
0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a,
0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75,
0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52,
0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x64,
0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04,
0x73, 0x69, 0x7a, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f,
0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22,
0xa6, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74,
0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f,
0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72,
0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65,
0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01,
0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x53,
0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e,
0x65, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69,
0x67, 0x6e, 0x65, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xa0, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65,
0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75,
0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42,
0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b,
0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63,
0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77,
0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x44, 0x65,
0x6c, 0x65, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74,
0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66,
0x61, 0x63, 0x74, 0x49, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
const file_artifact_proto_rawDesc = "" +
"\n" +
"\x0eartifact.proto\x12\x1dgithub.actions.results.api.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\xb0\x02\n" +
"\x15CreateArtifactRequest\x125\n" +
"\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" +
"\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" +
"\x04name\x18\x03 \x01(\tR\x04name\x129\n" +
"\n" +
"expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12\x18\n" +
"\aversion\x18\x05 \x01(\x05R\aversion\x129\n" +
"\tmime_type\x18\x06 \x01(\v2\x1c.google.protobuf.StringValueR\bmimeType\"T\n" +
"\x16CreateArtifactResponse\x12\x0e\n" +
"\x02ok\x18\x01 \x01(\bR\x02ok\x12*\n" +
"\x11signed_upload_url\x18\x02 \x01(\tR\x0fsignedUploadUrl\"\xe8\x01\n" +
"\x17FinalizeArtifactRequest\x125\n" +
"\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" +
"\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" +
"\x04name\x18\x03 \x01(\tR\x04name\x12\x12\n" +
"\x04size\x18\x04 \x01(\x03R\x04size\x120\n" +
"\x04hash\x18\x05 \x01(\v2\x1c.google.protobuf.StringValueR\x04hash\"K\n" +
"\x18FinalizeArtifactResponse\x12\x0e\n" +
"\x02ok\x18\x01 \x01(\bR\x02ok\x12\x1f\n" +
"\vartifact_id\x18\x02 \x01(\x03R\n" +
"artifactId\"\x84\x02\n" +
"\x14ListArtifactsRequest\x125\n" +
"\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" +
"\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12=\n" +
"\vname_filter\x18\x03 \x01(\v2\x1c.google.protobuf.StringValueR\n" +
"nameFilter\x128\n" +
"\tid_filter\x18\x04 \x01(\v2\x1b.google.protobuf.Int64ValueR\bidFilter\"|\n" +
"\x15ListArtifactsResponse\x12c\n" +
"\tartifacts\x18\x01 \x03(\v2E.github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifactR\tartifacts\"\xa1\x02\n" +
"&ListArtifactsResponse_MonolithArtifact\x125\n" +
"\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" +
"\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x1f\n" +
"\vdatabase_id\x18\x03 \x01(\x03R\n" +
"databaseId\x12\x12\n" +
"\x04name\x18\x04 \x01(\tR\x04name\x12\x12\n" +
"\x04size\x18\x05 \x01(\x03R\x04size\x129\n" +
"\n" +
"created_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\xa6\x01\n" +
"\x1bGetSignedArtifactURLRequest\x125\n" +
"\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" +
"\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" +
"\x04name\x18\x03 \x01(\tR\x04name\"=\n" +
"\x1cGetSignedArtifactURLResponse\x12\x1d\n" +
"\n" +
"signed_url\x18\x01 \x01(\tR\tsignedUrl\"\xa0\x01\n" +
"\x15DeleteArtifactRequest\x125\n" +
"\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" +
"\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" +
"\x04name\x18\x03 \x01(\tR\x04name\"I\n" +
"\x16DeleteArtifactResponse\x12\x0e\n" +
"\x02ok\x18\x01 \x01(\bR\x02ok\x12\x1f\n" +
"\vartifact_id\x18\x02 \x01(\x03R\n" +
"artifactIdB)Z'code.gitea.io/gitea/routers/api/actionsb\x06proto3"
var (
file_artifact_proto_rawDescOnce sync.Once
file_artifact_proto_rawDescData = file_artifact_proto_rawDesc
file_artifact_proto_rawDescData []byte
)
func file_artifact_proto_rawDescGZIP() []byte {
file_artifact_proto_rawDescOnce.Do(func() {
file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(file_artifact_proto_rawDescData)
file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_artifact_proto_rawDesc), len(file_artifact_proto_rawDesc)))
})
return file_artifact_proto_rawDescData
}
var (
file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
file_artifact_proto_goTypes = []interface{}{
(*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest
(*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse
(*FinalizeArtifactRequest)(nil), // 2: github.actions.results.api.v1.FinalizeArtifactRequest
(*FinalizeArtifactResponse)(nil), // 3: github.actions.results.api.v1.FinalizeArtifactResponse
(*ListArtifactsRequest)(nil), // 4: github.actions.results.api.v1.ListArtifactsRequest
(*ListArtifactsResponse)(nil), // 5: github.actions.results.api.v1.ListArtifactsResponse
(*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact
(*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest
(*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse
(*DeleteArtifactRequest)(nil), // 9: github.actions.results.api.v1.DeleteArtifactRequest
(*DeleteArtifactResponse)(nil), // 10: github.actions.results.api.v1.DeleteArtifactResponse
(*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp
(*wrapperspb.StringValue)(nil), // 12: google.protobuf.StringValue
(*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value
}
)
var file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_artifact_proto_goTypes = []any{
(*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest
(*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse
(*FinalizeArtifactRequest)(nil), // 2: github.actions.results.api.v1.FinalizeArtifactRequest
(*FinalizeArtifactResponse)(nil), // 3: github.actions.results.api.v1.FinalizeArtifactResponse
(*ListArtifactsRequest)(nil), // 4: github.actions.results.api.v1.ListArtifactsRequest
(*ListArtifactsResponse)(nil), // 5: github.actions.results.api.v1.ListArtifactsResponse
(*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact
(*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest
(*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse
(*DeleteArtifactRequest)(nil), // 9: github.actions.results.api.v1.DeleteArtifactRequest
(*DeleteArtifactResponse)(nil), // 10: github.actions.results.api.v1.DeleteArtifactResponse
(*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp
(*wrapperspb.StringValue)(nil), // 12: google.protobuf.StringValue
(*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value
}
var file_artifact_proto_depIdxs = []int32{
11, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp
12, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue
12, // 2: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue
13, // 3: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value
6, // 4: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact
11, // 5: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp
6, // [6:6] is the sub-list for method output_type
6, // [6:6] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
12, // 1: github.actions.results.api.v1.CreateArtifactRequest.mime_type:type_name -> google.protobuf.StringValue
12, // 2: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue
12, // 3: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue
13, // 4: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value
6, // 5: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact
11, // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp
7, // [7:7] is the sub-list for method output_type
7, // [7:7] is the sub-list for method input_type
7, // [7:7] is the sub-list for extension type_name
7, // [7:7] is the sub-list for extension extendee
0, // [0:7] is the sub-list for field type_name
}
func init() { file_artifact_proto_init() }
@@ -903,145 +811,11 @@ func file_artifact_proto_init() {
if File_artifact_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_artifact_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateArtifactRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_artifact_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateArtifactResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_artifact_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*FinalizeArtifactRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_artifact_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*FinalizeArtifactResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_artifact_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListArtifactsRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_artifact_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListArtifactsResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_artifact_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListArtifactsResponse_MonolithArtifact); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_artifact_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetSignedArtifactURLRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_artifact_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetSignedArtifactURLResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_artifact_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DeleteArtifactRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_artifact_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DeleteArtifactResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_artifact_proto_rawDesc,
RawDescriptor: unsafe.Slice(unsafe.StringData(file_artifact_proto_rawDesc), len(file_artifact_proto_rawDesc)),
NumEnums: 0,
NumMessages: 11,
NumExtensions: 0,
@@ -1052,7 +826,6 @@ func file_artifact_proto_init() {
MessageInfos: file_artifact_proto_msgTypes,
}.Build()
File_artifact_proto = out.File
file_artifact_proto_rawDesc = nil
file_artifact_proto_goTypes = nil
file_artifact_proto_depIdxs = nil
}
+3
View File
@@ -5,12 +5,15 @@ import "google/protobuf/wrappers.proto";
package github.actions.results.api.v1;
option go_package = "code.gitea.io/gitea/routers/api/actions";
message CreateArtifactRequest {
string workflow_run_backend_id = 1;
string workflow_job_run_backend_id = 2;
string name = 3;
google.protobuf.Timestamp expires_at = 4;
int32 version = 5;
google.protobuf.StringValue mime_type = 6;
}
message CreateArtifactResponse {
+14 -14
View File
@@ -103,7 +103,7 @@ func init() {
func ArtifactsRoutes(prefix string) *web.Router {
m := web.NewRouter()
m.Use(ArtifactContexter())
m.AfterRouting(ArtifactContexter())
r := artifactRoutes{
prefix: prefix,
@@ -112,7 +112,7 @@ func ArtifactsRoutes(prefix string) *web.Router {
m.Group(artifactRouteBase, func() {
// retrieve, list and confirm artifacts
m.Combo("").Get(r.listArtifacts).Post(r.getUploadArtifactURL).Patch(r.comfirmUploadArtifact)
m.Combo("").Get(r.listArtifacts).Post(r.getUploadArtifactURL).Patch(r.confirmUploadArtifact)
// handle container artifacts list and download
m.Put("/{artifact_hash}/upload", r.uploadArtifact)
// handle artifacts download
@@ -241,7 +241,7 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
}
// get upload file size
fileRealTotalSize, contentLength := getUploadFileSize(ctx)
fileRealTotalSize := getUploadFileSize(ctx)
// get artifact retention days
expiredDays := setting.Actions.ArtifactRetentionDays
@@ -265,24 +265,24 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
return
}
// save chunk to storage, if success, return chunk stotal size
// save chunk to storage, if success, return chunks total size
// if artifact is not gzip when uploading, chunksTotalSize == fileRealTotalSize
// if artifact is gzip when uploading, chunksTotalSize < fileRealTotalSize
chunksTotalSize, err := saveUploadChunk(ar.fs, ctx, artifact, contentLength, runID)
chunksTotalSize, err := saveUploadChunkV3GetTotalSize(ar.fs, ctx, artifact, runID)
if err != nil {
log.Error("Error save upload chunk: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error save upload chunk")
return
}
// update artifact size if zero or not match, over write artifact size
// update artifact size if zero or not match, overwrite artifact size
if artifact.FileSize == 0 ||
artifact.FileCompressedSize == 0 ||
artifact.FileSize != fileRealTotalSize ||
artifact.FileCompressedSize != chunksTotalSize {
artifact.FileSize = fileRealTotalSize
artifact.FileCompressedSize = chunksTotalSize
artifact.ContentEncoding = ctx.Req.Header.Get("Content-Encoding")
artifact.ContentEncodingOrType = ctx.Req.Header.Get("Content-Encoding")
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
log.Error("Error update artifact: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error update artifact")
@@ -297,9 +297,9 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
})
}
// comfirmUploadArtifact confirm upload artifact.
// confirmUploadArtifact confirm upload artifact.
// if all chunks are uploaded, merge them to one file.
func (ar artifactRoutes) comfirmUploadArtifact(ctx *ArtifactContext) {
func (ar artifactRoutes) confirmUploadArtifact(ctx *ArtifactContext) {
_, runID, ok := validateRunID(ctx)
if !ok {
return
@@ -419,8 +419,8 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
}
if itemPath != artifacts[0].ArtifactName {
log.Error("Error dismatch artifact name, itemPath: %v, artifact: %v", itemPath, artifacts[0].ArtifactName)
ctx.HTTPError(http.StatusBadRequest, "Error dismatch artifact name")
log.Error("Error mismatch artifact name, itemPath: %v, artifact: %v", itemPath, artifacts[0].ArtifactName)
ctx.HTTPError(http.StatusBadRequest, "Error mismatch artifact name")
return
}
@@ -428,7 +428,7 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
for _, artifact := range artifacts {
var downloadURL string
if setting.Actions.ArtifactStorage.ServeDirect() {
u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName, ctx.Req.Method, nil)
u, err := ar.fs.ServeDirectURL(artifact.StoragePath, artifact.ArtifactName, ctx.Req.Method, nil)
if err != nil && !errors.Is(err, storage.ErrURLNotSupported) {
log.Error("Error getting serve direct url: %v", err)
}
@@ -492,11 +492,11 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) {
defer fd.Close()
// if artifact is compressed, set content-encoding header to gzip
if artifact.ContentEncoding == "gzip" {
if artifact.ContentEncodingOrType == actions.ContentEncodingV3Gzip {
ctx.Resp.Header().Set("Content-Encoding", "gzip")
}
log.Debug("[artifact] downloadArtifact, name: %s, path: %s, storage: %s, size: %d", artifact.ArtifactName, artifact.ArtifactPath, artifact.StoragePath, artifact.FileSize)
ctx.ServeContent(fd, &context.ServeHeaderOptions{
ctx.ServeContent(fd, context.ServeHeaderOptions{
Filename: artifact.ArtifactName,
LastModified: artifact.CreatedUnix.AsLocalTime(),
})
+194 -102
View File
@@ -12,7 +12,7 @@ import (
"fmt"
"hash"
"io"
"path/filepath"
"path"
"sort"
"strings"
"time"
@@ -20,18 +20,73 @@ import (
"code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
)
func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
artifact *actions.ActionArtifact,
contentSize, runID, start, end, length int64, checkMd5 bool,
) (int64, error) {
type saveUploadChunkOptions struct {
start int64
end *int64
checkMd5 bool
}
func makeTmpPathNameV3(runID int64) string {
return fmt.Sprintf("tmp-upload/run-%d", runID)
}
func makeTmpPathNameV4(runID int64) string {
return fmt.Sprintf("tmp-upload/run-%d-v4", runID)
}
func makeChunkFilenameV3(runID, artifactID, start int64, endPtr *int64) string {
var end int64
if endPtr != nil {
end = *endPtr
}
return fmt.Sprintf("%d-%d-%d-%d.chunk", runID, artifactID, start, end)
}
func parseChunkFileItemV3(st storage.ObjectStorage, fpath string) (*chunkFileItem, error) {
baseName := path.Base(fpath)
if !strings.HasSuffix(baseName, ".chunk") {
return nil, errSkipChunkFile
}
var item chunkFileItem
var unusedRunID int64
if _, err := fmt.Sscanf(baseName, "%d-%d-%d-%d.chunk", &unusedRunID, &item.ArtifactID, &item.Start, &item.End); err != nil {
return nil, err
}
item.Path = fpath
if item.End == 0 {
fi, err := st.Stat(item.Path)
if err != nil {
return nil, err
}
item.Size = fi.Size()
item.End = item.Start + item.Size - 1
} else {
item.Size = item.End - item.Start + 1
}
return &item, nil
}
func saveUploadChunkV3(st storage.ObjectStorage, ctx *ArtifactContext, artifact *actions.ActionArtifact,
runID int64, opts saveUploadChunkOptions,
) (writtenSize int64, retErr error) {
// build chunk store path
storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end)
storagePath := fmt.Sprintf("%s/%s", makeTmpPathNameV3(runID), makeChunkFilenameV3(runID, artifact.ID, opts.start, opts.end))
// "end" is optional, so "contentSize=-1" means read until EOF
contentSize := int64(-1)
if opts.end != nil {
contentSize = *opts.end - opts.start + 1
}
var r io.Reader = ctx.Req.Body
var hasher hash.Hash
if checkMd5 {
if opts.checkMd5 {
// use io.TeeReader to avoid reading all body to md5 sum.
// it writes data to hasher after reading end
// if hash is not matched, delete the read-end result
@@ -41,76 +96,81 @@ func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
// save chunk to storage
writtenSize, err := st.Save(storagePath, r, contentSize)
if err != nil {
return -1, fmt.Errorf("save chunk to storage error: %v", err)
return 0, fmt.Errorf("save chunk to storage error: %v", err)
}
var checkErr error
if checkMd5 {
defer func() {
if retErr != nil {
if err := st.Delete(storagePath); err != nil {
log.Error("Error deleting chunk: %s, %v", storagePath, err)
}
}
}()
if contentSize != -1 && writtenSize != contentSize {
return writtenSize, fmt.Errorf("writtenSize %d does not match contentSize %d", writtenSize, contentSize)
}
if opts.checkMd5 {
// check md5
reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
log.Debug("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
// if md5 not match, delete the chunk
if reqMd5String != chunkMd5String {
checkErr = errors.New("md5 not match")
return writtenSize, errors.New("md5 not match")
}
}
if writtenSize != contentSize {
checkErr = errors.Join(checkErr, fmt.Errorf("writtenSize %d not match contentSize %d", writtenSize, contentSize))
}
if checkErr != nil {
if err := st.Delete(storagePath); err != nil {
log.Error("Error deleting chunk: %s, %v", storagePath, err)
}
return -1, checkErr
}
log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
storagePath, contentSize, artifact.ID, start, end)
// return chunk total size
return length, nil
log.Debug("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, size: %d", storagePath, writtenSize, artifact.ID, opts.start, contentSize)
return writtenSize, nil
}
func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
artifact *actions.ActionArtifact,
contentSize, runID int64,
) (int64, error) {
func saveUploadChunkV3GetTotalSize(st storage.ObjectStorage, ctx *ArtifactContext, artifact *actions.ActionArtifact, runID int64) (totalSize int64, _ error) {
// parse content-range header, format: bytes 0-1023/146515
contentRange := ctx.Req.Header.Get("Content-Range")
start, end, length := int64(0), int64(0), int64(0)
if _, err := fmt.Sscanf(contentRange, "bytes %d-%d/%d", &start, &end, &length); err != nil {
log.Warn("parse content range error: %v, content-range: %s", err, contentRange)
return -1, fmt.Errorf("parse content range error: %v", err)
var start, end int64
if _, err := fmt.Sscanf(contentRange, "bytes %d-%d/%d", &start, &end, &totalSize); err != nil {
return 0, fmt.Errorf("parse content range error: %v", err)
}
return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, length, true)
_, err := saveUploadChunkV3(st, ctx, artifact, runID, saveUploadChunkOptions{start: start, end: &end, checkMd5: true})
if err != nil {
return 0, err
}
return totalSize, nil
}
func appendUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
artifact *actions.ActionArtifact,
start, contentSize, runID int64,
) (int64, error) {
end := start + contentSize - 1
return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, contentSize, false)
// Returns uploaded length
func appendUploadChunkV3(st storage.ObjectStorage, ctx *ArtifactContext, artifact *actions.ActionArtifact, runID, start int64) (int64, error) {
opts := saveUploadChunkOptions{start: start}
if ctx.Req.ContentLength > 0 {
end := start + ctx.Req.ContentLength - 1
opts.end = &end
}
return saveUploadChunkV3(st, ctx, artifact, runID, opts)
}
type chunkFileItem struct {
RunID int64
ArtifactID int64
Start int64
End int64
Path string
// these offset/size related fields might be missing when parsing, they will be filled in the listing functions
Size int64
Start int64
End int64 // inclusive: Size=10, Start=0, End=9
ChunkName string // v4 only
}
func listChunksByRunID(st storage.ObjectStorage, runID int64) (map[int64][]*chunkFileItem, error) {
storageDir := fmt.Sprintf("tmp%d", runID)
func listV3UnorderedChunksMapByRunID(st storage.ObjectStorage, runID int64) (map[int64][]*chunkFileItem, error) {
storageDir := makeTmpPathNameV3(runID)
var chunks []*chunkFileItem
if err := st.IterateObjects(storageDir, func(fpath string, obj storage.Object) error {
baseName := filepath.Base(fpath)
// when read chunks from storage, it only contains storage dir and basename,
// no matter the subdirectory setting in storage config
item := chunkFileItem{Path: storageDir + "/" + baseName}
if _, err := fmt.Sscanf(baseName, "%d-%d-%d-%d.chunk", &item.RunID, &item.ArtifactID, &item.Start, &item.End); err != nil {
return fmt.Errorf("parse content range error: %v", err)
item, err := parseChunkFileItemV3(st, fpath)
if errors.Is(err, errSkipChunkFile) {
return nil
} else if err != nil {
return fmt.Errorf("unable to parse chunk name: %v", fpath)
}
chunks = append(chunks, &item)
chunks = append(chunks, item)
return nil
}); err != nil {
return nil, err
@@ -123,52 +183,78 @@ func listChunksByRunID(st storage.ObjectStorage, runID int64) (map[int64][]*chun
return chunksMap, nil
}
func listChunksByRunIDV4(st storage.ObjectStorage, runID, artifactID int64, blist *BlockList) ([]*chunkFileItem, error) {
storageDir := fmt.Sprintf("tmpv4%d", runID)
var chunks []*chunkFileItem
chunkMap := map[string]*chunkFileItem{}
dummy := &chunkFileItem{}
for _, name := range blist.Latest {
chunkMap[name] = dummy
func listOrderedChunksForArtifact(st storage.ObjectStorage, runID, artifactID int64, blist *BlockList) ([]*chunkFileItem, error) {
emptyListAsError := func(chunks []*chunkFileItem) ([]*chunkFileItem, error) {
if len(chunks) == 0 {
return nil, fmt.Errorf("no chunk found for artifact id: %d", artifactID)
}
return chunks, nil
}
storageDir := makeTmpPathNameV4(runID)
var chunks []*chunkFileItem
var chunkMapV4 map[string]*chunkFileItem
if blist != nil {
// make a dummy map for quick lookup of chunk names, the values are nil now and will be filled after iterating storage objects
chunkMapV4 = map[string]*chunkFileItem{}
for _, name := range blist.Latest {
chunkMapV4[name] = nil
}
}
if err := st.IterateObjects(storageDir, func(fpath string, obj storage.Object) error {
baseName := filepath.Base(fpath)
if !strings.HasPrefix(baseName, "block-") {
item, err := parseChunkFileItemV4(st, artifactID, fpath)
if errors.Is(err, errSkipChunkFile) {
return nil
} else if err != nil {
return fmt.Errorf("unable to parse chunk name: %v", fpath)
}
// when read chunks from storage, it only contains storage dir and basename,
// no matter the subdirectory setting in storage config
item := chunkFileItem{Path: storageDir + "/" + baseName, ArtifactID: artifactID}
var size int64
var b64chunkName string
if _, err := fmt.Sscanf(baseName, "block-%d-%d-%s", &item.RunID, &size, &b64chunkName); err != nil {
return fmt.Errorf("parse content range error: %v", err)
}
rchunkName, err := base64.URLEncoding.DecodeString(b64chunkName)
if err != nil {
return fmt.Errorf("failed to parse chunkName: %v", err)
}
chunkName := string(rchunkName)
item.End = item.Start + size - 1
if _, ok := chunkMap[chunkName]; ok {
chunkMap[chunkName] = &item
// Single chunk upload with block id
if _, ok := chunkMapV4[item.ChunkName]; ok {
chunkMapV4[item.ChunkName] = item
} else if chunkMapV4 == nil {
if chunks != nil {
return errors.New("blockmap is required for chunks > 1")
}
chunks = []*chunkFileItem{item}
}
return nil
}); err != nil {
return nil, err
}
for i, name := range blist.Latest {
chunk, ok := chunkMap[name]
if !ok || chunk.Path == "" {
return nil, fmt.Errorf("missing Chunk (%d/%d): %s", i, len(blist.Latest), name)
if blist == nil && chunks == nil {
chunkUnorderedItemsMapV3, err := listV3UnorderedChunksMapByRunID(st, runID)
if err != nil {
return nil, err
}
chunks = append(chunks, chunk)
if i > 0 {
chunk.Start = chunkMap[blist.Latest[i-1]].End + 1
chunk.End += chunk.Start
chunks = chunkUnorderedItemsMapV3[artifactID]
sort.Slice(chunks, func(i, j int) bool {
return chunks[i].Start < chunks[j].Start
})
return emptyListAsError(chunks)
}
if len(chunks) == 0 && blist != nil {
for i, name := range blist.Latest {
chunk := chunkMapV4[name]
if chunk == nil {
return nil, fmt.Errorf("missing chunk (%d/%d): %s", i, len(blist.Latest), name)
}
chunks = append(chunks, chunk)
}
}
return chunks, nil
for i, chunk := range chunks {
if i == 0 {
chunk.End += chunk.Size - 1
} else {
chunk.Start = chunkMapV4[blist.Latest[i-1]].End + 1
chunk.End = chunk.Start + chunk.Size - 1
}
}
return emptyListAsError(chunks)
}
func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int64, artifactName string) error {
@@ -181,13 +267,13 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int
return err
}
// read all uploading chunks from storage
chunksMap, err := listChunksByRunID(st, runID)
unorderedChunksMap, err := listV3UnorderedChunksMapByRunID(st, runID)
if err != nil {
return err
}
// range db artifacts to merge chunks
for _, art := range artifacts {
chunks, ok := chunksMap[art.ID]
chunks, ok := unorderedChunksMap[art.ID]
if !ok {
log.Debug("artifact %d chunks not found", art.ID)
continue
@@ -199,6 +285,17 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int
return nil
}
func generateArtifactStoragePath(artifact *actions.ActionArtifact) string {
// if chunk is gzip, use gz as extension
// download-artifact action will use content-encoding header to decide if it should decompress the file
extension := "chunk"
if artifact.ContentEncodingOrType == actions.ContentEncodingV3Gzip {
extension = "chunk.gz"
}
return fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension)
}
func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact, checksum string) error {
sort.Slice(chunks, func(i, j int) bool {
return chunks[i].Start < chunks[j].Start
@@ -239,23 +336,18 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
}
mergedReader := io.MultiReader(readers...)
shaPrefix := "sha256:"
var hash hash.Hash
var hashSha256 hash.Hash
if strings.HasPrefix(checksum, shaPrefix) {
hash = sha256.New()
hashSha256 = sha256.New()
} else if checksum != "" {
setting.PanicInDevOrTesting("unsupported checksum format: %s, will skip the checksum verification", checksum)
}
if hash != nil {
mergedReader = io.TeeReader(mergedReader, hash)
}
// if chunk is gzip, use gz as extension
// download-artifact action will use content-encoding header to decide if it should decompress the file
extension := "chunk"
if artifact.ContentEncoding == "gzip" {
extension = "chunk.gz"
if hashSha256 != nil {
mergedReader = io.TeeReader(mergedReader, hashSha256)
}
// save merged file
storagePath := fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension)
storagePath := generateArtifactStoragePath(artifact)
written, err := st.Save(storagePath, mergedReader, artifact.FileCompressedSize)
if err != nil {
return fmt.Errorf("save merged file error: %v", err)
@@ -274,8 +366,8 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
}
}()
if hash != nil {
rawChecksum := hash.Sum(nil)
if hashSha256 != nil {
rawChecksum := hashSha256.Sum(nil)
actualChecksum := hex.EncodeToString(rawChecksum)
if !strings.HasSuffix(checksum, actualChecksum) {
return fmt.Errorf("update artifact error checksum is invalid %v vs %v", checksum, actualChecksum)
+5 -6
View File
@@ -20,8 +20,8 @@ const (
artifactXActionsResultsMD5Header = "x-actions-results-md5"
)
// The rules are from https://github.com/actions/toolkit/blob/main/packages/artifact/src/internal/path-and-artifact-name-validation.ts#L32
var invalidArtifactNameChars = strings.Join([]string{"\\", "/", "\"", ":", "<", ">", "|", "*", "?", "\r", "\n"}, "")
// The rules are from https://github.com/actions/toolkit/blob/main/packages/artifact/src/internal/upload/path-and-artifact-name-validation.ts
const invalidArtifactNameChars = "\\/\":<>|*?\r\n"
func validateArtifactName(ctx *ArtifactContext, artifactName string) bool {
if strings.ContainsAny(artifactName, invalidArtifactNameChars) {
@@ -84,11 +84,10 @@ func parseArtifactItemPath(ctx *ArtifactContext) (string, string, bool) {
// getUploadFileSize returns the size of the file to be uploaded.
// The raw size is the size of the file as reported by the header X-TFS-FileLength.
func getUploadFileSize(ctx *ArtifactContext) (int64, int64) {
contentLength := ctx.Req.ContentLength
func getUploadFileSize(ctx *ArtifactContext) int64 {
xTfsLength, _ := strconv.ParseInt(ctx.Req.Header.Get(artifactXTfsFileLengthHeader), 10, 64)
if xTfsLength > 0 {
return xTfsLength, contentLength
return xTfsLength
}
return contentLength, contentLength
return ctx.Req.ContentLength
}
+254 -111
View File
@@ -89,17 +89,22 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/xml"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/models/actions"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -109,14 +114,12 @@ import (
"code.gitea.io/gitea/services/context"
"google.golang.org/protobuf/encoding/protojson"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/timestamppb"
"xorm.io/builder"
)
const (
ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService"
ArtifactV4ContentEncoding = "application/zip"
)
const ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService"
type artifactV4Routes struct {
prefix string
@@ -157,33 +160,81 @@ func ArtifactsV4Routes(prefix string) *web.Router {
return m
}
func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID, artifactID int64) []byte {
func (r *artifactV4Routes) buildSignature(endpoint, expires, artifactName string, taskID, artifactID int64) []byte {
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
mac.Write([]byte(endp))
mac.Write([]byte(endpoint))
mac.Write([]byte(expires))
mac.Write([]byte(artifactName))
fmt.Fprint(mac, taskID)
fmt.Fprint(mac, artifactID)
_, _ = fmt.Fprint(mac, taskID)
_, _ = fmt.Fprint(mac, artifactID)
return mac.Sum(nil)
}
func (r artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endp, artifactName string, taskID, artifactID int64) string {
func (r *artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endpoint, artifactName string, taskID, artifactID int64) string {
expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST")
uploadURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") + strings.TrimSuffix(r.prefix, "/") +
"/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID, artifactID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + strconv.FormatInt(taskID, 10) + "&artifactID=" + strconv.FormatInt(artifactID, 10)
"/" + endpoint +
"?sig=" + base64.RawURLEncoding.EncodeToString(r.buildSignature(endpoint, expires, artifactName, taskID, artifactID)) +
"&expires=" + url.QueryEscape(expires) +
"&artifactName=" + url.QueryEscape(artifactName) +
"&taskID=" + strconv.FormatInt(taskID, 10) +
"&artifactID=" + strconv.FormatInt(artifactID, 10)
return uploadURL
}
func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) {
func makeBlockFilenameV4(runID, artifactID, size int64, blockID string) string {
sizeInName := max(size, 0) // do not use "-1" in filename
return fmt.Sprintf("block-%d-%d-%d-%s", runID, artifactID, sizeInName, base64.URLEncoding.EncodeToString([]byte(blockID)))
}
var errSkipChunkFile = errors.New("skip this chunk file")
func parseChunkFileItemV4(st storage.ObjectStorage, artifactID int64, fpath string) (*chunkFileItem, error) {
baseName := path.Base(fpath)
if !strings.HasPrefix(baseName, "block-") {
return nil, errSkipChunkFile
}
var item chunkFileItem
var unusedRunID int64
var b64chunkName string
_, err := fmt.Sscanf(baseName, "block-%d-%d-%d-%s", &unusedRunID, &item.ArtifactID, &item.Size, &b64chunkName)
if err != nil {
return nil, err
}
if item.ArtifactID != artifactID {
return nil, errSkipChunkFile
}
chunkName, err := base64.URLEncoding.DecodeString(b64chunkName)
if err != nil {
return nil, err
}
item.ChunkName = string(chunkName)
item.Path = fpath
if item.Size <= 0 {
fi, err := st.Stat(item.Path)
if err != nil {
return nil, err
}
item.Size = fi.Size()
}
return &item, nil
}
func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions_model.ActionTask, string, bool) {
rawTaskID := ctx.Req.URL.Query().Get("taskID")
rawArtifactID := ctx.Req.URL.Query().Get("artifactID")
sig := ctx.Req.URL.Query().Get("sig")
expires := ctx.Req.URL.Query().Get("expires")
artifactName := ctx.Req.URL.Query().Get("artifactName")
dsig, _ := base64.URLEncoding.DecodeString(sig)
taskID, _ := strconv.ParseInt(rawTaskID, 10, 64)
artifactID, _ := strconv.ParseInt(rawArtifactID, 10, 64)
dsig, errSig := base64.RawURLEncoding.DecodeString(sig)
taskID, errTask := strconv.ParseInt(rawTaskID, 10, 64)
artifactID, errArtifactID := strconv.ParseInt(rawArtifactID, 10, 64)
err := errors.Join(errSig, errTask, errArtifactID)
if err != nil {
log.Error("Error decoding signature values: %v", err)
ctx.HTTPError(http.StatusBadRequest, "Error decoding signature values")
return nil, "", false
}
expecedsig := r.buildSignature(endp, expires, artifactName, taskID, artifactID)
if !hmac.Equal(dsig, expecedsig) {
log.Error("Error unauthorized")
@@ -196,13 +247,13 @@ func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*a
ctx.HTTPError(http.StatusUnauthorized, "Error link expired")
return nil, "", false
}
task, err := actions.GetTaskByID(ctx, taskID)
task, err := actions_model.GetTaskByID(ctx, taskID)
if err != nil {
log.Error("Error runner api getting task by ID: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task by ID")
return nil, "", false
}
if task.Status != actions.StatusRunning {
if task.Status != actions_model.StatusRunning {
log.Error("Error runner api getting task: task is not running")
ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task: task is not running")
return nil, "", false
@@ -215,9 +266,9 @@ func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*a
return task, artifactName, true
}
func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) {
var art actions.ActionArtifact
has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ? AND content_encoding = ?", runID, name, name+".zip", ArtifactV4ContentEncoding).Get(&art)
func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions_model.ActionArtifact, error) {
var art actions_model.ActionArtifact
has, err := db.GetEngine(ctx).Where(builder.Eq{"run_id": runID, "artifact_name": name}, builder.Like{"content_encoding", "%/%"}).Get(&art)
if err != nil {
return nil, err
} else if !has {
@@ -226,7 +277,7 @@ func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64,
return &art, nil
}
func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool {
func (r *artifactV4Routes) parseProtobufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool {
body, err := io.ReadAll(ctx.Req.Body)
if err != nil {
log.Error("Error decode request body: %v", err)
@@ -242,7 +293,7 @@ func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protorefle
return true
}
func (r *artifactV4Routes) sendProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) {
func (r *artifactV4Routes) sendProtobufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) {
resp, err := protojson.Marshal(req)
if err != nil {
log.Error("Error encode response body: %v", err)
@@ -257,7 +308,7 @@ func (r *artifactV4Routes) sendProtbufBody(ctx *ArtifactContext, req protoreflec
func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) {
var req CreateArtifactRequest
if ok := r.parseProtbufBody(ctx, &req); !ok {
if ok := r.parseProtobufBody(ctx, &req); !ok {
return
}
_, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
@@ -267,31 +318,64 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) {
artifactName := req.Name
rententionDays := setting.Actions.ArtifactRetentionDays
retentionDays := setting.Actions.ArtifactRetentionDays
if req.ExpiresAt != nil {
rententionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24)
retentionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24)
}
encoding := req.GetMimeType().GetValue()
// Validate media type
if encoding != "" {
encoding, _, _ = mime.ParseMediaType(encoding)
}
fileName := artifactName
if !strings.Contains(encoding, "/") || strings.EqualFold(encoding, actions_model.ContentTypeZip) && !strings.HasSuffix(fileName, ".zip") {
encoding = actions_model.ContentTypeZip
fileName = artifactName + ".zip"
}
// create or get artifact with name and path
artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", rententionDays)
artifact, err := actions_model.CreateArtifact(ctx, ctx.ActionTask, artifactName, fileName, retentionDays)
if err != nil {
log.Error("Error create or get artifact: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error create or get artifact")
return
}
artifact.ContentEncoding = ArtifactV4ContentEncoding
artifact.ContentEncodingOrType = encoding
artifact.FileSize = 0
artifact.FileCompressedSize = 0
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
var respData CreateArtifactResponse
if setting.Actions.ArtifactStorage.ServeDirect() && setting.Actions.ArtifactStorage.Type == setting.AzureBlobStorageType {
storagePath := generateArtifactStoragePath(artifact)
if artifact.StoragePath != "" {
_ = storage.ActionsArtifacts.Delete(artifact.StoragePath)
}
artifact.StoragePath = storagePath
artifact.Status = actions_model.ArtifactStatusUploadPending
u, err := storage.ActionsArtifacts.ServeDirectURL(artifact.StoragePath, artifact.ArtifactPath, http.MethodPut, nil)
if err != nil {
log.Error("Error ServeDirectURL: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error ServeDirectURL")
return
}
respData = CreateArtifactResponse{
Ok: true,
SignedUploadUrl: u.String(),
}
} else {
respData = CreateArtifactResponse{
Ok: true,
SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID),
}
}
if err := actions_model.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
log.Error("Error UpdateArtifactByID: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID")
return
}
respData := CreateArtifactResponse{
Ok: true,
SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID),
}
r.sendProtbufBody(ctx, &respData)
r.sendProtobufBody(ctx, &respData)
}
func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
@@ -303,34 +387,34 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
comp := ctx.Req.URL.Query().Get("comp")
switch comp {
case "block", "appendBlock":
blockid := ctx.Req.URL.Query().Get("blockid")
if blockid == "" {
// get artifact by name
artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
// get artifact by name
artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
return
}
blockID := ctx.Req.URL.Query().Get("blockid")
if blockID == "" {
uploadedLength, err := appendUploadChunkV3(r.fs, ctx, artifact, artifact.RunID, artifact.FileSize)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
log.Error("Error appending chunk %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error appending Chunk")
return
}
_, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID)
if err != nil {
log.Error("Error runner api getting task: task is not running")
ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task: task is not running")
return
}
artifact.FileCompressedSize += ctx.Req.ContentLength
artifact.FileSize += ctx.Req.ContentLength
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
artifact.FileCompressedSize += uploadedLength
artifact.FileSize += uploadedLength
if err := actions_model.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
log.Error("Error UpdateArtifactByID: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID")
return
}
} else {
_, err := r.fs.Save(fmt.Sprintf("tmpv4%d/block-%d-%d-%s", task.Job.RunID, task.Job.RunID, ctx.Req.ContentLength, base64.URLEncoding.EncodeToString([]byte(blockid))), ctx.Req.Body, -1)
blockFilename := makeBlockFilenameV4(task.Job.RunID, artifact.ID, ctx.Req.ContentLength, blockID)
_, err := r.fs.Save(fmt.Sprintf("%s/%s", makeTmpPathNameV4(task.Job.RunID), blockFilename), ctx.Req.Body, ctx.Req.ContentLength)
if err != nil {
log.Error("Error runner api getting task: task is not running")
ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task: task is not running")
log.Error("Error uploading block blob %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error uploading block blob")
return
}
}
@@ -338,10 +422,10 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
case "blocklist":
rawArtifactID := ctx.Req.URL.Query().Get("artifactID")
artifactID, _ := strconv.ParseInt(rawArtifactID, 10, 64)
_, err := r.fs.Save(fmt.Sprintf("tmpv4%d/%d-%d-blocklist", task.Job.RunID, task.Job.RunID, artifactID), ctx.Req.Body, -1)
_, err := r.fs.Save(fmt.Sprintf("%s/%d-%d-blocklist", makeTmpPathNameV4(task.Job.RunID), task.Job.RunID, artifactID), ctx.Req.Body, -1)
if err != nil {
log.Error("Error runner api getting task: task is not running")
ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task: task is not running")
log.Error("Error uploading blocklist %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error uploading blocklist")
return
}
ctx.JSON(http.StatusCreated, "created")
@@ -357,7 +441,7 @@ type Latest struct {
}
func (r *artifactV4Routes) readBlockList(runID, artifactID int64) (*BlockList, error) {
blockListName := fmt.Sprintf("tmpv4%d/%d-%d-blocklist", runID, runID, artifactID)
blockListName := fmt.Sprintf("%s/%d-%d-blocklist", makeTmpPathNameV4(runID), runID, artifactID)
s, err := r.fs.Open(blockListName)
if err != nil {
return nil, err
@@ -367,17 +451,22 @@ func (r *artifactV4Routes) readBlockList(runID, artifactID int64) (*BlockList, e
blockList := &BlockList{}
err = xdec.Decode(blockList)
_ = s.Close()
delerr := r.fs.Delete(blockListName)
if delerr != nil {
log.Warn("Failed to delete blockList %s: %v", blockListName, delerr)
}
return blockList, err
if err != nil {
return nil, err
}
return blockList, nil
}
func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) {
var req FinalizeArtifactRequest
if ok := r.parseProtbufBody(ctx, &req); !ok {
if ok := r.parseProtobufBody(ctx, &req); !ok {
return
}
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
@@ -393,40 +482,14 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) {
return
}
var chunks []*chunkFileItem
blockList, err := r.readBlockList(runID, artifact.ID)
if err != nil {
log.Warn("Failed to read BlockList, fallback to old behavior: %v", err)
chunkMap, err := listChunksByRunID(r.fs, runID)
if err != nil {
log.Error("Error merge chunks: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks")
return
}
chunks, ok = chunkMap[artifact.ID]
if !ok {
log.Error("Error merge chunks")
ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks")
return
}
if setting.Actions.ArtifactStorage.ServeDirect() && setting.Actions.ArtifactStorage.Type == setting.AzureBlobStorageType {
r.finalizeAzureServeDirect(ctx, &req, artifact)
} else {
chunks, err = listChunksByRunIDV4(r.fs, runID, artifact.ID, blockList)
if err != nil {
log.Error("Error merge chunks: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks")
return
}
artifact.FileSize = chunks[len(chunks)-1].End + 1
artifact.FileCompressedSize = chunks[len(chunks)-1].End + 1
r.finalizeDefaultArtifact(ctx, &req, artifact, runID)
}
checksum := ""
if req.Hash != nil {
checksum = req.Hash.Value
}
if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil {
log.Error("Error merge chunks: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks")
// Return on finalize error
if ctx.Written() {
return
}
@@ -434,13 +497,89 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) {
Ok: true,
ArtifactId: artifact.ID,
}
r.sendProtbufBody(ctx, &respData)
r.sendProtobufBody(ctx, &respData)
}
func (r *artifactV4Routes) finalizeDefaultArtifact(ctx *ArtifactContext, req *FinalizeArtifactRequest, artifact *actions_model.ActionArtifact, runID int64) {
blockList, blockListErr := r.readBlockList(runID, artifact.ID)
chunks, err := listOrderedChunksForArtifact(r.fs, runID, artifact.ID, blockList)
if err != nil {
log.Error("Error list chunks: %v", errors.Join(blockListErr, err))
ctx.HTTPError(http.StatusInternalServerError, "Error list chunks")
return
}
artifact.FileSize = chunks[len(chunks)-1].End + 1
artifact.FileCompressedSize = chunks[len(chunks)-1].End + 1
if req.Size != artifact.FileSize {
log.Error("Error merge chunks size mismatch")
ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks size mismatch")
return
}
if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, req.GetHash().GetValue()); err != nil {
log.Error("Error merge chunks: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks")
return
}
}
func (r *artifactV4Routes) finalizeAzureServeDirect(ctx *ArtifactContext, req *FinalizeArtifactRequest, artifact *actions_model.ActionArtifact) {
checksumValue, hasSha256Checksum := strings.CutPrefix(req.GetHash().GetValue(), "sha256:")
var actualLength int64
if hasSha256Checksum {
hashSha256 := sha256.New()
obj, err := storage.ActionsArtifacts.Open(artifact.StoragePath)
if err != nil {
log.Error("Error read block: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error read block")
return
}
defer obj.Close()
actualLength, err = io.Copy(hashSha256, obj)
if err != nil {
log.Error("Error read block: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error read block")
return
}
rawChecksum := hashSha256.Sum(nil)
actualChecksum := hex.EncodeToString(rawChecksum)
if checksumValue != actualChecksum {
log.Error("Error merge chunks: checksum mismatch")
ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks: checksum mismatch")
return
}
} else {
fi, err := storage.ActionsArtifacts.Stat(artifact.StoragePath)
if err != nil {
log.Error("Error stat block: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error stat block")
return
}
actualLength = fi.Size()
}
if req.Size != actualLength {
log.Error("Error merge chunks: length mismatch")
ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks: length mismatch")
return
}
// Update artifact metadata and status now that the upload is confirmed.
artifact.FileSize = actualLength
artifact.FileCompressedSize = actualLength
artifact.Status = actions_model.ArtifactStatusUploadConfirmed
if err := actions_model.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
log.Error("Error UpdateArtifactByID: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID")
return
}
}
func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
var req ListArtifactsRequest
if ok := r.parseProtbufBody(ctx, &req); !ok {
if ok := r.parseProtobufBody(ctx, &req); !ok {
return
}
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
@@ -448,9 +587,10 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
return
}
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{
RunID: runID,
Status: int(actions.ArtifactStatusUploadConfirmed),
artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
RunID: runID,
Status: int(actions_model.ArtifactStatusUploadConfirmed),
FinalizedArtifactsV4: true,
})
if err != nil {
log.Error("Error getting artifacts: %v", err)
@@ -462,7 +602,7 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
table := map[string]*ListArtifactsResponse_MonolithArtifact{}
for _, artifact := range artifacts {
if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != ArtifactV4ContentEncoding {
if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value {
table[artifact.ArtifactName] = nil
continue
}
@@ -485,13 +625,13 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
respData := ListArtifactsResponse{
Artifacts: list,
}
r.sendProtbufBody(ctx, &respData)
r.sendProtobufBody(ctx, &respData)
}
func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
var req GetSignedArtifactURLRequest
if ok := r.parseProtbufBody(ctx, &req); !ok {
if ok := r.parseProtobufBody(ctx, &req); !ok {
return
}
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
@@ -508,7 +648,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
return
}
if artifact.Status != actions.ArtifactStatusUploadConfirmed {
if artifact.Status != actions_model.ArtifactStatusUploadConfirmed {
log.Error("Error artifact not found: %s", artifact.Status.ToString())
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
return
@@ -517,15 +657,16 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
respData := GetSignedArtifactURLResponse{}
if setting.Actions.ArtifactStorage.ServeDirect() {
u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath, ctx.Req.Method, nil)
if u != nil && err == nil {
respData.SignedUrl = u.String()
// DO NOT USE the http POST method coming from the getSignedArtifactURL endpoint
u, err := actions.GetArtifactV4ServeDirectURL(artifact, http.MethodGet)
if err == nil {
respData.SignedUrl = u
}
}
if respData.SignedUrl == "" {
respData.SignedUrl = r.buildArtifactURL(ctx, "DownloadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID)
}
r.sendProtbufBody(ctx, &respData)
r.sendProtobufBody(ctx, &respData)
}
func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) {
@@ -541,21 +682,23 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) {
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
return
}
if artifact.Status != actions.ArtifactStatusUploadConfirmed {
if artifact.Status != actions_model.ArtifactStatusUploadConfirmed {
log.Error("Error artifact not found: %s", artifact.Status.ToString())
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
return
}
file, _ := r.fs.Open(artifact.StoragePath)
_, _ = io.Copy(ctx.Resp, file)
err = actions.DownloadArtifactV4ReadStorage(ctx.Base, artifact)
if err != nil {
log.Error("Error serve artifact: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "failed to download artifact")
}
}
func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) {
var req DeleteArtifactRequest
if ok := r.parseProtbufBody(ctx, &req); !ok {
if ok := r.parseProtobufBody(ctx, &req); !ok {
return
}
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
@@ -571,7 +714,7 @@ func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) {
return
}
err = actions.SetArtifactNeedDelete(ctx, runID, req.Name)
err = actions_model.SetArtifactNeedDelete(ctx, runID, req.Name)
if err != nil {
log.Error("Error deleting artifacts: %v", err)
ctx.HTTPError(http.StatusInternalServerError, err.Error())
@@ -582,5 +725,5 @@ func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) {
Ok: true,
ArtifactId: artifact.ID,
}
r.sendProtbufBody(ctx, &respData)
r.sendProtobufBody(ctx, &respData)
}
+8 -2
View File
@@ -156,10 +156,16 @@ func (s *Service) FetchTask(
}
if tasksVersion != latestVersion {
// Re-load runner from DB so task assignment uses current IsDisabled state
// (avoids race where disable commits while this request still has stale runner).
freshRunner, err := actions_model.GetRunnerByUUID(ctx, runner.UUID)
if err != nil {
return nil, status.Errorf(codes.Internal, "get runner: %v", err)
}
// if the task version in request is not equal to the version in db,
// it means there may still be some tasks that haven't been assigned.
// try to pick a task for the runner that send the request.
if t, ok, err := actions_service.PickTask(ctx, runner); err != nil {
if t, ok, err := actions_service.PickTask(ctx, freshRunner); err != nil {
log.Error("pick task failed: %v", err)
return nil, status.Errorf(codes.Internal, "pick task: %v", err)
} else if ok {
@@ -270,7 +276,7 @@ func (s *Service) UpdateLog(
rows := req.Msg.Rows[ack-req.Msg.Index:]
ns, err := actions.WriteLogs(ctx, task.LogFilename, task.LogSize, rows)
if err != nil {
return nil, status.Errorf(codes.Internal, "write logs: %v", err)
return nil, status.Errorf(codes.Internal, "unable to append logs to dbfs file: %v", err)
}
task.LogLength += int64(len(rows))
for _, n := range ns {
+1 -1
View File
@@ -54,7 +54,7 @@ func GetRepositoryKey(ctx *context.Context) {
return
}
ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{
ContentType: "application/x-pem-file",
Filename: fmt.Sprintf("%s@%s.rsa.pub", ctx.Package.Owner.LowerName, hex.EncodeToString(fingerprint)),
})
+39 -10
View File
@@ -32,6 +32,7 @@ import (
"code.gitea.io/gitea/routers/api/packages/rpm"
"code.gitea.io/gitea/routers/api/packages/rubygems"
"code.gitea.io/gitea/routers/api/packages/swift"
"code.gitea.io/gitea/routers/api/packages/terraform"
"code.gitea.io/gitea/routers/api/packages/vagrant"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
@@ -88,21 +89,26 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
}
}
func verifyAuth(r *web.Router, authMethods []auth.Method) {
type verifyAuthOptions struct {
afterAuthCallback func(ctx *context.Context, err error)
}
func verifyAuth(r *web.Router, authMethods []auth.Method, opts verifyAuthOptions) {
if setting.Service.EnableReverseProxyAuth {
authMethods = append(authMethods, &auth.ReverseProxy{})
}
authGroup := auth.NewGroup(authMethods...)
r.Use(func(ctx *context.Context) {
r.AfterRouting(func(ctx *context.Context) {
var err error
ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
if err != nil {
ctx.IsSigned = ctx.Doer != nil
if opts.afterAuthCallback != nil {
opts.afterAuthCallback(ctx, err)
} else if err != nil {
log.Error("Failed to verify user: %v", err)
ctx.HTTPError(http.StatusUnauthorized, "Failed to authenticate user")
return
}
ctx.IsSigned = ctx.Doer != nil
})
}
@@ -111,7 +117,7 @@ func verifyAuth(r *web.Router, authMethods []auth.Method) {
func CommonRoutes() *web.Router {
r := web.NewRouter()
r.Use(context.PackageContexter())
r.AfterRouting(context.PackageContexter())
verifyAuth(r, []auth.Method{
&auth.OAuth2{},
@@ -119,7 +125,7 @@ func CommonRoutes() *web.Router {
&nuget.Auth{},
&Auth{},
&chef.Auth{},
})
}, verifyAuthOptions{})
r.Group("/{username}", func() {
r.Group("/alpine", func() {
@@ -467,9 +473,10 @@ func CommonRoutes() *web.Router {
g.MatchPath("HEAD", "/<group:*>/repodata/<filename>", rpm.CheckRepositoryFileExistence)
g.MatchPath("GET", "/<group:*>/repodata/<filename>", rpm.GetRepositoryFile)
g.MatchPath("PUT", "/<group:*>/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile)
g.MatchPath("POST", "/<group:*>/package/<name>/<version>/errata", reqPackageAccess(perm.AccessModeWrite), rpm.UploadErrata)
// this URL pattern is only used internally in the RPM index, it is generated by us, the filename part is not really used (can be anything)
g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", rpm.DownloadPackageFile)
g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>/<filename>", rpm.DownloadPackageFile)
g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", rpm.DownloadPackageFile)
g.MatchPath("DELETE", "/<group:*>/package/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile)
}, reqPackageAccess(perm.AccessModeRead))
@@ -509,6 +516,21 @@ func CommonRoutes() *web.Router {
r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers)
}, reqPackageAccess(perm.AccessModeRead))
})
// See https://docs.gitlab.com/ci/jobs/fine_grained_permissions/#terraform-state-endpoints
// For endpoint and permission reference
r.Group("/terraform/state/{name}", func() {
r.Get("", terraform.GetTerraformState)
r.Get("/versions/{serial}", terraform.GetTerraformStateBySerial)
r.Group("", func() {
r.Post("", terraform.UploadState)
r.Delete("", terraform.DeleteState)
r.Delete("/versions/{serial}", terraform.DeleteStateBySerial)
}, reqPackageAccess(perm.AccessModeWrite))
r.Group("/lock", func() {
r.Post("", terraform.LockState)
r.Delete("", terraform.UnlockState)
}, reqPackageAccess(perm.AccessModeWrite))
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/vagrant", func() {
r.Group("/authenticate", func() {
r.Get("", vagrant.CheckAuthenticate)
@@ -533,12 +555,19 @@ func CommonRoutes() *web.Router {
func ContainerRoutes() *web.Router {
r := web.NewRouter()
r.Use(context.PackageContexter())
r.AfterRouting(context.PackageContexter())
verifyAuth(r, []auth.Method{
&auth.Basic{},
// container auth requires an token, so container.Authenticate issues a Ghost user token for anonymous access
// container auth requires token, so container.Authenticate issues a Ghost user token for anonymous access
&Auth{AllowGhostUser: true},
}, verifyAuthOptions{
afterAuthCallback: func(ctx *context.Context, err error) {
if err != nil {
log.Error("Failed to verify container user: %v", err)
container.APIUnauthorizedError(ctx)
}
},
})
// TODO: Content Discovery / References (not implemented yet)
+4 -4
View File
@@ -35,7 +35,7 @@ func GetRepositoryKey(ctx *context.Context) {
return
}
ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{
ContentType: "application/pgp-keys",
})
}
@@ -91,7 +91,7 @@ func UploadPackageFile(ctx *context.Context) {
return
}
release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID)
release, err := arch_service.AcquireRegistryLock(ctx, ctx.Package.Owner.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
@@ -232,7 +232,7 @@ func GetPackageOrRepositoryFile(ctx *context.Context) {
return
}
ctx.ServeContent(bytes.NewReader(data), &context.ServeHeaderOptions{
ctx.ServeContent(bytes.NewReader(data), context.ServeHeaderOptions{
Filename: filenameOrig,
})
return
@@ -257,7 +257,7 @@ func DeletePackageVersion(ctx *context.Context) {
name := ctx.PathParam("name")
version := ctx.PathParam("version")
release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID)
release, err := arch_service.AcquireRegistryLock(ctx, ctx.Package.Owner.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
+10 -7
View File
@@ -26,6 +26,7 @@ import (
packages_module "code.gitea.io/gitea/modules/packages"
container_module "code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
auth_service "code.gitea.io/gitea/services/auth"
@@ -120,17 +121,19 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
})
}
func apiUnauthorizedError(ctx *context.Context) {
func APIUnauthorizedError(ctx *context.Context) {
// container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed
realmURL := httplib.GuessCurrentHostURL(ctx) + "/v2/token"
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`)
// support apple container like: container registry login <gitea-host> -u
ctx.Resp.Header().Add("WWW-Authenticate", `Basic realm="Gitea Container Registry"`)
apiErrorDefined(ctx, errUnauthorized)
}
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
func ReqContainerAccess(ctx *context.Context) {
if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) {
apiUnauthorizedError(ctx)
APIUnauthorizedError(ctx)
}
}
@@ -156,7 +159,7 @@ func Authenticate(ctx *context.Context) {
packageScope := auth_service.GetAccessScope(ctx.Data)
if u == nil {
if setting.Service.RequireSignInViewStrict {
apiUnauthorizedError(ctx)
APIUnauthorizedError(ctx)
return
}
@@ -170,7 +173,7 @@ func Authenticate(ctx *context.Context) {
if err != nil {
log.Error("Error checking access scope: %v", err)
}
apiUnauthorizedError(ctx)
APIUnauthorizedError(ctx)
return
}
}
@@ -704,9 +707,9 @@ func DeleteManifest(ctx *context.Context) {
}
func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
serveDirectReqParams := make(url.Values)
serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType))
s, u, _, err := packages_service.OpenBlobForDownload(ctx, pfd.File, pfd.Blob, ctx.Req.Method, serveDirectReqParams)
s, u, _, err := packages_service.OpenBlobForDownload(ctx, pfd.File, pfd.Blob, ctx.Req.Method, &storage.ServeDirectOptions{
ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
+2 -2
View File
@@ -35,7 +35,7 @@ func GetRepositoryKey(ctx *context.Context) {
return
}
ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{
ContentType: "application/pgp-keys",
Filename: "repository.key",
})
@@ -233,7 +233,7 @@ func DownloadPackageFile(ctx *context.Context) {
return
}
helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{
helper.ServePackageFile(ctx, s, u, pf, context.ServeHeaderOptions{
ContentType: "application/vnd.debian.binary-package",
Filename: pf.Name,
LastModified: pf.CreatedUnix.AsLocalTime(),
+3 -3
View File
@@ -39,7 +39,7 @@ func ProcessErrorForUser(ctx *context.Context, status int, errObj any) string {
// ServePackageFile the content of the package file
// If the url is set it will redirect the request, otherwise the content is copied to the response.
func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...*context.ServeHeaderOptions) {
func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...context.ServeHeaderOptions) {
if u != nil {
ctx.Redirect(u.String())
return
@@ -47,11 +47,11 @@ func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf
defer s.Close()
var opts *context.ServeHeaderOptions
var opts context.ServeHeaderOptions
if len(forceOpts) > 0 {
opts = forceOpts[0]
} else {
opts = &context.ServeHeaderOptions{
opts = context.ServeHeaderOptions{
Filename: pf.Name,
LastModified: pf.CreatedUnix.AsLocalTime(),
}
+1 -1
View File
@@ -200,7 +200,7 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool
return
}
opts := &context.ServeHeaderOptions{
opts := context.ServeHeaderOptions{
ContentLength: &pb.Size,
LastModified: pf.CreatedUnix.AsLocalTime(),
}
+1 -1
View File
@@ -169,7 +169,7 @@ func UploadPackage(ctx *context.Context) {
canWrite := repo.OwnerID == ctx.Doer.ID
if !canWrite {
perms, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
perms, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
+146 -2
View File
@@ -10,6 +10,7 @@ import (
"io"
"net/http"
"strings"
"time"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
@@ -57,7 +58,7 @@ func GetRepositoryKey(ctx *context.Context) {
return
}
ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{
ContentType: "application/pgp-keys",
Filename: "repository.key",
})
@@ -80,7 +81,7 @@ func CheckRepositoryFileExistence(ctx *context.Context) {
return
}
ctx.SetServeHeaders(&context.ServeHeaderOptions{
ctx.SetServeHeaders(context.ServeHeaderOptions{
Filename: pf.Name,
LastModified: pf.CreatedUnix.AsLocalTime(),
})
@@ -316,3 +317,146 @@ func DeletePackageFile(webctx *context.Context) {
webctx.Status(http.StatusNoContent)
}
// UploadErrata handles uploading errata information for a package version
func UploadErrata(ctx *context.Context) {
name := ctx.PathParam("name")
version := ctx.PathParam("version")
group := ctx.PathParam("group")
var updates []*rpm_module.Update
if err := json.NewDecoder(ctx.Req.Body).Decode(&updates); err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
pv, err := packages_model.GetVersionByNameAndVersion(ctx,
ctx.Package.Owner.ID,
packages_model.TypeRpm,
name,
version,
)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
var vm *rpm_module.VersionMetadata
if pv.MetadataJSON != "" {
if err := json.Unmarshal([]byte(pv.MetadataJSON), &vm); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
} else {
vm = &rpm_module.VersionMetadata{}
}
now := time.Now().Format("2006-01-02 15:04:05")
for _, u := range updates {
if u == nil {
continue
}
// Sanitize to remove nil elements from JSON payload
var cleanPkgList []*rpm_module.Collection
for _, coll := range u.PkgList {
if coll == nil {
continue
}
var cleanPackages []*rpm_module.UpdatePackage
for _, pkg := range coll.Packages {
if pkg == nil {
continue
}
cleanPackages = append(cleanPackages, pkg)
}
coll.Packages = cleanPackages
cleanPkgList = append(cleanPkgList, coll)
}
u.PkgList = cleanPkgList
found := false
for i, existing := range vm.Updates {
if existing.ID == u.ID {
// Merge PkgList with deduplication
for _, newColl := range u.PkgList {
if newColl == nil {
continue
}
collFound := false
for j, existingColl := range existing.PkgList {
if existingColl.Short == newColl.Short {
// Merge packages
for _, newPkg := range newColl.Packages {
if newPkg == nil {
continue
}
pkgFound := false
for _, existingPkg := range existingColl.Packages {
if existingPkg.Name == newPkg.Name &&
existingPkg.Version == newPkg.Version &&
existingPkg.Release == newPkg.Release &&
existingPkg.Arch == newPkg.Arch {
pkgFound = true
break
}
}
if !pkgFound {
vm.Updates[i].PkgList[j].Packages = append(vm.Updates[i].PkgList[j].Packages, newPkg)
}
}
collFound = true
break
}
}
if !collFound {
vm.Updates[i].PkgList = append(vm.Updates[i].PkgList, newColl)
}
}
vm.Updates[i].From = u.From
vm.Updates[i].Status = u.Status
vm.Updates[i].Type = u.Type
vm.Updates[i].Version = u.Version
vm.Updates[i].Title = u.Title
vm.Updates[i].Severity = u.Severity
vm.Updates[i].Description = u.Description
vm.Updates[i].References = u.References
vm.Updates[i].Updated = &rpm_module.DateAttr{Date: now}
found = true
break
}
}
if !found {
if u.Issued == nil {
u.Issued = &rpm_module.DateAttr{Date: now}
}
if u.Updated == nil {
u.Updated = &rpm_module.DateAttr{Date: now}
}
vm.Updates = append(vm.Updates, u)
}
}
vmBytes, err := json.Marshal(vm)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
pv.MetadataJSON = string(vmBytes)
if err := packages_model.UpdateVersion(ctx, pv); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if err := rpm_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, group); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Status(http.StatusOK)
}
+2 -2
View File
@@ -79,7 +79,7 @@ func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_mo
})
}
ctx.SetServeHeaders(&context.ServeHeaderOptions{
ctx.SetServeHeaders(context.ServeHeaderOptions{
Filename: filename + ".gz",
})
@@ -119,7 +119,7 @@ func ServePackageSpecification(ctx *context.Context) {
return
}
ctx.SetServeHeaders(&context.ServeHeaderOptions{
ctx.SetServeHeaders(context.ServeHeaderOptions{
Filename: filename,
})
+2 -2
View File
@@ -281,7 +281,7 @@ func DownloadManifest(ctx *context.Context) {
filename = fmt.Sprintf("Package@swift-%s.swift", swiftVersion)
}
ctx.ServeContent(strings.NewReader(m.Content), &context.ServeHeaderOptions{
ctx.ServeContent(strings.NewReader(m.Content), context.ServeHeaderOptions{
ContentType: "text/x-swift",
Filename: filename,
LastModified: pv.CreatedUnix.AsLocalTime(),
@@ -437,7 +437,7 @@ func DownloadPackageFile(ctx *context.Context) {
Digest: pd.Files[0].Blob.HashSHA256,
})
helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{
helper.ServePackageFile(ctx, s, u, pf, context.ServeHeaderOptions{
Filename: pf.Name,
ContentType: "application/zip",
LastModified: pf.CreatedUnix.AsLocalTime(),
+438
View File
@@ -0,0 +1,438 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package terraform
import (
"errors"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"unicode"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
terraform_module "code.gitea.io/gitea/modules/packages/terraform"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
var packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
const (
stateFilename = "tfstate"
)
func apiError(ctx *context.Context, status int, obj any) {
message := helper.ProcessErrorForUser(ctx, status, obj)
ctx.PlainText(status, message)
}
// GetTerraformState serves the latest version of the state
func GetTerraformState(ctx *context.Context) {
stateName := ctx.PathParam("name")
pv, err := getLatestVersion(ctx, stateName)
if errors.Is(err, packages_model.ErrPackageNotExist) {
apiError(ctx, http.StatusNotFound, nil)
return
} else if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
streamState(ctx, stateName, pv.Version)
}
// GetTerraformStateBySerial serves a specific version of terraform state.
func GetTerraformStateBySerial(ctx *context.Context) {
streamState(ctx, ctx.PathParam("name"), ctx.PathParam("serial"))
}
// streamState serves the terraform state file
func streamState(ctx *context.Context, name, serial string) {
s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeTerraformState,
Name: name,
Version: serial,
},
&packages_service.PackageFileInfo{
Filename: stateFilename,
},
ctx.Req.Method,
)
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) {
apiError(ctx, http.StatusNotFound, err)
return
}
apiError(ctx, http.StatusInternalServerError, err)
return
}
helper.ServePackageFile(ctx, s, u, pf)
}
func isValidPackageName(packageName string) bool {
if len(packageName) == 1 && !unicode.IsLetter(rune(packageName[0])) && !unicode.IsNumber(rune(packageName[0])) {
return false
}
return packageNameRegex.MatchString(packageName) && packageName != ".."
}
// UploadState uploads the specific terraform package.
func UploadState(ctx *context.Context) {
packageName := ctx.PathParam("name")
if !isValidPackageName(packageName) {
apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
return
}
lockKey := getLockKey(ctx)
release, err := globallock.Lock(ctx, lockKey)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer release()
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName)
if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if p != nil {
// Check lock
lock, err := terraform_module.GetLock(ctx, p.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
// If the state is locked, enforce the lock
if lock.IsLocked() && lock.ID != ctx.FormString("ID") {
ctx.JSON(http.StatusLocked, lock)
return
}
}
upload, needToClose, err := ctx.UploadStream()
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if needToClose {
defer upload.Close()
}
buf, err := packages_module.CreateHashedBufferFromReader(upload)
if err != nil {
log.Error("Error creating hashed buffer: %v", err)
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer buf.Close()
state, err := terraform_module.ParseState(buf)
if err != nil {
log.Error("Error decoding state: %v", err)
apiError(ctx, http.StatusBadRequest, err)
return
}
if _, err := buf.Seek(0, io.SeekStart); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
ctx,
&packages_service.PackageCreationInfo{
PackageInfo: packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeTerraformState,
Name: packageName,
Version: strconv.FormatUint(state.Serial, 10),
},
Creator: ctx.Doer,
},
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: stateFilename,
},
Creator: ctx.Doer,
Data: buf,
IsLead: true,
},
)
if err != nil {
switch {
case errors.Is(err, packages_model.ErrDuplicatePackageFile):
apiError(ctx, http.StatusConflict, err)
case errors.Is(err, packages_service.ErrQuotaTotalCount), errors.Is(err, packages_service.ErrQuotaTypeSize), errors.Is(err, packages_service.ErrQuotaTotalSize):
apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
ctx.Status(http.StatusCreated)
}
// DeleteStateBySerial deletes the specific serial of a terraform package as long as it's not the latest one.
func DeleteStateBySerial(ctx *context.Context) {
lockKey := getLockKey(ctx)
release, err := globallock.Lock(ctx, lockKey)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer release()
serial := ctx.PathParam("serial")
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, ctx.PathParam("name"), serial)
if errors.Is(err, packages_model.ErrPackageNotExist) {
apiError(ctx, http.StatusNotFound, err)
return
} else if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
pvLatest, err := getLatestVersion(ctx, ctx.PathParam("name"))
if errors.Is(err, packages_model.ErrPackageNotExist) {
apiError(ctx, http.StatusNotFound, err)
return
} else if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if pvLatest.ID == pv.ID {
apiError(ctx, http.StatusForbidden, errors.New("cannot delete the latest version"))
return
}
err = packages_service.DeletePackageVersionAndReferences(ctx, pv)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteState deletes the specific file of a terraform package.
// Fails if the state is locked
func DeleteState(ctx *context.Context) {
packageName := ctx.PathParam("name")
lockKey := getLockKey(ctx)
release, err := globallock.Lock(ctx, lockKey)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer release()
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName)
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) {
apiError(ctx, http.StatusNotFound, err)
return
}
apiError(ctx, http.StatusInternalServerError, err)
return
}
lock, err := terraform_module.GetLock(ctx, p.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if lock.IsLocked() {
apiError(ctx, http.StatusLocked, errors.New("terraform state is locked"))
return
}
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: p.ID,
IsInternal: optional.None[bool](),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
err = packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypePackage, p.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
for _, pv := range pvs {
if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
}
if err := packages_model.DeletePackageByID(ctx, p.ID); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Status(http.StatusOK)
}
// LockState locks the specific terraform state.
// Internally, it adds a property to the package with the lock information
// Caveat being that it allocates a package if one doesn't exist to attach the property
func LockState(ctx *context.Context) {
packageName := ctx.PathParam("name")
if !isValidPackageName(packageName) {
apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
return
}
var reqLockInfo *terraform_module.LockInfo
reqLockInfo, err := terraform_module.ParseLockInfo(ctx.Req.Body)
if err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
lockKey := getLockKey(ctx)
release, err := globallock.Lock(ctx, lockKey)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer release()
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName)
if err != nil {
// If the package doesn't exist, allocate it for the lock.
if errors.Is(err, packages_model.ErrPackageNotExist) {
p = &packages_model.Package{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeTerraformState,
Name: packageName,
LowerName: strings.ToLower(packageName),
}
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
} else {
apiError(ctx, http.StatusInternalServerError, err)
return
}
}
currentLock, err := terraform_module.GetLock(ctx, p.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if currentLock.IsLocked() {
ctx.JSON(http.StatusLocked, currentLock)
return
}
err = terraform_module.SetLock(ctx, p.ID, reqLockInfo)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Status(http.StatusOK)
}
// UnlockState unlock the specific terraform state.
// Internally, it clears the package property
func UnlockState(ctx *context.Context) {
packageName := ctx.PathParam("name")
if !isValidPackageName(packageName) {
apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
return
}
reqLockInfo, err := terraform_module.ParseLockInfo(ctx.Req.Body)
if err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
lockKey := getLockKey(ctx)
release, err := globallock.Lock(ctx, lockKey)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer release()
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName)
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) {
ctx.Status(http.StatusOK)
return
}
apiError(ctx, http.StatusInternalServerError, err)
return
}
existingLock, err := terraform_module.GetLock(ctx, p.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
// we can bypass messing with the lock since it's empty
if !existingLock.IsLocked() {
ctx.Status(http.StatusOK)
return
}
// Unlocking ID must be the same as locker one.
if existingLock.ID != reqLockInfo.ID {
apiError(ctx, http.StatusLocked, errors.New("lock ID mismatch"))
return
}
// We can clear the state if lock id matches
err = terraform_module.RemoveLock(ctx, p.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Status(http.StatusOK)
}
func getLatestVersion(ctx *context.Context, packageName string) (*packages_model.PackageVersion, error) {
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeTerraformState,
Name: packages_model.SearchValue{ExactMatch: true, Value: packageName},
IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
})
if err != nil {
return nil, err
}
if len(pvs) == 0 {
return nil, packages_model.ErrPackageNotExist
}
return pvs[0], nil
}
func getLockKey(ctx *context.Context) string {
return fmt.Sprintf("terraform_lock_%d_%s", ctx.Package.Owner.ID, strings.ToLower(ctx.PathParam("name")))
}
@@ -0,0 +1,36 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package terraform
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidatePackageName(t *testing.T) {
bad := []string{
"",
".",
"..",
"-",
"a?b",
"a b",
"a/b",
}
for _, name := range bad {
assert.False(t, isValidPackageName(name), "bad=%q", name)
}
good := []string{
"a",
"1",
"a-",
"a_b",
"c.d+",
}
for _, name := range good {
assert.True(t, isValidPackageName(name), "good=%q", name)
}
}
+2 -94
View File
@@ -4,103 +4,11 @@
package activitypub
import (
"fmt"
"net/http"
"strings"
"code.gitea.io/gitea/modules/activitypub"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
ap "github.com/go-ap/activitypub"
"github.com/go-ap/jsonld"
)
// Person function returns the Person actor for a user
func Person(ctx *context.APIContext) {
// swagger:operation GET /activitypub/user-id/{user-id} activitypub activitypubPerson
// ---
// summary: Returns the Person actor for a user
// produces:
// - application/json
// parameters:
// - name: user-id
// in: path
// description: user ID of the user
// type: integer
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActivityPub"
// TODO: the setting.AppURL during the test doesn't follow the definition: "It always has a '/' suffix"
link := fmt.Sprintf("%s/api/v1/activitypub/user-id/%d", strings.TrimSuffix(setting.AppURL, "/"), ctx.ContextUser.ID)
person := ap.PersonNew(ap.IRI(link))
person.Name = ap.NaturalLanguageValuesNew()
err := person.Name.Set("en", ap.Content(ctx.ContextUser.FullName))
if err != nil {
ctx.APIErrorInternal(err)
return
}
person.PreferredUsername = ap.NaturalLanguageValuesNew()
err = person.PreferredUsername.Set("en", ap.Content(ctx.ContextUser.Name))
if err != nil {
ctx.APIErrorInternal(err)
return
}
person.URL = ap.IRI(ctx.ContextUser.HTMLURL(ctx))
person.Icon = ap.Image{
Type: ap.ImageType,
MediaType: "image/png",
URL: ap.IRI(ctx.ContextUser.AvatarLink(ctx)),
}
person.Inbox = ap.IRI(link + "/inbox")
person.Outbox = ap.IRI(link + "/outbox")
person.PublicKey.ID = ap.IRI(link + "#main-key")
person.PublicKey.Owner = ap.IRI(link)
publicKeyPem, err := activitypub.GetPublicKey(ctx, ctx.ContextUser)
if err != nil {
ctx.APIErrorInternal(err)
return
}
person.PublicKey.PublicKeyPem = publicKeyPem
binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(person)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType)
ctx.Resp.WriteHeader(http.StatusOK)
if _, err = ctx.Resp.Write(binary); err != nil {
log.Error("write to resp err: %v", err)
}
}
// PersonInbox function handles the incoming data for a user inbox
func PersonInbox(ctx *context.APIContext) {
// swagger:operation POST /activitypub/user-id/{user-id}/inbox activitypub activitypubPersonInbox
// ---
// summary: Send to the inbox
// produces:
// - application/json
// parameters:
// - name: user-id
// in: path
// description: user ID of the user
// type: integer
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
ctx.Status(http.StatusNoContent)
func NotImplemented(ctx *context.APIContext) {
http.Error(ctx.Resp, "Not implemented", http.StatusNotImplemented)
}
@@ -1,98 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package activitypub
import (
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"code.gitea.io/gitea/modules/activitypub"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
gitea_context "code.gitea.io/gitea/services/context"
"github.com/42wim/httpsig"
ap "github.com/go-ap/activitypub"
)
func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err error) {
person := ap.PersonNew(ap.IRI(keyID.String()))
err = person.UnmarshalJSON(b)
if err != nil {
return nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err)
}
pubKey := person.PublicKey
if pubKey.ID.String() != keyID.String() {
return nil, fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, string(b))
}
pubKeyPem := pubKey.PublicKeyPem
block, _ := pem.Decode([]byte(pubKeyPem))
if block == nil || block.Type != "PUBLIC KEY" {
return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
}
p, err = x509.ParsePKIXPublicKey(block.Bytes)
return p, err
}
func fetch(iri *url.URL) (b []byte, err error) {
req := httplib.NewRequest(iri.String(), http.MethodGet)
req.Header("Accept", activitypub.ActivityStreamsContentType)
req.Header("User-Agent", "Gitea/"+setting.AppVer)
resp, err := req.Response()
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("url IRI fetch [%s] failed with status (%d): %s", iri, resp.StatusCode, resp.Status)
}
b, err = io.ReadAll(io.LimitReader(resp.Body, setting.Federation.MaxSize))
return b, err
}
func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, err error) {
r := ctx.Req
// 1. Figure out what key we need to verify
v, err := httpsig.NewVerifier(r)
if err != nil {
return false, err
}
ID := v.KeyId()
idIRI, err := url.Parse(ID)
if err != nil {
return false, err
}
// 2. Fetch the public key of the other actor
b, err := fetch(idIRI)
if err != nil {
return false, err
}
pubKey, err := getPublicKeyFromResponse(b, idIRI)
if err != nil {
return false, err
}
// 3. Verify the other actor's key
algo := httpsig.Algorithm(setting.Federation.Algorithms[0])
authenticated = v.Verify(pubKey, algo) == nil
return authenticated, err
}
// ReqHTTPSignature function
func ReqHTTPSignature() func(ctx *gitea_context.APIContext) {
return func(ctx *gitea_context.APIContext) {
if authenticated, err := verifyHTTPSignatures(ctx); err != nil {
ctx.APIErrorInternal(err)
} else if !authenticated {
ctx.APIError(http.StatusForbidden, "request signature verification failed")
}
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ func ListUnadoptedRepositories(ctx *context.APIContext) {
return
}
ctx.SetTotalCountHeader(int64(count))
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, repoNames)
}
+1 -1
View File
@@ -51,7 +51,7 @@ func GetAllEmails(ctx *context.APIContext) {
results[i] = convert.ToEmailSearch(emails[i])
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}
+1 -1
View File
@@ -77,7 +77,7 @@ func ListHooks(ctx *context.APIContext) {
}
hooks[i] = h
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, hooks)
}
+1 -1
View File
@@ -117,7 +117,7 @@ func GetAllOrgs(ctx *context.APIContext) {
orgs[i] = convert.ToOrganization(ctx, organization.OrgFromUser(users[i]))
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &orgs)
}
+39 -17
View File
@@ -10,21 +10,6 @@ import (
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// GetRegistrationToken returns the token to register global runners
func GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /admin/runners/registration-token admin adminGetRunnerRegistrationToken
// ---
// summary: Get a global actions runner registration token
// produces:
// - application/json
// parameters:
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, 0, 0)
}
// CreateRegistrationToken returns the token to register global runners
func CreateRegistrationToken(ctx *context.APIContext) {
// swagger:operation POST /admin/actions/runners/registration-token admin adminCreateRunnerRegistrationToken
@@ -47,9 +32,15 @@ func ListRunners(ctx *context.APIContext) {
// summary: Get all runners
// produces:
// - application/json
// parameters:
// - name: disabled
// in: query
// description: filter by disabled status (true or false)
// type: boolean
// required: false
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
// "$ref": "#/responses/RunnerList"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -72,7 +63,7 @@ func GetRunner(ctx *context.APIContext) {
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -102,3 +93,34 @@ func DeleteRunner(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
shared.DeleteRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}
// UpdateRunner update a global runner
func UpdateRunner(ctx *context.APIContext) {
// swagger:operation PATCH /admin/actions/runners/{runner_id} admin updateAdminRunner
// ---
// summary: Update a global runner
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditActionRunnerOption"
// responses:
// "200":
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.UpdateRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}
+1 -1
View File
@@ -534,7 +534,7 @@ func SearchUsers(ctx *context.APIContext) {
results[i] = convert.ToUser(ctx, users[i], ctx.Doer)
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}
+42 -53
View File
@@ -11,11 +11,9 @@
//
// Consumes:
// - application/json
// - text/plain
//
// Produces:
// - application/json
// - text/html
//
// Security:
// - BasicAuth :
@@ -77,7 +75,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
@@ -203,7 +200,7 @@ func repoAssignment() func(ctx *context.APIContext) {
if needTwoFactor {
ctx.Repo.Permission = access_model.PermissionNoAccess()
} else {
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
ctx.Repo.Permission, err = access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -756,13 +753,9 @@ func buildAuthGroup() *auth.Group {
&auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API
)
if setting.Service.EnableReverseProxyAuthAPI {
group.Add(&auth.ReverseProxy{})
group.Add(&auth.ReverseProxy{}) // TODO: does it still make sense to support reverse proxy auth in API?
}
if setting.IsWindows && auth_model.IsSSPIEnabled(graceful.GetManager().ShutdownContext()) {
group.Add(&auth.SSPI{}) // it MUST be the last, see the comment of SSPI
}
// others: API doesn't support SSPI auth because the caller should use token
return group
}
@@ -872,9 +865,9 @@ func checkDeprecatedAuthMethods(ctx *context.APIContext) {
func Routes() *web.Router {
m := web.NewRouter()
m.Use(securityHeaders())
m.BeforeRouting(securityHeaders())
if setting.CORSConfig.Enabled {
m.Use(cors.Handler(cors.Options{
m.BeforeRouting(cors.Handler(cors.Options{
AllowedOrigins: setting.CORSConfig.AllowDomain,
AllowedMethods: setting.CORSConfig.Methods,
AllowCredentials: setting.CORSConfig.AllowCredentials,
@@ -882,48 +875,49 @@ func Routes() *web.Router {
MaxAge: int(setting.CORSConfig.MaxAge.Seconds()),
}))
}
m.Use(context.APIContexter())
m.Use(checkDeprecatedAuthMethods)
m.AfterRouting(context.APIContexter())
m.AfterRouting(checkDeprecatedAuthMethods)
// Get user from session if logged in.
m.Use(apiAuth(buildAuthGroup()))
m.AfterRouting(apiAuth(buildAuthGroup()))
m.Use(verifyAuthWithOptions(&common.VerifyOptions{
m.AfterRouting(verifyAuthWithOptions(&common.VerifyOptions{
SignInRequired: setting.Service.RequireSignInViewStrict,
}))
addActionsRoutes := func(
m *web.Router,
reqChecker func(ctx *context.APIContext),
reqReaderCheck func(ctx *context.APIContext),
reqOwnerCheck func(ctx *context.APIContext),
act actions.API,
) {
m.Group("/actions", func() {
m.Group("/secrets", func() {
m.Get("", reqToken(), reqChecker, act.ListActionsSecrets)
m.Get("", reqToken(), reqOwnerCheck, act.ListActionsSecrets)
m.Combo("/{secretname}").
Put(reqToken(), reqChecker, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret).
Delete(reqToken(), reqChecker, act.DeleteSecret)
Put(reqToken(), reqOwnerCheck, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret).
Delete(reqToken(), reqOwnerCheck, act.DeleteSecret)
})
m.Group("/variables", func() {
m.Get("", reqToken(), reqChecker, act.ListVariables)
m.Get("", reqToken(), reqOwnerCheck, act.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqChecker, act.GetVariable).
Delete(reqToken(), reqChecker, act.DeleteVariable).
Post(reqToken(), reqChecker, bind(api.CreateVariableOption{}), act.CreateVariable).
Put(reqToken(), reqChecker, bind(api.UpdateVariableOption{}), act.UpdateVariable)
Get(reqToken(), reqOwnerCheck, act.GetVariable).
Delete(reqToken(), reqOwnerCheck, act.DeleteVariable).
Post(reqToken(), reqOwnerCheck, bind(api.CreateVariableOption{}), act.CreateVariable).
Put(reqToken(), reqOwnerCheck, bind(api.UpdateVariableOption{}), act.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("", reqToken(), reqChecker, act.ListRunners)
m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken)
m.Post("/registration-token", reqToken(), reqChecker, act.CreateRegistrationToken)
m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner)
m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner)
m.Get("", reqToken(), reqOwnerCheck, act.ListRunners)
m.Post("/registration-token", reqToken(), reqOwnerCheck, act.CreateRegistrationToken)
m.Get("/{runner_id}", reqToken(), reqOwnerCheck, act.GetRunner)
m.Delete("/{runner_id}", reqToken(), reqOwnerCheck, act.DeleteRunner)
m.Patch("/{runner_id}", reqToken(), reqOwnerCheck, bind(api.EditActionRunnerOption{}), act.UpdateRunner)
})
m.Get("/runs", reqToken(), reqChecker, act.ListWorkflowRuns)
m.Get("/jobs", reqToken(), reqChecker, act.ListWorkflowJobs)
m.Get("/runs", reqToken(), reqReaderCheck, act.ListWorkflowRuns)
m.Get("/jobs", reqToken(), reqReaderCheck, act.ListWorkflowJobs)
})
}
@@ -936,18 +930,8 @@ func Routes() *web.Router {
}
if setting.Federation.Enabled {
m.Get("/nodeinfo", misc.NodeInfo)
m.Group("/activitypub", func() {
// deprecated, remove in 1.20, use /user-id/{user-id} instead
m.Group("/user/{username}", func() {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
m.Group("/user-id/{user-id}", func() {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
}, context.UserIDAssignmentAPI(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
m.Get("/nodeinfo", activitypub.NotImplemented)
m.Any("/activitypub/*", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub), activitypub.NotImplemented)
}
// Misc (public accessible)
@@ -1055,10 +1039,10 @@ func Routes() *web.Router {
m.Group("/runners", func() {
m.Get("", reqToken(), user.ListRunners)
m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
m.Post("/registration-token", reqToken(), user.CreateRegistrationToken)
m.Get("/{runner_id}", reqToken(), user.GetRunner)
m.Delete("/{runner_id}", reqToken(), user.DeleteRunner)
m.Patch("/{runner_id}", reqToken(), bind(api.EditActionRunnerOption{}), user.UpdateRunner)
})
m.Get("/runs", reqToken(), user.ListWorkflowRuns)
@@ -1176,7 +1160,8 @@ func Routes() *web.Router {
m.Post("/reject", repo.RejectTransfer)
}, reqToken())
addActionsRoutes(m, reqOwner(), repo.NewAction()) // it adds the routes for secrets/variables and runner management
// Adds the routes for secrets/variables and runner management
addActionsRoutes(m, reqRepoReader(unit.TypeActions), reqOwner(), repo.NewAction())
m.Group("/actions/workflows", func() {
m.Get("", repo.ActionsListRepositoryWorkflows)
@@ -1243,10 +1228,10 @@ func Routes() *web.Router {
m.Group("/branch_protections", func() {
m.Get("", repo.ListBranchProtections)
m.Post("", bind(api.CreateBranchProtectionOption{}), mustNotBeArchived, repo.CreateBranchProtection)
m.Group("/{name}", func() {
m.Group("/*", func() {
m.Get("", repo.GetBranchProtection)
m.Patch("", bind(api.EditBranchProtectionOption{}), mustNotBeArchived, repo.EditBranchProtection)
m.Delete("", repo.DeleteBranchProtection)
m.Delete("", mustNotBeArchived, repo.DeleteBranchProtection)
})
m.Post("/priority", bind(api.UpdateBranchProtectionPriories{}), mustNotBeArchived, repo.UpdateBranchProtectionPriories)
}, reqToken(), reqAdmin())
@@ -1271,7 +1256,10 @@ func Routes() *web.Router {
m.Group("/{run}", func() {
m.Get("", repo.GetWorkflowRun)
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
m.Post("/rerun-failed-jobs", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunFailedWorkflowRun)
m.Get("/jobs", repo.ListWorkflowRunJobs)
m.Post("/jobs/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob)
m.Get("/artifacts", repo.GetArtifactsOfRun)
})
})
@@ -1590,10 +1578,11 @@ func Routes() *web.Router {
m.Group("/packages/{username}", func() {
m.Group("/{type}/{name}", func() {
m.Get("/", packages.ListPackageVersions)
m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
m.Group("/{version}", func() {
m.Get("", packages.GetPackage)
m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackageVersion)
m.Get("/files", packages.ListPackageFiles)
})
@@ -1621,7 +1610,8 @@ func Routes() *web.Router {
Delete(reqToken(), reqOrgOwnership(), org.Delete)
m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename)
m.Combo("/repos").Get(user.ListOrgRepos).
Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo).
Delete(reqToken(), reqOrgOwnership(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), org.DeleteOrgRepos)
m.Group("/members", func() {
m.Get("", reqToken(), org.ListMembers)
m.Combo("/{username}").Get(reqToken(), org.IsMember).
@@ -1629,6 +1619,7 @@ func Routes() *web.Router {
})
addActionsRoutes(
m,
reqOrgMembership(),
reqOrgOwnership(),
org.NewAction(),
)
@@ -1740,13 +1731,11 @@ func Routes() *web.Router {
m.Post("/registration-token", admin.CreateRegistrationToken)
m.Get("/{runner_id}", admin.GetRunner)
m.Delete("/{runner_id}", admin.DeleteRunner)
m.Patch("/{runner_id}", bind(api.EditActionRunnerOption{}), admin.UpdateRunner)
})
m.Get("/runs", admin.ListWorkflowRuns)
m.Get("/jobs", admin.ListWorkflowJobs)
})
m.Group("/runners", func() {
m.Get("/registration-token", admin.GetRegistrationToken)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
m.Group("/topics", func() {
-78
View File
@@ -1,78 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package misc
import (
"net/http"
"time"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
)
const cacheKeyNodeInfoUsage = "API_NodeInfoUsage"
// NodeInfo returns the NodeInfo for the Gitea instance to allow for federation
func NodeInfo(ctx *context.APIContext) {
// swagger:operation GET /nodeinfo miscellaneous getNodeInfo
// ---
// summary: Returns the nodeinfo of the Gitea application
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/NodeInfo"
nodeInfoUsage := structs.NodeInfoUsage{}
if setting.Federation.ShareUserStatistics {
cached, _ := ctx.Cache.GetJSON(cacheKeyNodeInfoUsage, &nodeInfoUsage)
if !cached {
usersTotal := int(user_model.CountUsers(ctx, nil))
now := time.Now()
timeOneMonthAgo := now.AddDate(0, -1, 0).Unix()
timeHaveYearAgo := now.AddDate(0, -6, 0).Unix()
usersActiveMonth := int(user_model.CountUsers(ctx, &user_model.CountUserFilter{LastLoginSince: &timeOneMonthAgo}))
usersActiveHalfyear := int(user_model.CountUsers(ctx, &user_model.CountUserFilter{LastLoginSince: &timeHaveYearAgo}))
allIssues, _ := issues_model.CountIssues(ctx, &issues_model.IssuesOptions{})
allComments, _ := issues_model.CountComments(ctx, &issues_model.FindCommentsOptions{})
nodeInfoUsage = structs.NodeInfoUsage{
Users: structs.NodeInfoUsageUsers{
Total: usersTotal,
ActiveMonth: usersActiveMonth,
ActiveHalfyear: usersActiveHalfyear,
},
LocalPosts: int(allIssues),
LocalComments: int(allComments),
}
if err := ctx.Cache.PutJSON(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil {
ctx.APIErrorInternal(err)
return
}
}
}
nodeInfo := &structs.NodeInfo{
Version: "2.1",
Software: structs.NodeInfoSoftware{
Name: "gitea",
Version: setting.AppVer,
Repository: "https://github.com/go-gitea/gitea.git",
Homepage: "https://gitea.io/",
},
Protocols: []string{"activitypub"},
Services: structs.NodeInfoServices{
Inbound: []string{},
Outbound: []string{"rss2.0"},
},
OpenRegistrations: setting.Service.ShowRegistrationButton,
Usage: nodeInfoUsage,
}
ctx.JSON(http.StatusOK, nodeInfo)
}
+1 -1
View File
@@ -125,7 +125,7 @@ func ListRepoNotifications(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(totalCount), opts.PageSize)
ctx.SetLinkHeader(totalCount, opts.PageSize)
ctx.SetTotalCountHeader(totalCount)
ctx.JSON(http.StatusOK, convert.ToNotifications(ctx, nl))
}
+1 -1
View File
@@ -86,7 +86,7 @@ func ListNotifications(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(totalCount), opts.PageSize)
ctx.SetLinkHeader(totalCount, opts.PageSize)
ctx.SetTotalCountHeader(totalCount)
ctx.JSON(http.StatusOK, convert.ToNotifications(ctx, nl))
}
+45 -25
View File
@@ -67,7 +67,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(int(count), opts.PageSize)
ctx.SetLinkHeader(count, opts.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
@@ -170,27 +170,6 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// GetRegistrationToken returns the token to register org runners
func (Action) GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/runners/registration-token organization orgGetRunnerRegistrationToken
// ---
// summary: Get an organization's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
}
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// CreateRegistrationToken returns the token to register org runners
func (Action) CreateRegistrationToken(ctx *context.APIContext) {
@@ -261,7 +240,7 @@ func (Action) ListVariables(ctx *context.APIContext) {
Description: v.Description,
}
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
@@ -506,9 +485,14 @@ func (Action) ListRunners(ctx *context.APIContext) {
// description: name of the organization
// type: string
// required: true
// - name: disabled
// in: query
// description: filter by disabled status (true or false)
// type: boolean
// required: false
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
// "$ref": "#/responses/RunnerList"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -536,7 +520,7 @@ func (Action) GetRunner(ctx *context.APIContext) {
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -572,6 +556,42 @@ func (Action) DeleteRunner(ctx *context.APIContext) {
shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id"))
}
// UpdateRunner update an org-level runner
func (Action) UpdateRunner(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org}/actions/runners/{runner_id} organization updateOrgRunner
// ---
// summary: Update an org-level runner
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditActionRunnerOption"
// responses:
// "200":
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.UpdateRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id"))
}
func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/jobs organization getOrgWorkflowJobs
// ---
+1 -1
View File
@@ -13,7 +13,7 @@ import (
webhook_service "code.gitea.io/gitea/services/webhook"
)
// ListHooks list an organziation's webhooks
// ListHooks list an organization's webhooks
func ListHooks(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/hooks organization orgListHooks
// ---
+1 -1
View File
@@ -45,7 +45,7 @@ func listMembers(ctx *context.APIContext, isMember bool) {
apiMembers[i] = convert.ToUser(ctx, member, ctx.Doer)
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiMembers)
}
+89 -11
View File
@@ -5,15 +5,23 @@
package org
import (
gocontext "context"
"errors"
"fmt"
"net/http"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
system_model "code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
@@ -21,6 +29,7 @@ import (
"code.gitea.io/gitea/services/convert"
feed_service "code.gitea.io/gitea/services/feed"
"code.gitea.io/gitea/services/org"
repo_service "code.gitea.io/gitea/services/repository"
user_service "code.gitea.io/gitea/services/user"
)
@@ -42,7 +51,7 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
apiOrgs[i] = convert.ToOrganization(ctx, orgs[i])
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &apiOrgs)
}
@@ -135,7 +144,7 @@ func GetUserOrgsPermissions(ctx *context.APIContext) {
op := api.OrganizationPermissions{}
if !organization.HasOrgOrUserVisible(ctx, o, ctx.ContextUser) {
if !organization.HasOrgOrUserVisible(ctx, o, ctx.Doer) {
ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
return
}
@@ -215,7 +224,7 @@ func GetAll(ctx *context.APIContext) {
orgs[i] = convert.ToOrganization(ctx, organization.OrgFromUser(publicOrgs[i]))
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &orgs)
}
@@ -379,19 +388,21 @@ func Edit(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditOrgOption)
if form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil {
ctx.APIErrorInternal(err)
if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
opts := &user_service.UpdateOptions{
FullName: optional.Some(form.FullName),
Description: optional.Some(form.Description),
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
FullName: optional.FromPtr(form.FullName),
Description: optional.FromPtr(form.Description),
Website: optional.FromPtr(form.Website),
Location: optional.FromPtr(form.Location),
Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
@@ -493,3 +504,70 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}
func deleteOrgReposBackground(ctx gocontext.Context, org *organization.Organization, repoIDs []int64, doer *user_model.User) {
defer func() {
if r := recover(); r != nil {
log.Error("panic during org repo deletion: %v, stack: %v", r, log.Stack(2))
}
}()
for _, repoID := range repoIDs {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
desc := fmt.Sprintf("Failed to get repository ID %d in org %s: %v", repoID, org.Name, err)
_ = system_model.CreateNotice(ctx, system_model.NoticeRepository, desc)
log.Error("GetRepositoryByID failed: %v", desc)
continue
}
if err := repo_service.DeleteRepository(ctx, doer, repo, true); err != nil {
desc := fmt.Sprintf("Failed to delete repository %s (ID: %d) in org %s: %v", repo.Name, repo.ID, org.Name, err)
_ = system_model.CreateNotice(ctx, system_model.NoticeRepository, desc)
log.Error("DeleteRepository failed: %v", desc)
continue
}
log.Info("Successfully deleted repository %s (ID: %d) in org %s", repo.Name, repo.ID, org.Name)
}
log.Info("Completed deletion of repositories in org %s", org.Name)
}
func DeleteOrgRepos(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/repos organization orgDeleteRepos
// ---
// summary: Delete all repositories in an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "202":
// "$ref": "#/responses/empty"
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// Intentionally it only loads repository IDs to avoid loading too much data into memory
// There is no need to do pagination here as the number of repositories is expected to be manageable
repoIDs, err := repo_model.GetOrgRepositoryIDs(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if len(repoIDs) == 0 {
ctx.Status(http.StatusNoContent)
return
}
// Start deletion (slow) in background with detached context, so it can continue even if the request is canceled
go deleteOrgReposBackground(graceful.GetManager().ShutdownContext(), ctx.Org.Organization, repoIDs, ctx.Doer)
ctx.Status(http.StatusAccepted)
}
+9 -9
View File
@@ -70,7 +70,7 @@ func ListTeams(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiTeams)
}
@@ -111,7 +111,7 @@ func ListUserTeams(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiTeams)
}
@@ -411,7 +411,7 @@ func GetTeamMembers(ctx *context.APIContext) {
members[i] = convert.ToUser(ctx, member, ctx.Doer)
}
ctx.SetLinkHeader(ctx.Org.Team.NumMembers, listOptions.PageSize)
ctx.SetLinkHeader(int64(ctx.Org.Team.NumMembers), listOptions.PageSize)
ctx.SetTotalCountHeader(int64(ctx.Org.Team.NumMembers))
ctx.JSON(http.StatusOK, members)
}
@@ -576,14 +576,14 @@ func GetTeamRepos(ctx *context.APIContext) {
}
repos := make([]*api.Repository, len(teamRepos))
for i, repo := range teamRepos {
permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
repos[i] = convert.ToRepo(ctx, repo, permission)
}
ctx.SetLinkHeader(team.NumRepos, listOptions.PageSize)
ctx.SetLinkHeader(int64(team.NumRepos), listOptions.PageSize)
ctx.SetTotalCountHeader(int64(team.NumRepos))
ctx.JSON(http.StatusOK, repos)
}
@@ -628,7 +628,7 @@ func GetTeamRepo(ctx *context.APIContext) {
return
}
permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -806,7 +806,7 @@ func SearchTeam(ctx *context.APIContext) {
ListOptions: listOptions,
}
// Only admin is allowd to search for all teams
// Only admin is allowed to search for all teams
if !ctx.Doer.IsAdmin {
opts.UserID = ctx.Doer.ID
}
@@ -827,7 +827,7 @@ func SearchTeam(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, map[string]any{
"ok": true,
@@ -882,7 +882,7 @@ func ListTeamActivityFeeds(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}
+39 -4
View File
@@ -43,7 +43,7 @@ func ListPackages(ctx *context.APIContext) {
// in: query
// description: package type filter
// type: string
// enum: [alpine, cargo, chef, composer, conan, conda, container, cran, debian, generic, go, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, vagrant]
// enum: [alpine, cargo, chef, composer, conan, conda, container, cran, debian, generic, go, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, terraform, vagrant]
// - name: q
// in: query
// description: name filter
@@ -68,7 +68,7 @@ func ListPackages(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiPackages)
}
@@ -118,7 +118,7 @@ func GetPackage(ctx *context.APIContext) {
// DeletePackage deletes a package
func DeletePackage(ctx *context.APIContext) {
// swagger:operation DELETE /packages/{owner}/{type}/{name}/{version} package deletePackage
// swagger:operation DELETE /packages/{owner}/{type}/{name} package deletePackage
// ---
// summary: Delete a package
// parameters:
@@ -137,6 +137,41 @@ func DeletePackage(ctx *context.APIContext) {
// description: name of the package
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
err := packages_service.RemovePackage(ctx, ctx.Doer, ctx.Package.Descriptor.Package)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeletePackageVersion deletes a package version
func DeletePackageVersion(ctx *context.APIContext) {
// swagger:operation DELETE /packages/{owner}/{type}/{name}/{version} package deletePackageVersion
// ---
// summary: Delete a package version
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// - name: version
// in: path
// description: version of the package
@@ -249,7 +284,7 @@ func ListPackageVersions(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiPackages)
}
+290 -72
View File
@@ -12,6 +12,7 @@ import (
"fmt"
"net/http"
"net/url"
"slices"
"strconv"
"strings"
"time"
@@ -90,7 +91,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
Created: v.CreatedUnix.AsTime(),
}
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
@@ -505,36 +506,11 @@ func (Action) ListVariables(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
// GetRegistrationToken returns the token to register repo runners
func (Action) GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runners/registration-token repository repoGetRunnerRegistrationToken
// ---
// summary: Get a repository's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID)
}
// CreateRegistrationToken returns the token to register repo runners
func (Action) CreateRegistrationToken(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/runners/registration-token repository repoCreateRunnerRegistrationToken
@@ -578,9 +554,14 @@ func (Action) ListRunners(ctx *context.APIContext) {
// description: name of the repo
// type: string
// required: true
// - name: disabled
// in: query
// description: filter by disabled status (true or false)
// type: boolean
// required: false
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
// "$ref": "#/responses/RunnerList"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -588,11 +569,11 @@ func (Action) ListRunners(ctx *context.APIContext) {
shared.ListRunners(ctx, 0, ctx.Repo.Repository.ID)
}
// GetRunner get an repo-level runner
// GetRunner get a repo-level runner
func (Action) GetRunner(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runners/{runner_id} repository getRepoRunner
// ---
// summary: Get an repo-level runner
// summary: Get a repo-level runner
// produces:
// - application/json
// parameters:
@@ -613,7 +594,7 @@ func (Action) GetRunner(ctx *context.APIContext) {
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -621,11 +602,11 @@ func (Action) GetRunner(ctx *context.APIContext) {
shared.GetRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}
// DeleteRunner delete an repo-level runner
// DeleteRunner delete a repo-level runner
func (Action) DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/actions/runners/{runner_id} repository deleteRepoRunner
// ---
// summary: Delete an repo-level runner
// summary: Delete a repo-level runner
// produces:
// - application/json
// parameters:
@@ -654,6 +635,47 @@ func (Action) DeleteRunner(ctx *context.APIContext) {
shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}
// UpdateRunner update a repo-level runner
func (Action) UpdateRunner(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/actions/runners/{runner_id} repository updateRepoRunner
// ---
// summary: Update a repo-level runner
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditActionRunnerOption"
// responses:
// "200":
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.UpdateRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}
// GetWorkflowRunJobs Lists all jobs for a workflow run.
func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs
@@ -835,7 +857,7 @@ func ListActionTasks(ctx *context.APIContext) {
res.Entries[i] = convertedTask
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total) // Duplicates api response field but it's better to set it for consistency
ctx.JSON(http.StatusOK, &res)
}
@@ -1004,9 +1026,15 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
// in: body
// schema:
// "$ref": "#/definitions/CreateActionWorkflowDispatch"
// - name: return_run_details
// description: Whether the response should include the workflow run ID and URLs.
// in: query
// type: boolean
// responses:
// "200":
// "$ref": "#/responses/RunDetails"
// "204":
// description: No Content
// description: No Content, if return_run_details is missing or false
// "400":
// "$ref": "#/responses/error"
// "403":
@@ -1023,7 +1051,7 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
return
}
err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
runID, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
if strings.Contains(ctx.Req.Header.Get("Content-Type"), "form-urlencoded") {
// The chi framework's "Binding" doesn't support to bind the form map values into a map[string]string
// So we have to manually read the `inputs[key]` from the form
@@ -1054,7 +1082,16 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
return
}
ctx.Status(http.StatusNoContent)
if !ctx.FormBool("return_run_details") {
ctx.Status(http.StatusNoContent)
return
}
ctx.JSON(http.StatusOK, &api.RunDetails{
WorkflowRunID: runID,
HTMLURL: fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.Repository.HTMLURL(ctx), runID),
RunURL: fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.Repository.APIURL(), runID),
})
}
func ActionsEnableWorkflow(ctx *context.APIContext) {
@@ -1107,6 +1144,33 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
func getCurrentRepoActionRunByID(ctx *context.APIContext) *actions_model.ActionRun {
runID := ctx.PathParamInt64("run")
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
return nil
} else if err != nil {
ctx.APIErrorInternal(err)
return nil
}
return run
}
func getCurrentRepoActionRunJobsByID(ctx *context.APIContext) (*actions_model.ActionRun, actions_model.ActionJobList) {
run := getCurrentRepoActionRunByID(ctx)
if ctx.Written() {
return nil, nil
}
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
return run, jobs
}
// GetWorkflowRun Gets a specific workflow run.
func GetWorkflowRun(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun
@@ -1128,7 +1192,7 @@ func GetWorkflowRun(ctx *context.APIContext) {
// - name: run
// in: path
// description: id of the run
// type: string
// type: integer
// required: true
// responses:
// "200":
@@ -1138,19 +1202,12 @@ func GetWorkflowRun(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
runID := ctx.PathParamInt64("run")
job, has, err := db.GetByID[actions_model.ActionRun](ctx, runID)
if err != nil {
ctx.APIErrorInternal(err)
run := getCurrentRepoActionRunByID(ctx)
if ctx.Written() {
return
}
if !has || job.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound(util.ErrNotExist)
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job)
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -1158,6 +1215,179 @@ func GetWorkflowRun(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, convertedRun)
}
// RerunWorkflowRun Reruns an entire workflow run.
func RerunWorkflowRun(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/rerun repository rerunWorkflowRun
// ---
// summary: Reruns an entire workflow run
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: run
// in: path
// description: id of the run
// type: integer
// required: true
// responses:
// "201":
// "$ref": "#/responses/WorkflowRun"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
run, jobs := getCurrentRepoActionRunJobsByID(ctx)
if ctx.Written() {
return
}
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs); err != nil {
handleWorkflowRerunError(ctx, err)
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convertedRun)
}
// RerunFailedWorkflowRun Reruns all failed jobs in a workflow run.
func RerunFailedWorkflowRun(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/rerun-failed-jobs repository rerunFailedWorkflowRun
// ---
// summary: Reruns all failed jobs in a workflow run
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: run
// in: path
// description: id of the run
// type: integer
// required: true
// responses:
// "201":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
run, jobs := getCurrentRepoActionRunJobsByID(ctx)
if ctx.Written() {
return
}
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil {
handleWorkflowRerunError(ctx, err)
return
}
ctx.Status(http.StatusCreated)
}
// RerunWorkflowJob Reruns a specific workflow job in a run.
func RerunWorkflowJob(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun repository rerunWorkflowJob
// ---
// summary: Reruns a specific workflow job in a run
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: run
// in: path
// description: id of the run
// type: integer
// required: true
// - name: job_id
// in: path
// description: id of the job
// type: integer
// required: true
// responses:
// "201":
// "$ref": "#/responses/WorkflowJob"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
run, jobs := getCurrentRepoActionRunJobsByID(ctx)
if ctx.Written() {
return
}
jobID := ctx.PathParamInt64("job_id")
jobIdx := slices.IndexFunc(jobs, func(job *actions_model.ActionRunJob) bool { return job.ID == jobID })
if jobIdx == -1 {
ctx.APIErrorNotFound(util.NewNotExistErrorf("workflow job with id %d", jobID))
return
}
targetJob := jobs[jobIdx]
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetAllRerunJobs(targetJob, jobs)); err != nil {
handleWorkflowRerunError(ctx, err)
return
}
convertedJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, targetJob)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convertedJob)
}
func handleWorkflowRerunError(ctx *context.APIContext, err error) {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
return
}
ctx.APIErrorInternal(err)
}
// ListWorkflowRunJobs Lists all jobs for a workflow run.
func ListWorkflowRunJobs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository listWorkflowRunJobs
@@ -1202,9 +1432,7 @@ func ListWorkflowRunJobs(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
repoID := ctx.Repo.Repository.ID
runID := ctx.PathParamInt64("run")
repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
// Avoid the list all jobs functionality for this api route to be used with a runID == 0.
if runID <= 0 {
@@ -1304,10 +1532,8 @@ func GetArtifactsOfRun(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
repoID := ctx.Repo.Repository.ID
artifactName := ctx.Req.URL.Query().Get("name")
runID := ctx.PathParamInt64("run")
repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
artifacts, total, err := db.FindAndCount[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
RepoID: repoID,
@@ -1368,15 +1594,11 @@ func DeleteActionRun(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
runID := ctx.PathParamInt64("run")
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
return
} else if err != nil {
ctx.APIErrorInternal(err)
run := getCurrentRepoActionRunByID(ctx)
if ctx.Written() {
return
}
if !run.Status.IsDone() {
ctx.APIError(http.StatusBadRequest, "this workflow run is not done")
return
@@ -1562,7 +1784,7 @@ func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) str
func buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string {
// endPoint is a path like "api/v1/repos/owner/repo/actions/artifacts/1/zip/raw"
expires := time.Now().Add(60 * time.Minute).Unix()
uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.URLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10)
uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.RawURLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10)
return uploadURL
}
@@ -1607,18 +1829,16 @@ func DownloadArtifact(ctx *context.APIContext) {
ctx.APIError(http.StatusNotFound, "Artifact has expired")
return
}
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName))
if actions.IsArtifactV4(art) {
ok, err := actions.DownloadArtifactV4ServeDirectOnly(ctx.Base, art)
if ok {
return
}
if err != nil {
ctx.APIErrorInternal(err)
// @actions/toolkit asserts that downloaded artifacts of a different runid return 302
// https://github.com/actions/toolkit/blob/44d43b5490b02998bd09b0c4ff369a4cc67876c2/packages/artifact/src/internal/download/download-artifact.ts#L203-L210
if actions.DownloadArtifactV4ServeDirect(ctx.Base, art) {
return
}
// @actions/toolkit asserts a 302 for the artifact download, so we have to build a signed URL and redirect to it
// TODO: a perma link to the code for reference
redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository, art.ID), art.ID)
ctx.Redirect(redirectURL, http.StatusFound)
return
@@ -1646,7 +1866,7 @@ func DownloadArtifactRaw(ctx *context.APIContext) {
sigStr := ctx.Req.URL.Query().Get("sig")
expiresStr := ctx.Req.URL.Query().Get("expires")
sigBytes, _ := base64.URLEncoding.DecodeString(sigStr)
sigBytes, _ := base64.RawURLEncoding.DecodeString(sigStr)
expires, _ := strconv.ParseInt(expiresStr, 10, 64)
expectedSig := buildSignature(buildDownloadRawEndpoint(repo, art.ID), expires, art.ID)
@@ -1665,8 +1885,6 @@ func DownloadArtifactRaw(ctx *context.APIContext) {
ctx.APIError(http.StatusNotFound, "Artifact has expired")
return
}
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName))
if actions.IsArtifactV4(art) {
err := actions.DownloadArtifactV4(ctx.Base, art)
if err != nil {
+6 -2
View File
@@ -43,9 +43,13 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
jobID := ctx.PathParamInt64("job_id")
curJob, err := actions_model.GetRunJobByID(ctx, jobID)
curJob, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobID)
if err != nil {
ctx.APIErrorInternal(err)
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if err = curJob.LoadRepo(ctx); err != nil {
+5 -5
View File
@@ -150,7 +150,7 @@ func DeleteBranch(ctx *context.APIContext) {
}
}
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName, nil); err != nil {
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
switch {
case git.IsErrBranchNotExist(err):
ctx.APIErrorNotFound(err)
@@ -375,7 +375,7 @@ func ListBranches(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(int(totalNumOfBranches), listOptions.PageSize)
ctx.SetLinkHeader(totalNumOfBranches, listOptions.PageSize)
ctx.SetTotalCountHeader(totalNumOfBranches)
ctx.JSON(http.StatusOK, apiBranches)
}
@@ -563,7 +563,7 @@ func GetBranchProtection(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
bpName := ctx.PathParam("name")
bpName := ctx.PathParam("*")
bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil {
ctx.APIErrorInternal(err)
@@ -845,7 +845,7 @@ func EditBranchProtection(ctx *context.APIContext) {
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.EditBranchProtectionOption)
repo := ctx.Repo.Repository
bpName := ctx.PathParam("name")
bpName := ctx.PathParam("*")
protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil {
ctx.APIErrorInternal(err)
@@ -1168,7 +1168,7 @@ func DeleteBranchProtection(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
bpName := ctx.PathParam("name")
bpName := ctx.PathParam("*")
bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil {
ctx.APIErrorInternal(err)
+1 -1
View File
@@ -291,7 +291,7 @@ func GetRepoPermissions(ctx *context.APIContext) {
return
}
permission, err := access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, collaborator)
permission, err := access_model.GetIndividualUserRepoPermission(ctx, ctx.Repo.Repository, collaborator)
if err != nil {
ctx.APIErrorInternal(err)
return
+1 -1
View File
@@ -290,7 +290,7 @@ func GetAllCommits(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(int(commitsCountTotal), listOptions.PageSize)
ctx.SetLinkHeader(commitsCountTotal, listOptions.PageSize)
ctx.SetTotalCountHeader(commitsCountTotal)
// kept for backwards compatibility
+11 -34
View File
@@ -17,9 +17,9 @@ import (
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
api "code.gitea.io/gitea/modules/structs"
@@ -138,7 +138,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
// LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file
if blob.Size() > lfs.MetaFileMaxSize {
// First handle caching for the blob
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
return
}
@@ -151,35 +151,18 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
// OK, now the blob is known to have at most 1024 (lfs pointer max size) bytes,
// we can simply read this in one go (This saves reading it twice)
dataRc, err := blob.DataAsync()
lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize)
if err != nil {
ctx.APIErrorInternal(err)
return
}
buf, err := io.ReadAll(dataRc)
if err != nil {
_ = dataRc.Close()
ctx.APIErrorInternal(err)
return
}
if err := dataRc.Close(); err != nil {
log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Repo.Repository, err)
}
// Check if the blob represents a pointer
pointer, _ := lfs.ReadPointer(bytes.NewReader(buf))
pointer, _ := lfs.ReadPointerFromBuffer(lfsPointerBuf)
// if it's not a pointer, just serve the data directly
if !pointer.IsValid() {
// First handle caching for the blob
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
return
}
// If not cached - serve!
common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
_, _ = ctx.Resp.Write(lfsPointerBuf)
return
}
@@ -188,12 +171,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
// If there isn't one, just serve the data directly
if errors.Is(err, git_model.ErrLFSObjectNotExist) {
// Handle caching for the blob SHA (not the LFS object OID)
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
return
}
common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
_, _ = ctx.Resp.Write(lfsPointerBuf)
return
} else if err != nil {
ctx.APIErrorInternal(err)
@@ -201,27 +179,26 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
}
// Handle caching for the LFS object OID
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`, meta.UpdatedUnix.AsTimePtr()) {
return
}
if setting.LFS.Storage.ServeDirect() {
// If we have a signed url (S3, object storage), redirect to this directly.
u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), ctx.Req.Method, nil)
u, err := storage.LFS.ServeDirectURL(pointer.RelativePath(), blob.Name(), ctx.Req.Method, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
return
}
}
lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
lfsDataFile, err := lfs.ReadMetaObject(meta.Pointer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
defer lfsDataRc.Close()
common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc)
defer lfsDataFile.Close()
httplib.ServeUserContentByFile(ctx.Base.Req, ctx.Base.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath})
}
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) {
+39 -33
View File
@@ -6,7 +6,6 @@ package repo
import (
"errors"
"fmt"
"net/http"
"code.gitea.io/gitea/models/organization"
@@ -14,6 +13,7 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -71,7 +71,7 @@ func ListForks(ctx *context.APIContext) {
apiForks := make([]*api.Repository, len(forks))
for i, fork := range forks {
permission, err := access_model.GetUserRepoPermission(ctx, fork, ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, fork, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -83,6 +83,35 @@ func ListForks(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiForks)
}
func prepareDoerCreateRepoInOrg(ctx *context.APIContext, orgName string) *organization.Organization {
org, err := organization.GetOrgByName(ctx, orgName)
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound()
return nil
} else if err != nil {
ctx.APIErrorInternal(err)
return nil
}
if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound()
return nil
}
if !ctx.Doer.IsAdmin {
canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return nil
}
if !canCreate {
ctx.APIError(http.StatusForbidden, "User is not allowed to create repositories in this organization.")
return nil
}
}
return org
}
// CreateFork create a fork of a repo
func CreateFork(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/forks repository createFork
@@ -118,41 +147,18 @@ func CreateFork(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateForkOption)
repo := ctx.Repo.Repository
var forker *user_model.User // user/org that will own the fork
if form.Organization == nil {
forker = ctx.Doer
} else {
org, err := organization.GetOrgByName(ctx, *form.Organization)
if err != nil {
if organization.IsErrOrgNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
forkOwner := ctx.Doer // user/org that will own the fork
if form.Organization != nil {
org := prepareDoerCreateRepoInOrg(ctx, *form.Organization)
if ctx.Written() {
return
}
if !ctx.Doer.IsAdmin {
isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
} else if !isMember {
ctx.APIError(http.StatusForbidden, fmt.Sprintf("User is no Member of Organisation '%s'", org.Name))
return
}
}
forker = org.AsUser()
forkOwner = org.AsUser()
}
var name string
if form.Name == nil {
name = repo.Name
} else {
name = *form.Name
}
fork, err := repo_service.ForkRepository(ctx, ctx.Doer, forker, repo_service.ForkRepoOptions{
repo := ctx.Repo.Repository
name := optional.FromPtr(form.Name).ValueOrDefault(repo.Name)
fork, err := repo_service.ForkRepository(ctx, ctx.Doer, forkOwner, repo_service.ForkRepoOptions{
BaseRepo: repo,
Name: name,
Description: repo.Description,
+2 -2
View File
@@ -34,7 +34,7 @@ func GetGitAllRefs(ctx *context.APIContext) {
// required: true
// responses:
// "200":
// # "$ref": "#/responses/Reference" TODO: swagger doesnt support different output formats by ref
// # "$ref": "#/responses/Reference" TODO: swagger doesn't support different output formats by ref
// "$ref": "#/responses/ReferenceList"
// "404":
// "$ref": "#/responses/notFound"
@@ -67,7 +67,7 @@ func GetGitRefs(ctx *context.APIContext) {
// required: true
// responses:
// "200":
// # "$ref": "#/responses/Reference" TODO: swagger doesnt support different output formats by ref
// # "$ref": "#/responses/Reference" TODO: swagger doesn't support different output formats by ref
// "$ref": "#/responses/ReferenceList"
// "404":
// "$ref": "#/responses/notFound"
+32 -4
View File
@@ -157,6 +157,10 @@ func SearchIssues(ctx *context.APIContext) {
// in: query
// description: Filter by repository owner
// type: string
// - name: created_by
// in: query
// description: Only show items which were created by the given user
// type: string
// - name: team
// in: query
// description: Filter by team (requires organization owner parameter)
@@ -257,6 +261,14 @@ func SearchIssues(ctx *context.APIContext) {
searchOpt.UpdatedBeforeUnix = optional.Some(before)
}
createdByID := getUserIDForFilter(ctx, "created_by")
if ctx.Written() {
return
}
if createdByID > 0 {
searchOpt.PosterID = strconv.FormatInt(createdByID, 10)
}
if ctx.IsSigned {
ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") {
@@ -287,7 +299,7 @@ func SearchIssues(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(total), limit)
ctx.SetLinkHeader(total, limit)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}
@@ -515,7 +527,7 @@ func ListIssues(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}
@@ -714,6 +726,9 @@ func EditIssue(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/issues/{index} issue issueEditIssue
// ---
// summary: Edit an issue. If using deadline only the date will be taken into account, and time of day ignored.
// description: |
// Pass `content_version` to enable optimistic locking on body edits.
// If the version doesn't match the current value, the request fails with 409 Conflict.
// consumes:
// - application/json
// produces:
@@ -773,6 +788,15 @@ func EditIssue(ctx *context.APIContext) {
return
}
// Fail fast: if content_version is provided and already stale, reject
// before any mutations. The DB-level check in ChangeContent still
// handles concurrent requests.
// TODO: wrap all mutations in a transaction to fully prevent partial writes.
if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion {
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged)
return
}
if len(form.Title) > 0 {
err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
if err != nil {
@@ -781,10 +805,14 @@ func EditIssue(ctx *context.APIContext) {
}
}
if form.Body != nil {
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion)
contentVersion := issue.ContentVersion
if form.ContentVersion != nil {
contentVersion = *form.ContentVersion
}
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion)
if err != nil {
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusConflict, err)
return
}
+1 -1
View File
@@ -186,7 +186,7 @@ func CreateIssueAttachment(ctx *context.APIContext) {
}
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
attachment, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Attachment.AllowedTypes, &repo_model.Attachment{
attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
+1 -1
View File
@@ -219,7 +219,7 @@ func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *issue
if err != nil {
return false
}
perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, user)
perm, err := access_model.GetDoerRepoPermission(ctx, c.RefRepo, user)
if err != nil {
return false
}
@@ -193,7 +193,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
}
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
attachment, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Attachment.AllowedTypes, &repo_model.Attachment{
attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
+5 -5
View File
@@ -81,7 +81,7 @@ func GetIssueDependencies(ctx *context.APIContext) {
canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
blockerIssues := make([]*issues_model.Issue, 0, listOptions.PageSize)
blockerIssues := make([]*issues_model.Issue, 0, min(listOptions.PageSize, setting.API.MaxResponseItems))
// 2. Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
blockersInfo, total, err := issue.BlockedByDependencies(ctx, listOptions)
@@ -102,7 +102,7 @@ func GetIssueDependencies(ctx *context.APIContext) {
perm = existPerm
} else {
var err error
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
perm, err = access_model.GetDoerRepoPermission(ctx, &blocker.Repository, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -140,7 +140,7 @@ func GetIssueDependencies(ctx *context.APIContext) {
}
blockerIssues = append(blockerIssues, &blocker.Issue)
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, blockerIssues))
}
@@ -351,7 +351,7 @@ func GetIssueBlocks(ctx *context.APIContext) {
perm = existPerm
} else {
var err error
perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
perm, err = access_model.GetDoerRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -537,7 +537,7 @@ func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository)
return &ctx.Repo.Permission
}
perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return nil
+4 -6
View File
@@ -175,7 +175,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) {
// schema:
// "$ref": "#/definitions/EditReactionOption"
// responses:
// "200":
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
@@ -248,8 +248,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
ctx.APIErrorInternal(err)
return
}
// ToDo respond 204
ctx.Status(http.StatusOK)
ctx.Status(http.StatusNoContent)
}
}
@@ -408,7 +407,7 @@ func DeleteIssueReaction(ctx *context.APIContext) {
// schema:
// "$ref": "#/definitions/EditReactionOption"
// responses:
// "200":
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
@@ -464,7 +463,6 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
ctx.APIErrorInternal(err)
return
}
// ToDo respond 204
ctx.Status(http.StatusOK)
ctx.Status(http.StatusNoContent)
}
}
+1 -1
View File
@@ -138,7 +138,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) {
return
}
// If watch state wont change
// If watch state won't change
if current == watch {
ctx.Status(http.StatusOK)
return
+1 -1
View File
@@ -179,7 +179,7 @@ func ListPushMirrors(ctx *context.APIContext) {
responsePushMirrors = append(responsePushMirrors, m)
}
}
ctx.SetLinkHeader(len(responsePushMirrors), utils.GetListOptions(ctx).PageSize)
ctx.SetLinkHeader(int64(len(responsePushMirrors)), utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, responsePushMirrors)
}
+22 -7
View File
@@ -154,7 +154,7 @@ func ListPullRequests(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &apiPrs)
}
@@ -657,6 +657,15 @@ func EditPullRequest(ctx *context.APIContext) {
return
}
// Fail fast: if content_version is provided and already stale, reject
// before any mutations. The DB-level check in ChangeContent still
// handles concurrent requests.
// TODO: wrap all mutations in a transaction to fully prevent partial writes.
if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion {
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged)
return
}
if len(form.Title) > 0 {
err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
if err != nil {
@@ -665,10 +674,14 @@ func EditPullRequest(ctx *context.APIContext) {
}
}
if form.Body != nil {
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion)
contentVersion := issue.ContentVersion
if form.ContentVersion != nil {
contentVersion = *form.ContentVersion
}
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion)
if err != nil {
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusConflict, err)
return
}
@@ -898,6 +911,8 @@ func MergePullRequest(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "405":
@@ -1113,7 +1128,7 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
}()
// user should have permission to read baseRepo's codes and pulls, NOT headRepo's
permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
permBase, err := access_model.GetDoerRepoPermission(ctx, baseRepo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
@@ -1127,7 +1142,7 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
// user should have permission to read headRepo's codes
// TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it.
permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer)
permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
@@ -1449,7 +1464,7 @@ func GetPullRequestCommits(ctx *context.APIContext) {
apiCommits = append(apiCommits, apiCommit)
}
ctx.SetLinkHeader(totalNumberOfCommits, listOptions.PageSize)
ctx.SetLinkHeader(int64(totalNumberOfCommits), listOptions.PageSize)
ctx.SetTotalCountHeader(int64(totalNumberOfCommits))
ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page))
@@ -1591,7 +1606,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.BaseRepo, endCommitID))
}
ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize)
ctx.SetLinkHeader(int64(totalNumberOfFiles), listOptions.PageSize)
ctx.SetTotalCountHeader(int64(totalNumberOfFiles))
ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page))
+1 -1
View File
@@ -833,7 +833,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
return
}
permDoer, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer)
permDoer, err := access_model.GetDoerRepoPermission(ctx, pr.Issue.Repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
+16 -30
View File
@@ -10,7 +10,6 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
@@ -22,26 +21,19 @@ import (
release_service "code.gitea.io/gitea/services/release"
)
func hasRepoWriteScope(ctx *context.APIContext) bool {
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
if ctx.Data["IsApiToken"] != true || !ok {
return true
}
requiredScopes := auth_model.GetRequiredScopes(auth_model.Write, auth_model.AccessTokenScopeCategoryRepository)
allow, err := scope.HasScope(requiredScopes...)
if err != nil {
ctx.APIError(http.StatusForbidden, "checking scope failed: "+err.Error())
return false
}
return allow
}
func canAccessDraftRelease(ctx *context.APIContext) bool {
func canAccessReleaseDraft(ctx *context.APIContext) bool {
if !ctx.IsSigned || !ctx.Repo.CanWrite(unit.TypeReleases) {
return false
}
return hasRepoWriteScope(ctx)
if ctx.Data["IsApiToken"] != true {
// not API token request, the request is from a user session with write access
return true
}
// the request is from an access token with scope
scope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
requiredScopes := auth_model.GetRequiredScopes(auth_model.Write, auth_model.AccessTokenScopeCategoryRepository)
allow, _ := scope.HasScope(requiredScopes...) // err (invalid token) can be safely ignored
return allow
}
// GetRelease get a single release of a repository
@@ -85,13 +77,9 @@ func GetRelease(ctx *context.APIContext) {
return
}
if release.IsDraft { // only the users with write access can see draft releases
if !canAccessDraftRelease(ctx) {
if !ctx.Written() {
ctx.APIErrorNotFound()
}
return
}
if release.IsDraft && !canAccessReleaseDraft(ctx) { // only the users with write access can see draft releases
ctx.APIErrorNotFound()
return
}
if err := release.LoadAttributes(ctx); err != nil {
@@ -162,7 +150,7 @@ func ListReleases(ctx *context.APIContext) {
// required: true
// - name: draft
// in: query
// description: filter (exclude / include) drafts, if you dont have repo write access none will show
// description: filter (exclude / include) drafts, if you don't have repo write access none will show
// type: boolean
// - name: pre-release
// in: query
@@ -182,14 +170,12 @@ func ListReleases(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
includeDrafts := (ctx.Repo.AccessMode >= perm.AccessModeWrite || ctx.Repo.UnitAccessMode(unit.TypeReleases) >= perm.AccessModeWrite) && hasRepoWriteScope(ctx)
if ctx.Written() {
return
}
opts := repo_model.FindReleasesOptions{
ListOptions: listOptions,
IncludeDrafts: includeDrafts,
IncludeDrafts: canAccessReleaseDraft(ctx),
IncludeTags: false,
IsDraft: ctx.FormOptionalBool("draft"),
IsPreRelease: ctx.FormOptionalBool("pre-release"),
@@ -216,7 +202,7 @@ func ListReleases(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(filteredCount), listOptions.PageSize)
ctx.SetLinkHeader(filteredCount, listOptions.PageSize)
ctx.SetTotalCountHeader(filteredCount)
ctx.JSON(http.StatusOK, rels)
}
+7 -15
View File
@@ -34,13 +34,9 @@ func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool {
ctx.APIErrorNotFound()
return false
}
if release.IsDraft {
if !canAccessDraftRelease(ctx) {
if !ctx.Written() {
ctx.APIErrorNotFound()
}
return false
}
if release.IsDraft && !canAccessReleaseDraft(ctx) {
ctx.APIErrorNotFound()
return false
}
return true
}
@@ -149,13 +145,9 @@ func ListReleaseAttachments(ctx *context.APIContext) {
ctx.APIErrorNotFound()
return
}
if release.IsDraft {
if !canAccessDraftRelease(ctx) {
if !ctx.Written() {
ctx.APIErrorNotFound()
}
return
}
if release.IsDraft && !canAccessReleaseDraft(ctx) {
ctx.APIErrorNotFound()
return
}
if err := release.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
@@ -250,7 +242,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
}
// Create a new attachment and save the file
attach, err := attachment_service.UploadAttachmentReleaseSizeLimit(ctx, uploaderFile, setting.Repository.Release.AllowedTypes, &repo_model.Attachment{
attach, err := attachment_service.UploadAttachmentForRelease(ctx, uploaderFile, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
+43 -96
View File
@@ -221,7 +221,7 @@ func Search(ctx *context.APIContext) {
})
return
}
permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.JSON(http.StatusInternalServerError, api.SearchError{
OK: false,
@@ -230,7 +230,7 @@ func Search(ctx *context.APIContext) {
}
results[i] = convert.ToRepo(ctx, repo, permission)
}
ctx.SetLinkHeader(int(count), opts.PageSize)
ctx.SetLinkHeader(count, opts.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, api.SearchResults{
OK: true,
@@ -498,31 +498,11 @@ func CreateOrgRepo(ctx *context.APIContext) {
// "403":
// "$ref": "#/responses/forbidden"
opt := web.GetForm(ctx).(*api.CreateRepoOption)
org, err := organization.GetOrgByName(ctx, ctx.PathParam("org"))
if err != nil {
if organization.IsErrOrgNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
orgName := ctx.PathParam("org")
org := prepareDoerCreateRepoInOrg(ctx, orgName)
if ctx.Written() {
return
}
if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
return
}
if !ctx.Doer.IsAdmin {
canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
} else if !canCreate {
ctx.APIError(http.StatusForbidden, "Given user is not allowed to create repository in organization.")
return
}
}
CreateUserRepo(ctx, org.AsUser(), *opt)
}
@@ -588,7 +568,7 @@ func GetByID(ctx *context.APIContext) {
return
}
permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -884,77 +864,44 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
}
}
if opts.HasPullRequests != nil && !unit_model.TypePullRequests.UnitGlobalDisabled() {
if *opts.HasPullRequests {
// We do allow setting individual PR settings through the API, so
// we get the config settings and then set them
// if those settings were provided in the opts.
unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests)
var config *repo_model.PullRequestsConfig
if err != nil {
// Unit type doesn't exist so we make a new config file with default values
config = &repo_model.PullRequestsConfig{
IgnoreWhitespaceConflicts: false,
AllowMerge: true,
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
AllowFastForwardOnly: true,
AllowManualMerge: true,
AutodetectManualMerge: false,
AllowRebaseUpdate: true,
DefaultDeleteBranchAfterMerge: false,
DefaultMergeStyle: repo_model.MergeStyleMerge,
DefaultAllowMaintainerEdit: false,
}
} else {
config = unit.PullRequestsConfig()
}
if opts.IgnoreWhitespaceConflicts != nil {
config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts
}
if opts.AllowMerge != nil {
config.AllowMerge = *opts.AllowMerge
}
if opts.AllowRebase != nil {
config.AllowRebase = *opts.AllowRebase
}
if opts.AllowRebaseMerge != nil {
config.AllowRebaseMerge = *opts.AllowRebaseMerge
}
if opts.AllowSquash != nil {
config.AllowSquash = *opts.AllowSquash
}
if opts.AllowFastForwardOnly != nil {
config.AllowFastForwardOnly = *opts.AllowFastForwardOnly
}
if opts.AllowManualMerge != nil {
config.AllowManualMerge = *opts.AllowManualMerge
}
if opts.AutodetectManualMerge != nil {
config.AutodetectManualMerge = *opts.AutodetectManualMerge
}
if opts.AllowRebaseUpdate != nil {
config.AllowRebaseUpdate = *opts.AllowRebaseUpdate
}
if opts.DefaultDeleteBranchAfterMerge != nil {
config.DefaultDeleteBranchAfterMerge = *opts.DefaultDeleteBranchAfterMerge
}
if opts.DefaultMergeStyle != nil {
config.DefaultMergeStyle = repo_model.MergeStyle(*opts.DefaultMergeStyle)
}
if opts.DefaultAllowMaintainerEdit != nil {
config.DefaultAllowMaintainerEdit = *opts.DefaultAllowMaintainerEdit
}
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypePullRequests,
Config: config,
})
} else {
if !unit_model.TypePullRequests.UnitGlobalDisabled() {
mustDeletePullRequestUnit := opts.HasPullRequests != nil && !*opts.HasPullRequests
mustInsertPullRequestUnit := opts.HasPullRequests != nil && *opts.HasPullRequests
if mustDeletePullRequestUnit {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
} else {
// We do allow setting individual PR settings through the API,
// so we get the config settings and then set them if those settings were provided in the opts.
unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return err
}
if unit == nil {
// Unit doesn't exist yet but is being enabled, create with defaults
unit = new(repo_model.DefaultPullRequestsUnit(repo.ID))
}
changed := new(false)
config := unit.PullRequestsConfig()
optional.AssignPtrValue(changed, &config.IgnoreWhitespaceConflicts, opts.IgnoreWhitespaceConflicts)
optional.AssignPtrValue(changed, &config.AllowMerge, opts.AllowMerge)
optional.AssignPtrValue(changed, &config.AllowRebase, opts.AllowRebase)
optional.AssignPtrValue(changed, &config.AllowRebaseMerge, opts.AllowRebaseMerge)
optional.AssignPtrValue(changed, &config.AllowSquash, opts.AllowSquash)
optional.AssignPtrValue(changed, &config.AllowFastForwardOnly, opts.AllowFastForwardOnly)
optional.AssignPtrValue(changed, &config.AllowManualMerge, opts.AllowManualMerge)
optional.AssignPtrValue(changed, &config.AutodetectManualMerge, opts.AutodetectManualMerge)
optional.AssignPtrValue(changed, &config.AllowRebaseUpdate, opts.AllowRebaseUpdate)
optional.AssignPtrValue(changed, &config.DefaultDeleteBranchAfterMerge, opts.DefaultDeleteBranchAfterMerge)
optional.AssignPtrValue(changed, &config.DefaultAllowMaintainerEdit, opts.DefaultAllowMaintainerEdit)
optional.AssignPtrString(changed, &config.DefaultMergeStyle, opts.DefaultMergeStyle)
if *changed || mustInsertPullRequestUnit {
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypePullRequests,
Config: config,
})
}
}
}
+2 -2
View File
@@ -206,7 +206,7 @@ func getCommitStatuses(ctx *context.APIContext, commitID string) {
apiStatuses = append(apiStatuses, convert.ToCommitStatus(ctx, status))
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, apiStatuses)
@@ -269,7 +269,7 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) {
ctx.APIErrorInternal(fmt.Errorf("CountLatestCommitStatus[%s, %s]: %w", repo.FullName(), refCommit.CommitID, err))
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
combiStatus := convert.ToCombinedStatus(ctx, refCommit.Commit.ID.String(), statuses,
+10 -7
View File
@@ -107,15 +107,18 @@ func GetAnnotatedTag(ctx *context.APIContext) {
return
}
if tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha); err != nil {
tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
} else {
commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
}
ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit))
return
}
commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit))
}
// GetTag get the tag of a repository
+1 -1
View File
@@ -333,7 +333,7 @@ func ListWikiPages(ctx *context.APIContext) {
pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository))
}
ctx.SetLinkHeader(len(entries), limit)
ctx.SetLinkHeader(int64(len(entries)), limit)
ctx.SetTotalCountHeader(int64(len(entries)))
ctx.JSON(http.StatusOK, pages)
}
+2 -2
View File
@@ -79,7 +79,7 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) {
}
res.Entries[i] = convertedWorkflowJob
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &res)
}
@@ -185,7 +185,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
}
res.Entries[i] = convertedRun
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &res)
}
+1 -1
View File
@@ -36,7 +36,7 @@ func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
users = append(users, convert.ToUser(ctx, b.Blockee, blocker))
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &users)
}
+25 -2
View File
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
@@ -46,11 +47,13 @@ func ListRunners(ctx *context.APIContext, ownerID, repoID int64) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
runners, total, err := db.FindAndCount[actions_model.ActionRunner](ctx, &actions_model.FindRunnerOptions{
opts := &actions_model.FindRunnerOptions{
OwnerID: ownerID,
RepoID: repoID,
ListOptions: utils.GetListOptions(ctx),
})
}
opts.IsDisabled = ctx.FormOptionalBool("disabled")
runners, total, err := db.FindAndCount[actions_model.ActionRunner](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -125,3 +128,23 @@ func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
}
ctx.Status(http.StatusNoContent)
}
func UpdateRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
if !ok {
return
}
form := web.GetForm(ctx).(*api.EditActionRunnerOption)
if form.Disabled == nil {
ctx.APIError(http.StatusUnprocessableEntity, "[Disabled]: Required")
return
}
if err := actions_model.SetRunnerDisabled(ctx, runner, *form.Disabled); err != nil {
ctx.APIErrorInternal(err)
return
}
GetRunner(ctx, ownerID, repoID, runnerID)
}
+7
View File
@@ -46,3 +46,10 @@ type swaggerResponseActionWorkflowList struct {
// in:body
Body api.ActionWorkflowResponse `json:"body"`
}
// RunDetails
// swagger:response RunDetails
type swaggerResponseRunDetails struct {
// in:body
Body api.RunDetails `json:"body"`
}
-15
View File
@@ -1,15 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "code.gitea.io/gitea/modules/structs"
)
// ActivityPub
// swagger:response ActivityPub
type swaggerResponseActivityPub struct {
// in:body
Body api.ActivityPub `json:"body"`
}
+3
View File
@@ -225,6 +225,9 @@ type swaggerParameterBodies struct {
// in:body
UpdateVariableOption api.UpdateVariableOption
// in:body
EditActionRunnerOption api.EditActionRunnerOption
// in:body
LockIssueOption api.LockIssueOption
}
+1 -1
View File
@@ -354,7 +354,7 @@ func ListVariables(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
+2 -2
View File
@@ -31,7 +31,7 @@ func listUserFollowers(ctx *context.APIContext, u *user_model.User) {
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
responseAPIUsers(ctx, users)
}
@@ -97,7 +97,7 @@ func listUserFollowing(ctx *context.APIContext, u *user_model.User) {
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
responseAPIUsers(ctx, users)
}
+1 -1
View File
@@ -94,7 +94,7 @@ func listPublicKeys(ctx *context.APIContext, user *user_model.User) {
}
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, &apiKeys)
}
+4 -4
View File
@@ -37,7 +37,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
apiRepos := make([]*api.Repository, 0, len(repos))
for i := range repos {
permission, err := access_model.GetUserRepoPermission(ctx, repos[i], ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, repos[i], ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -47,7 +47,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
}
}
ctx.SetLinkHeader(int(count), opts.PageSize)
ctx.SetLinkHeader(count, opts.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, &apiRepos)
}
@@ -123,14 +123,14 @@ func ListMyRepos(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
}
results[i] = convert.ToRepo(ctx, repo, permission)
}
ctx.SetLinkHeader(int(count), opts.ListOptions.PageSize)
ctx.SetLinkHeader(count, opts.ListOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, &results)
}
+43 -21
View File
@@ -10,21 +10,6 @@ import (
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// GetRegistrationToken returns the token to register user runners
func GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /user/actions/runners/registration-token user userGetRunnerRegistrationToken
// ---
// summary: Get an user's actions runner registration token
// produces:
// - application/json
// parameters:
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0)
}
// CreateRegistrationToken returns the token to register user runners
func CreateRegistrationToken(ctx *context.APIContext) {
// swagger:operation POST /user/actions/runners/registration-token user userCreateRunnerRegistrationToken
@@ -47,9 +32,15 @@ func ListRunners(ctx *context.APIContext) {
// summary: Get user-level runners
// produces:
// - application/json
// parameters:
// - name: disabled
// in: query
// description: filter by disabled status (true or false)
// type: boolean
// required: false
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
// "$ref": "#/responses/RunnerList"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -57,11 +48,11 @@ func ListRunners(ctx *context.APIContext) {
shared.ListRunners(ctx, ctx.Doer.ID, 0)
}
// GetRunner get an user-level runner
// GetRunner get a user-level runner
func GetRunner(ctx *context.APIContext) {
// swagger:operation GET /user/actions/runners/{runner_id} user getUserRunner
// ---
// summary: Get an user-level runner
// summary: Get a user-level runner
// produces:
// - application/json
// parameters:
@@ -72,7 +63,7 @@ func GetRunner(ctx *context.APIContext) {
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -80,11 +71,11 @@ func GetRunner(ctx *context.APIContext) {
shared.GetRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id"))
}
// DeleteRunner delete an user-level runner
// DeleteRunner delete a user-level runner
func DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /user/actions/runners/{runner_id} user deleteUserRunner
// ---
// summary: Delete an user-level runner
// summary: Delete a user-level runner
// produces:
// - application/json
// parameters:
@@ -102,3 +93,34 @@ func DeleteRunner(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
shared.DeleteRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id"))
}
// UpdateRunner update a user-level runner
func UpdateRunner(ctx *context.APIContext) {
// swagger:operation PATCH /user/actions/runners/{runner_id} user updateUserRunner
// ---
// summary: Update a user-level runner
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditActionRunnerOption"
// responses:
// "200":
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.UpdateRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id"))
}
+3 -3
View File
@@ -31,7 +31,7 @@ func getStarredRepos(ctx *context.APIContext, user *user_model.User, private boo
repos := make([]*api.Repository, len(starredRepos))
for i, starred := range starredRepos {
permission, err := access_model.GetUserRepoPermission(ctx, starred, user)
permission, err := access_model.GetIndividualUserRepoPermission(ctx, starred, user)
if err != nil {
return nil, err
}
@@ -76,7 +76,7 @@ func GetStarredRepos(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(ctx.ContextUser.NumStars, utils.GetListOptions(ctx).PageSize)
ctx.SetLinkHeader(int64(ctx.ContextUser.NumStars), utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(int64(ctx.ContextUser.NumStars))
ctx.JSON(http.StatusOK, &repos)
}
@@ -108,7 +108,7 @@ func GetMyStarredRepos(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
}
ctx.SetLinkHeader(ctx.Doer.NumStars, utils.GetListOptions(ctx).PageSize)
ctx.SetLinkHeader(int64(ctx.Doer.NumStars), utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(int64(ctx.Doer.NumStars))
ctx.JSON(http.StatusOK, &repos)
}
+1 -1
View File
@@ -91,7 +91,7 @@ func Search(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, map[string]any{
+3 -3
View File
@@ -29,7 +29,7 @@ func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private boo
repos := make([]*api.Repository, len(watchedRepos))
for i, watched := range watchedRepos {
permission, err := access_model.GetUserRepoPermission(ctx, watched, user)
permission, err := access_model.GetIndividualUserRepoPermission(ctx, watched, user)
if err != nil {
return nil, 0, err
}
@@ -71,7 +71,7 @@ func GetWatchedRepos(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
}
ctx.SetLinkHeader(int(total), utils.GetListOptions(ctx).PageSize)
ctx.SetLinkHeader(total, utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &repos)
}
@@ -100,7 +100,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) {
if err != nil {
ctx.APIErrorInternal(err)
}
ctx.SetLinkHeader(int(total), utils.GetListOptions(ctx).PageSize)
ctx.SetLinkHeader(total, utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &repos)
}
+6 -1
View File
@@ -43,7 +43,7 @@ func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) {
return
}
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiHooks)
}
@@ -215,6 +215,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
w := &webhook.Webhook{
OwnerID: ownerID,
RepoID: repoID,
Name: strings.TrimSpace(form.Name),
URL: form.Config["url"],
ContentType: webhook.ToHookContentType(form.Config["content_type"]),
Secret: form.Config["secret"],
@@ -392,6 +393,10 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
w.IsActive = *form.Active
}
if form.Name != nil {
w.Name = strings.TrimSpace(*form.Name)
}
if err := webhook.UpdateWebhook(ctx, w); err != nil {
ctx.APIErrorInternal(err)
return false