mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-20 19:08:29 +02:00
Merge branch 'main' of https://github.com/go-gitea/gitea into pr/ChristopherHX/33964-1
This commit is contained in:
commit
7d2cb149cd
@ -80,6 +80,11 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runDumpRepository(ctx *cli.Context) error {
|
func runDumpRepository(ctx *cli.Context) error {
|
||||||
|
setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr)
|
||||||
|
|
||||||
|
setting.DisableLoggerInit()
|
||||||
|
setting.LoadSettings() // cannot access skip_tls_verify settings otherwise
|
||||||
|
|
||||||
stdCtx, cancel := installSignals()
|
stdCtx, cancel := installSignals()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
6
flake.lock
generated
6
flake.lock
generated
@ -20,11 +20,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1739214665,
|
"lastModified": 1747179050,
|
||||||
"narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=",
|
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a",
|
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
2
go.mod
2
go.mod
@ -317,7 +317,7 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
|
|||||||
|
|
||||||
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
|
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
|
||||||
|
|
||||||
replace github.com/nektos/act => gitea.com/gitea/act v0.261.4
|
replace github.com/nektos/act => gitea.com/gitea/act v0.261.6
|
||||||
|
|
||||||
// TODO: the only difference is in `PutObject`: the fork doesn't use `NewVerifyingReader(r, sha256.New(), oid, expectedSize)`, need to figure out why
|
// TODO: the only difference is in `PutObject`: the fork doesn't use `NewVerifyingReader(r, sha256.New(), oid, expectedSize)`, need to figure out why
|
||||||
replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0
|
replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0
|
||||||
|
4
go.sum
4
go.sum
@ -14,8 +14,8 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
|||||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
gitea.com/gitea/act v0.261.4 h1:Tf9eLlvsYFtKcpuxlMvf9yT3g4Hshb2Beqw6C1STuH8=
|
gitea.com/gitea/act v0.261.6 h1:CjZwKOyejonNFDmsXOw3wGm5Vet573hHM6VMLsxtvPY=
|
||||||
gitea.com/gitea/act v0.261.4/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
gitea.com/gitea/act v0.261.6/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
||||||
gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40=
|
gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40=
|
||||||
gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
|
gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
|
||||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
|
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
|
||||||
|
@ -5,6 +5,7 @@ package actions
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -298,6 +299,23 @@ func DeleteRunner(ctx context.Context, id int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteEphemeralRunner deletes a ephemeral runner by given ID.
|
||||||
|
func DeleteEphemeralRunner(ctx context.Context, id int64) error {
|
||||||
|
runner, err := GetRunnerByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !runner.Ephemeral {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.DeleteByID[ActionRunner](ctx, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// CreateRunner creates new runner.
|
// CreateRunner creates new runner.
|
||||||
func CreateRunner(ctx context.Context, t *ActionRunner) error {
|
func CreateRunner(ctx context.Context, t *ActionRunner) error {
|
||||||
if t.OwnerID != 0 && t.RepoID != 0 {
|
if t.OwnerID != 0 && t.RepoID != 0 {
|
||||||
|
@ -336,6 +336,11 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
|
|||||||
sess.Cols(cols...)
|
sess.Cols(cols...)
|
||||||
}
|
}
|
||||||
_, err := sess.Update(task)
|
_, err := sess.Update(task)
|
||||||
|
|
||||||
|
// Automatically delete the ephemeral runner if the task is done
|
||||||
|
if err == nil && task.Status.IsDone() && util.SliceContainsString(cols, "status") {
|
||||||
|
return DeleteEphemeralRunner(ctx, task.RunnerID)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,3 +82,22 @@ func calculateDuration(started, stopped timeutil.TimeStamp, status Status) time.
|
|||||||
}
|
}
|
||||||
return timeSince(s).Truncate(time.Second)
|
return timeSince(s).Truncate(time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// best effort function to convert an action schedule to action run, to be used in GenerateGiteaContext
|
||||||
|
func (s *ActionSchedule) ToActionRun() *ActionRun {
|
||||||
|
return &ActionRun{
|
||||||
|
Title: s.Title,
|
||||||
|
RepoID: s.RepoID,
|
||||||
|
Repo: s.Repo,
|
||||||
|
OwnerID: s.OwnerID,
|
||||||
|
WorkflowID: s.WorkflowID,
|
||||||
|
TriggerUserID: s.TriggerUserID,
|
||||||
|
TriggerUser: s.TriggerUser,
|
||||||
|
Ref: s.Ref,
|
||||||
|
CommitSHA: s.CommitSHA,
|
||||||
|
Event: s.Event,
|
||||||
|
EventPayload: s.EventPayload,
|
||||||
|
Created: s.Created,
|
||||||
|
Updated: s.Updated,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -38,3 +38,14 @@
|
|||||||
repo_id: 0
|
repo_id: 0
|
||||||
description: "This runner is going to be deleted"
|
description: "This runner is going to be deleted"
|
||||||
agent_labels: '["runner_to_be_deleted","linux"]'
|
agent_labels: '["runner_to_be_deleted","linux"]'
|
||||||
|
-
|
||||||
|
id: 34350
|
||||||
|
name: runner_to_be_deleted-org-ephemeral
|
||||||
|
uuid: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20
|
||||||
|
token_hash: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20
|
||||||
|
ephemeral: true
|
||||||
|
version: "1.0.0"
|
||||||
|
owner_id: 3
|
||||||
|
repo_id: 0
|
||||||
|
description: "This runner is going to be deleted"
|
||||||
|
agent_labels: '["runner_to_be_deleted","linux"]'
|
||||||
|
@ -117,6 +117,26 @@
|
|||||||
log_length: 707
|
log_length: 707
|
||||||
log_size: 90179
|
log_size: 90179
|
||||||
log_expired: 0
|
log_expired: 0
|
||||||
|
-
|
||||||
|
id: 52
|
||||||
|
job_id: 196
|
||||||
|
attempt: 1
|
||||||
|
runner_id: 34350
|
||||||
|
status: 6 # running
|
||||||
|
started: 1683636528
|
||||||
|
stopped: 1683636626
|
||||||
|
repo_id: 4
|
||||||
|
owner_id: 1
|
||||||
|
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||||
|
is_fork_pull_request: 0
|
||||||
|
token_hash: f8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222
|
||||||
|
token_salt: ffffffffff
|
||||||
|
token_last_eight: ffffffff
|
||||||
|
log_filename: artifact-test2/2f/47.log
|
||||||
|
log_in_storage: 1
|
||||||
|
log_length: 707
|
||||||
|
log_size: 90179
|
||||||
|
log_expired: 0
|
||||||
-
|
-
|
||||||
id: 53
|
id: 53
|
||||||
job_id: 198
|
job_id: 198
|
||||||
|
@ -14,5 +14,8 @@ func AddIndexToActionTaskStoppedLogExpired(x *xorm.Engine) error {
|
|||||||
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
|
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
|
||||||
LogExpired bool `xorm:"index(stopped_log_expired)"`
|
LogExpired bool `xorm:"index(stopped_log_expired)"`
|
||||||
}
|
}
|
||||||
return x.Sync(new(ActionTask))
|
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||||
|
IgnoreDropIndices: true,
|
||||||
|
}, new(ActionTask))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
51
models/migrations/v1_23/v302_test.go
Normal file
51
models/migrations/v1_23/v302_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_23 //nolint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/migrations/base"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_AddIndexToActionTaskStoppedLogExpired(t *testing.T) {
|
||||||
|
type ActionTask struct {
|
||||||
|
ID int64
|
||||||
|
JobID int64
|
||||||
|
Attempt int64
|
||||||
|
RunnerID int64 `xorm:"index"`
|
||||||
|
Status int `xorm:"index"`
|
||||||
|
Started timeutil.TimeStamp `xorm:"index"`
|
||||||
|
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
|
||||||
|
|
||||||
|
RepoID int64 `xorm:"index"`
|
||||||
|
OwnerID int64 `xorm:"index"`
|
||||||
|
CommitSHA string `xorm:"index"`
|
||||||
|
IsForkPullRequest bool
|
||||||
|
|
||||||
|
Token string `xorm:"-"`
|
||||||
|
TokenHash string `xorm:"UNIQUE"` // sha256 of token
|
||||||
|
TokenSalt string
|
||||||
|
TokenLastEight string `xorm:"index token_last_eight"`
|
||||||
|
|
||||||
|
LogFilename string // file name of log
|
||||||
|
LogInStorage bool // read log from database or from storage
|
||||||
|
LogLength int64 // lines count
|
||||||
|
LogSize int64 // blob size
|
||||||
|
LogIndexes []int64 `xorm:"LONGBLOB"` // line number to offset
|
||||||
|
LogExpired bool `xorm:"index(stopped_log_expired)"` // files that are too old will be deleted
|
||||||
|
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
|
Updated timeutil.TimeStamp `xorm:"updated index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare and load the testing database
|
||||||
|
x, deferable := base.PrepareTestEnv(t, 0, new(ActionTask))
|
||||||
|
defer deferable()
|
||||||
|
|
||||||
|
assert.NoError(t, AddIndexToActionTaskStoppedLogExpired(x))
|
||||||
|
}
|
@ -9,5 +9,8 @@ func AddIndexForReleaseSha1(x *xorm.Engine) error {
|
|||||||
type Release struct {
|
type Release struct {
|
||||||
Sha1 string `xorm:"INDEX VARCHAR(64)"`
|
Sha1 string `xorm:"INDEX VARCHAR(64)"`
|
||||||
}
|
}
|
||||||
return x.Sync(new(Release))
|
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||||
|
IgnoreDropIndices: true,
|
||||||
|
}, new(Release))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
40
models/migrations/v1_23/v304_test.go
Normal file
40
models/migrations/v1_23/v304_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_23 //nolint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/migrations/base"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_AddIndexForReleaseSha1(t *testing.T) {
|
||||||
|
type Release struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"INDEX UNIQUE(n)"`
|
||||||
|
PublisherID int64 `xorm:"INDEX"`
|
||||||
|
TagName string `xorm:"INDEX UNIQUE(n)"`
|
||||||
|
OriginalAuthor string
|
||||||
|
OriginalAuthorID int64 `xorm:"index"`
|
||||||
|
LowerTagName string
|
||||||
|
Target string
|
||||||
|
Title string
|
||||||
|
Sha1 string `xorm:"VARCHAR(64)"`
|
||||||
|
NumCommits int64
|
||||||
|
Note string `xorm:"TEXT"`
|
||||||
|
IsDraft bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
IsPrerelease bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare and load the testing database
|
||||||
|
x, deferable := base.PrepareTestEnv(t, 0, new(Release))
|
||||||
|
defer deferable()
|
||||||
|
|
||||||
|
assert.NoError(t, AddIndexForReleaseSha1(x))
|
||||||
|
}
|
@ -166,6 +166,8 @@ type CommitsCountOptions struct {
|
|||||||
Not string
|
Not string
|
||||||
Revision []string
|
Revision []string
|
||||||
RelPath []string
|
RelPath []string
|
||||||
|
Since string
|
||||||
|
Until string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitsCount returns number of total commits of until given revision.
|
// CommitsCount returns number of total commits of until given revision.
|
||||||
@ -199,8 +201,8 @@ func (c *Commit) CommitsCount() (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
|
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
|
||||||
func (c *Commit) CommitsByRange(page, pageSize int, not string) ([]*Commit, error) {
|
func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) {
|
||||||
return c.repo.commitsByRange(c.ID, page, pageSize, not)
|
return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitsBefore returns all the commits before current revision
|
// CommitsBefore returns all the commits before current revision
|
||||||
|
@ -89,7 +89,8 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) {
|
|||||||
return commits[0], nil
|
return commits[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *Repository) commitsByRange(id ObjectID, page, pageSize int, not string) ([]*Commit, error) {
|
// commitsByRangeWithTime returns the specific page commits before current revision, with not, since, until support
|
||||||
|
func (repo *Repository) commitsByRangeWithTime(id ObjectID, page, pageSize int, not, since, until string) ([]*Commit, error) {
|
||||||
cmd := NewCommand("log").
|
cmd := NewCommand("log").
|
||||||
AddOptionFormat("--skip=%d", (page-1)*pageSize).
|
AddOptionFormat("--skip=%d", (page-1)*pageSize).
|
||||||
AddOptionFormat("--max-count=%d", pageSize).
|
AddOptionFormat("--max-count=%d", pageSize).
|
||||||
@ -99,6 +100,12 @@ func (repo *Repository) commitsByRange(id ObjectID, page, pageSize int, not stri
|
|||||||
if not != "" {
|
if not != "" {
|
||||||
cmd.AddOptionValues("--not", not)
|
cmd.AddOptionValues("--not", not)
|
||||||
}
|
}
|
||||||
|
if since != "" {
|
||||||
|
cmd.AddOptionFormat("--since=%s", since)
|
||||||
|
}
|
||||||
|
if until != "" {
|
||||||
|
cmd.AddOptionFormat("--until=%s", until)
|
||||||
|
}
|
||||||
|
|
||||||
stdout, _, err := cmd.RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path})
|
stdout, _, err := cmd.RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -212,6 +219,8 @@ type CommitsByFileAndRangeOptions struct {
|
|||||||
File string
|
File string
|
||||||
Not string
|
Not string
|
||||||
Page int
|
Page int
|
||||||
|
Since string
|
||||||
|
Until string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitsByFileAndRange return the commits according revision file and the page
|
// CommitsByFileAndRange return the commits according revision file and the page
|
||||||
@ -231,6 +240,12 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions)
|
|||||||
if opts.Not != "" {
|
if opts.Not != "" {
|
||||||
gitCmd.AddOptionValues("--not", opts.Not)
|
gitCmd.AddOptionValues("--not", opts.Not)
|
||||||
}
|
}
|
||||||
|
if opts.Since != "" {
|
||||||
|
gitCmd.AddOptionFormat("--since=%s", opts.Since)
|
||||||
|
}
|
||||||
|
if opts.Until != "" {
|
||||||
|
gitCmd.AddOptionFormat("--until=%s", opts.Until)
|
||||||
|
}
|
||||||
|
|
||||||
gitCmd.AddDashesAndList(opts.File)
|
gitCmd.AddDashesAndList(opts.File)
|
||||||
err := gitCmd.Run(repo.Ctx, &RunOpts{
|
err := gitCmd.Run(repo.Ctx, &RunOpts{
|
||||||
|
@ -40,7 +40,9 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
|
|||||||
|
|
||||||
since := fromTime.Format(time.RFC3339)
|
since := fromTime.Format(time.RFC3339)
|
||||||
|
|
||||||
stdout, _, runErr := NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso").AddOptionFormat("--since='%s'", since).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
|
stdout, _, runErr := NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso").
|
||||||
|
AddOptionFormat("--since=%s", since).
|
||||||
|
RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
|
||||||
if runErr != nil {
|
if runErr != nil {
|
||||||
return nil, runErr
|
return nil, runErr
|
||||||
}
|
}
|
||||||
@ -60,7 +62,8 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
|
|||||||
_ = stdoutWriter.Close()
|
_ = stdoutWriter.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
gitCmd := NewCommand("log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso").AddOptionFormat("--since='%s'", since)
|
gitCmd := NewCommand("log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso").
|
||||||
|
AddOptionFormat("--since=%s", since)
|
||||||
if len(branch) == 0 {
|
if len(branch) == 0 {
|
||||||
gitCmd.AddArguments("--branches=*")
|
gitCmd.AddArguments("--branches=*")
|
||||||
} else {
|
} else {
|
||||||
|
@ -200,5 +200,3 @@ func TestListToPushCommits(t *testing.T) {
|
|||||||
assert.Equal(t, now, pushCommits.Commits[1].Timestamp)
|
assert.Equal(t, now, pushCommits.Commits[1].Timestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO TestPushUpdate
|
|
||||||
|
@ -101,6 +101,8 @@ type Repository struct {
|
|||||||
AllowSquash bool `json:"allow_squash_merge"`
|
AllowSquash bool `json:"allow_squash_merge"`
|
||||||
AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"`
|
AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"`
|
||||||
AllowRebaseUpdate bool `json:"allow_rebase_update"`
|
AllowRebaseUpdate bool `json:"allow_rebase_update"`
|
||||||
|
AllowManualMerge bool `json:"allow_manual_merge"`
|
||||||
|
AutodetectManualMerge bool `json:"autodetect_manual_merge"`
|
||||||
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
|
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
|
||||||
DefaultMergeStyle string `json:"default_merge_style"`
|
DefaultMergeStyle string `json:"default_merge_style"`
|
||||||
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"`
|
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"`
|
||||||
|
@ -66,6 +66,7 @@ Piotr Orzechowski <piotr AT orzechowski DOT tech>
|
|||||||
Richard Bukovansky <richard DOT bukovansky AT gmail DOT com>
|
Richard Bukovansky <richard DOT bukovansky AT gmail DOT com>
|
||||||
Robert Nuske <robert DOT nuske AT web DOT de>
|
Robert Nuske <robert DOT nuske AT web DOT de>
|
||||||
Robin Hübner <profan AT prfn DOT se>
|
Robin Hübner <profan AT prfn DOT se>
|
||||||
|
Ryo Hanafusa <ryo7gumi AT gmail DOT com>
|
||||||
SeongJae Park <sj38 DOT park AT gmail DOT com>
|
SeongJae Park <sj38 DOT park AT gmail DOT com>
|
||||||
Thiago Avelino <thiago AT avelino DOT xxx>
|
Thiago Avelino <thiago AT avelino DOT xxx>
|
||||||
Thomas Fanninger <gogs DOT thomas AT fanninger DOT at>
|
Thomas Fanninger <gogs DOT thomas AT fanninger DOT at>
|
||||||
|
@ -1228,6 +1228,7 @@ migrate.migrating_issues = Migrating Issues
|
|||||||
migrate.migrating_pulls = Migrating Pull Requests
|
migrate.migrating_pulls = Migrating Pull Requests
|
||||||
migrate.cancel_migrating_title = Cancel Migration
|
migrate.cancel_migrating_title = Cancel Migration
|
||||||
migrate.cancel_migrating_confirm = Do you want to cancel this migration?
|
migrate.cancel_migrating_confirm = Do you want to cancel this migration?
|
||||||
|
migrating_status = Migrating status
|
||||||
|
|
||||||
mirror_from = mirror of
|
mirror_from = mirror of
|
||||||
forked_from = forked from
|
forked_from = forked from
|
||||||
|
@ -3728,7 +3728,11 @@ creation.name_placeholder=Caractères alphanumériques ou tirets bas uniquement,
|
|||||||
creation.value_placeholder=Entrez n’importe quoi. Les blancs cernant seront taillés.
|
creation.value_placeholder=Entrez n’importe quoi. Les blancs cernant seront taillés.
|
||||||
creation.description_placeholder=Décrire brièvement votre dépôt (optionnel).
|
creation.description_placeholder=Décrire brièvement votre dépôt (optionnel).
|
||||||
|
|
||||||
|
save_success=Le secret « %s » a été enregistré.
|
||||||
|
save_failed=Impossible d’enregistrer le secret.
|
||||||
|
|
||||||
|
add_secret=Ajouter un secret
|
||||||
|
edit_secret=Modifier le secret
|
||||||
deletion=Supprimer le secret
|
deletion=Supprimer le secret
|
||||||
deletion.description=La suppression d'un secret est permanente et irréversible. Continuer ?
|
deletion.description=La suppression d'un secret est permanente et irréversible. Continuer ?
|
||||||
deletion.success=Le secret a été supprimé.
|
deletion.success=Le secret a été supprimé.
|
||||||
@ -3806,6 +3810,9 @@ runs.no_workflows.documentation=Pour plus d’informations sur les actions Gitea
|
|||||||
runs.no_runs=Le flux de travail n'a pas encore d'exécution.
|
runs.no_runs=Le flux de travail n'a pas encore d'exécution.
|
||||||
runs.empty_commit_message=(message de révision vide)
|
runs.empty_commit_message=(message de révision vide)
|
||||||
runs.expire_log_message=Les journaux ont été supprimés car ils étaient trop anciens.
|
runs.expire_log_message=Les journaux ont été supprimés car ils étaient trop anciens.
|
||||||
|
runs.delete=Supprimer cette exécution
|
||||||
|
runs.delete.description=Êtes-vous sûr de vouloir supprimer définitivement cette exécution ? Cette action ne peut pas être annulée.
|
||||||
|
runs.not_done=Cette exécution du flux de travail n’est pas terminée.
|
||||||
|
|
||||||
workflow.disable=Désactiver le flux de travail
|
workflow.disable=Désactiver le flux de travail
|
||||||
workflow.disable_success=Le flux de travail « %s » a bien été désactivé.
|
workflow.disable_success=Le flux de travail « %s » a bien été désactivé.
|
||||||
|
@ -3810,6 +3810,9 @@ runs.no_workflows.documentation=Le haghaidh tuilleadh eolais ar Gitea Actions, f
|
|||||||
runs.no_runs=Níl aon rith ag an sreabhadh oibre fós.
|
runs.no_runs=Níl aon rith ag an sreabhadh oibre fós.
|
||||||
runs.empty_commit_message=(teachtaireacht tiomantas folamh)
|
runs.empty_commit_message=(teachtaireacht tiomantas folamh)
|
||||||
runs.expire_log_message=Glanadh logaí toisc go raibh siad ró-sean.
|
runs.expire_log_message=Glanadh logaí toisc go raibh siad ró-sean.
|
||||||
|
runs.delete=Scrios rith sreabha oibre
|
||||||
|
runs.delete.description=An bhfuil tú cinnte gur mian leat an rith sreabha oibre seo a scriosadh go buan? Ní féidir an gníomh seo a chealú.
|
||||||
|
runs.not_done=Níl an rith sreabha oibre seo críochnaithe.
|
||||||
|
|
||||||
workflow.disable=Díchumasaigh sreabhadh oibre
|
workflow.disable=Díchumasaigh sreabhadh oibre
|
||||||
workflow.disable_success=D'éirigh le sreabhadh oibre '%s' a dhíchumasú.
|
workflow.disable_success=D'éirigh le sreabhadh oibre '%s' a dhíchumasú.
|
||||||
|
@ -450,6 +450,7 @@ use_scratch_code=スクラッチコードを使う
|
|||||||
twofa_scratch_used=あなたはスクラッチコードを使用しました。 2要素認証の設定ページにリダイレクトしましたので、デバイスの登録を解除するか、新しいスクラッチコードを生成しましょう。
|
twofa_scratch_used=あなたはスクラッチコードを使用しました。 2要素認証の設定ページにリダイレクトしましたので、デバイスの登録を解除するか、新しいスクラッチコードを生成しましょう。
|
||||||
twofa_passcode_incorrect=パスコードが正しくありません。デバイスを紛失した場合は、スクラッチコードを使ってサインインしてください。
|
twofa_passcode_incorrect=パスコードが正しくありません。デバイスを紛失した場合は、スクラッチコードを使ってサインインしてください。
|
||||||
twofa_scratch_token_incorrect=スクラッチコードが正しくありません。
|
twofa_scratch_token_incorrect=スクラッチコードが正しくありません。
|
||||||
|
twofa_required=リポジトリにアクセスするには2段階認証を設定するか、再度ログインしてください。
|
||||||
login_userpass=サインイン
|
login_userpass=サインイン
|
||||||
login_openid=OpenID
|
login_openid=OpenID
|
||||||
oauth_signup_tab=新規アカウント登録
|
oauth_signup_tab=新規アカウント登録
|
||||||
@ -1878,6 +1879,7 @@ pulls.add_prefix=先頭に <strong>%s</strong> を追加
|
|||||||
pulls.remove_prefix=先頭の <strong>%s</strong> を除去
|
pulls.remove_prefix=先頭の <strong>%s</strong> を除去
|
||||||
pulls.data_broken=このプルリクエストは、フォークの情報が見つからないため壊れています。
|
pulls.data_broken=このプルリクエストは、フォークの情報が見つからないため壊れています。
|
||||||
pulls.files_conflicted=このプルリクエストは、ターゲットブランチと競合する変更を含んでいます。
|
pulls.files_conflicted=このプルリクエストは、ターゲットブランチと競合する変更を含んでいます。
|
||||||
|
pulls.is_checking=マージの競合を確認しています...
|
||||||
pulls.is_ancestor=このブランチは既にターゲットブランチに含まれています。マージするものはありません。
|
pulls.is_ancestor=このブランチは既にターゲットブランチに含まれています。マージするものはありません。
|
||||||
pulls.is_empty=このブランチの変更は既にターゲットブランチにあります。これは空のコミットになります。
|
pulls.is_empty=このブランチの変更は既にターゲットブランチにあります。これは空のコミットになります。
|
||||||
pulls.required_status_check_failed=いくつかの必要なステータスチェックが成功していません。
|
pulls.required_status_check_failed=いくつかの必要なステータスチェックが成功していません。
|
||||||
@ -3725,7 +3727,11 @@ creation.name_placeholder=大文字小文字の区別なし、英数字とアン
|
|||||||
creation.value_placeholder=内容を入力してください。前後の空白は除去されます。
|
creation.value_placeholder=内容を入力してください。前後の空白は除去されます。
|
||||||
creation.description_placeholder=簡単な説明を入力してください。 (オプション)
|
creation.description_placeholder=簡単な説明を入力してください。 (オプション)
|
||||||
|
|
||||||
|
save_success=シークレット "%s" を保存しました。
|
||||||
|
save_failed=シークレットの保存に失敗しました。
|
||||||
|
|
||||||
|
add_secret=シークレットを追加
|
||||||
|
edit_secret=シークレットを編集
|
||||||
deletion=シークレットの削除
|
deletion=シークレットの削除
|
||||||
deletion.description=シークレットの削除は恒久的で元に戻すことはできません。 続行しますか?
|
deletion.description=シークレットの削除は恒久的で元に戻すことはできません。 続行しますか?
|
||||||
deletion.success=シークレットを削除しました。
|
deletion.success=シークレットを削除しました。
|
||||||
@ -3842,6 +3848,8 @@ deleted.display_name=削除されたプロジェクト
|
|||||||
type-1.display_name=個人プロジェクト
|
type-1.display_name=個人プロジェクト
|
||||||
type-2.display_name=リポジトリ プロジェクト
|
type-2.display_name=リポジトリ プロジェクト
|
||||||
type-3.display_name=組織プロジェクト
|
type-3.display_name=組織プロジェクト
|
||||||
|
enter_fullscreen=フルスクリーン
|
||||||
|
exit_fullscreen=フルスクリーンを終了
|
||||||
|
|
||||||
[git.filemode]
|
[git.filemode]
|
||||||
changed_filemode=%[1]s → %[2]s
|
changed_filemode=%[1]s → %[2]s
|
||||||
|
@ -3810,6 +3810,9 @@ runs.no_workflows.documentation=Para mais informação sobre o Gitea Actions vej
|
|||||||
runs.no_runs=A sequência de trabalho ainda não foi executada.
|
runs.no_runs=A sequência de trabalho ainda não foi executada.
|
||||||
runs.empty_commit_message=(mensagem de cometimento vazia)
|
runs.empty_commit_message=(mensagem de cometimento vazia)
|
||||||
runs.expire_log_message=Os registros foram removidos porque eram muito antigos.
|
runs.expire_log_message=Os registros foram removidos porque eram muito antigos.
|
||||||
|
runs.delete=Eliminar execução da sequência de trabalho
|
||||||
|
runs.delete.description=Tem a certeza que pretende eliminar permanentemente a execução desta sequência de trabalho? Esta operação não poderá ser desfeita.
|
||||||
|
runs.not_done=A execução desta sequência de trabalho ainda não terminou.
|
||||||
|
|
||||||
workflow.disable=Desabilitar sequência de trabalho
|
workflow.disable=Desabilitar sequência de trabalho
|
||||||
workflow.disable_success=A sequência de trabalho '%s' foi desabilitada com sucesso.
|
workflow.disable_success=A sequência de trabalho '%s' foi desabilitada com sucesso.
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
@ -116,6 +117,16 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||||||
// in: query
|
// in: query
|
||||||
// description: filepath of a file/dir
|
// description: filepath of a file/dir
|
||||||
// type: string
|
// type: string
|
||||||
|
// - name: since
|
||||||
|
// in: query
|
||||||
|
// description: Only commits after this date will be returned (ISO 8601 format)
|
||||||
|
// type: string
|
||||||
|
// format: date-time
|
||||||
|
// - name: until
|
||||||
|
// in: query
|
||||||
|
// description: Only commits before this date will be returned (ISO 8601 format)
|
||||||
|
// type: string
|
||||||
|
// format: date-time
|
||||||
// - name: stat
|
// - name: stat
|
||||||
// in: query
|
// in: query
|
||||||
// description: include diff stats for every commit (disable for speedup, default 'true')
|
// description: include diff stats for every commit (disable for speedup, default 'true')
|
||||||
@ -148,6 +159,23 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||||||
// "409":
|
// "409":
|
||||||
// "$ref": "#/responses/EmptyRepository"
|
// "$ref": "#/responses/EmptyRepository"
|
||||||
|
|
||||||
|
since := ctx.FormString("since")
|
||||||
|
until := ctx.FormString("until")
|
||||||
|
|
||||||
|
// Validate since/until as ISO 8601 (RFC3339)
|
||||||
|
if since != "" {
|
||||||
|
if _, err := time.Parse(time.RFC3339, since); err != nil {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, "invalid 'since' format, expected ISO 8601 (RFC3339)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if until != "" {
|
||||||
|
if _, err := time.Parse(time.RFC3339, until); err != nil {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, "invalid 'until' format, expected ISO 8601 (RFC3339)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.Repo.Repository.IsEmpty {
|
if ctx.Repo.Repository.IsEmpty {
|
||||||
ctx.JSON(http.StatusConflict, api.APIError{
|
ctx.JSON(http.StatusConflict, api.APIError{
|
||||||
Message: "Git Repository is empty.",
|
Message: "Git Repository is empty.",
|
||||||
@ -198,6 +226,8 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||||||
RepoPath: ctx.Repo.GitRepo.Path,
|
RepoPath: ctx.Repo.GitRepo.Path,
|
||||||
Not: not,
|
Not: not,
|
||||||
Revision: []string{baseCommit.ID.String()},
|
Revision: []string{baseCommit.ID.String()},
|
||||||
|
Since: since,
|
||||||
|
Until: until,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
@ -205,7 +235,7 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Query commits
|
// Query commits
|
||||||
commits, err = baseCommit.CommitsByRange(listOptions.Page, listOptions.PageSize, not)
|
commits, err = baseCommit.CommitsByRange(listOptions.Page, listOptions.PageSize, not, since, until)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
@ -221,6 +251,8 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||||||
Not: not,
|
Not: not,
|
||||||
Revision: []string{sha},
|
Revision: []string{sha},
|
||||||
RelPath: []string{path},
|
RelPath: []string{path},
|
||||||
|
Since: since,
|
||||||
|
Until: until,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -237,6 +269,8 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||||||
File: path,
|
File: path,
|
||||||
Not: not,
|
Not: not,
|
||||||
Page: listOptions.Page,
|
Page: listOptions.Page,
|
||||||
|
Since: since,
|
||||||
|
Until: until,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
|
@ -67,6 +67,28 @@ func ListRunners(ctx *context.APIContext, ownerID, repoID int64) {
|
|||||||
ctx.JSON(http.StatusOK, &res)
|
ctx.JSON(http.StatusOK, &res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRunnerByID(ctx *context.APIContext, ownerID, repoID, runnerID int64) (*actions_model.ActionRunner, bool) {
|
||||||
|
if ownerID != 0 && repoID != 0 {
|
||||||
|
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
|
||||||
|
}
|
||||||
|
|
||||||
|
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIErrorNotFound("Runner not found")
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !runner.EditableInContext(ownerID, repoID) {
|
||||||
|
ctx.APIErrorNotFound("No permission to access this runner")
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return runner, true
|
||||||
|
}
|
||||||
|
|
||||||
// GetRunner get the runner for api route validated ownerID and repoID
|
// GetRunner get the runner for api route validated ownerID and repoID
|
||||||
// ownerID == 0 and repoID == 0 means any runner including global runners
|
// ownerID == 0 and repoID == 0 means any runner including global runners
|
||||||
// ownerID == 0 and repoID != 0 means any runner for the given repo
|
// ownerID == 0 and repoID != 0 means any runner for the given repo
|
||||||
@ -77,13 +99,8 @@ func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
|
|||||||
if ownerID != 0 && repoID != 0 {
|
if ownerID != 0 && repoID != 0 {
|
||||||
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
|
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
|
||||||
}
|
}
|
||||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
|
||||||
if err != nil {
|
if !ok {
|
||||||
ctx.APIErrorNotFound(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !runner.EditableInContext(ownerID, repoID) {
|
|
||||||
ctx.APIErrorNotFound("No permission to get this runner")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, convert.ToActionRunner(ctx, runner))
|
ctx.JSON(http.StatusOK, convert.ToActionRunner(ctx, runner))
|
||||||
@ -96,20 +113,12 @@ func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
|
|||||||
// ownerID != 0 and repoID != 0 undefined behavior
|
// ownerID != 0 and repoID != 0 undefined behavior
|
||||||
// Access rights are checked at the API route level
|
// Access rights are checked at the API route level
|
||||||
func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
|
func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
|
||||||
if ownerID != 0 && repoID != 0 {
|
runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
|
||||||
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
|
if !ok {
|
||||||
}
|
|
||||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !runner.EditableInContext(ownerID, repoID) {
|
|
||||||
ctx.APIErrorNotFound("No permission to delete this runner")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = actions_model.DeleteRunner(ctx, runner.ID)
|
err := actions_model.DeleteRunner(ctx, runner.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/modules/validation"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
webhook_service "code.gitea.io/gitea/services/webhook"
|
webhook_service "code.gitea.io/gitea/services/webhook"
|
||||||
@ -92,6 +93,10 @@ func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption)
|
|||||||
ctx.APIError(http.StatusUnprocessableEntity, "Invalid content type")
|
ctx.APIError(http.StatusUnprocessableEntity, "Invalid content type")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if !validation.IsValidURL(form.Config["url"]) {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, "Invalid url")
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,6 +159,42 @@ func pullHook(events []string, event string) bool {
|
|||||||
return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true)
|
return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateHookEvents(events []string) webhook_module.HookEvents {
|
||||||
|
if len(events) == 0 {
|
||||||
|
events = []string{"push"}
|
||||||
|
}
|
||||||
|
hookEvents := make(webhook_module.HookEvents)
|
||||||
|
hookEvents[webhook_module.HookEventCreate] = util.SliceContainsString(events, string(webhook_module.HookEventCreate), true)
|
||||||
|
hookEvents[webhook_module.HookEventPush] = util.SliceContainsString(events, string(webhook_module.HookEventPush), true)
|
||||||
|
hookEvents[webhook_module.HookEventDelete] = util.SliceContainsString(events, string(webhook_module.HookEventDelete), true)
|
||||||
|
hookEvents[webhook_module.HookEventFork] = util.SliceContainsString(events, string(webhook_module.HookEventFork), true)
|
||||||
|
hookEvents[webhook_module.HookEventRepository] = util.SliceContainsString(events, string(webhook_module.HookEventRepository), true)
|
||||||
|
hookEvents[webhook_module.HookEventWiki] = util.SliceContainsString(events, string(webhook_module.HookEventWiki), true)
|
||||||
|
hookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(events, string(webhook_module.HookEventRelease), true)
|
||||||
|
hookEvents[webhook_module.HookEventPackage] = util.SliceContainsString(events, string(webhook_module.HookEventPackage), true)
|
||||||
|
hookEvents[webhook_module.HookEventStatus] = util.SliceContainsString(events, string(webhook_module.HookEventStatus), true)
|
||||||
|
hookEvents[webhook_module.HookEventWorkflowRun] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowRun), true)
|
||||||
|
hookEvents[webhook_module.HookEventWorkflowJob] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowJob), true)
|
||||||
|
|
||||||
|
// Issues
|
||||||
|
hookEvents[webhook_module.HookEventIssues] = issuesHook(events, "issues_only")
|
||||||
|
hookEvents[webhook_module.HookEventIssueAssign] = issuesHook(events, string(webhook_module.HookEventIssueAssign))
|
||||||
|
hookEvents[webhook_module.HookEventIssueLabel] = issuesHook(events, string(webhook_module.HookEventIssueLabel))
|
||||||
|
hookEvents[webhook_module.HookEventIssueMilestone] = issuesHook(events, string(webhook_module.HookEventIssueMilestone))
|
||||||
|
hookEvents[webhook_module.HookEventIssueComment] = issuesHook(events, string(webhook_module.HookEventIssueComment))
|
||||||
|
|
||||||
|
// Pull requests
|
||||||
|
hookEvents[webhook_module.HookEventPullRequest] = pullHook(events, "pull_request_only")
|
||||||
|
hookEvents[webhook_module.HookEventPullRequestAssign] = pullHook(events, string(webhook_module.HookEventPullRequestAssign))
|
||||||
|
hookEvents[webhook_module.HookEventPullRequestLabel] = pullHook(events, string(webhook_module.HookEventPullRequestLabel))
|
||||||
|
hookEvents[webhook_module.HookEventPullRequestMilestone] = pullHook(events, string(webhook_module.HookEventPullRequestMilestone))
|
||||||
|
hookEvents[webhook_module.HookEventPullRequestComment] = pullHook(events, string(webhook_module.HookEventPullRequestComment))
|
||||||
|
hookEvents[webhook_module.HookEventPullRequestReview] = pullHook(events, "pull_request_review")
|
||||||
|
hookEvents[webhook_module.HookEventPullRequestReviewRequest] = pullHook(events, string(webhook_module.HookEventPullRequestReviewRequest))
|
||||||
|
hookEvents[webhook_module.HookEventPullRequestSync] = pullHook(events, string(webhook_module.HookEventPullRequestSync))
|
||||||
|
return hookEvents
|
||||||
|
}
|
||||||
|
|
||||||
// addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is
|
// addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is
|
||||||
// an error, write to `ctx` accordingly. Return (webhook, ok)
|
// an error, write to `ctx` accordingly. Return (webhook, ok)
|
||||||
func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) {
|
func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) {
|
||||||
@ -162,9 +203,6 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(form.Events) == 0 {
|
|
||||||
form.Events = []string{"push"}
|
|
||||||
}
|
|
||||||
if form.Config["is_system_webhook"] != "" {
|
if form.Config["is_system_webhook"] != "" {
|
||||||
sw, err := strconv.ParseBool(form.Config["is_system_webhook"])
|
sw, err := strconv.ParseBool(form.Config["is_system_webhook"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -183,32 +221,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
|
|||||||
IsSystemWebhook: isSystemWebhook,
|
IsSystemWebhook: isSystemWebhook,
|
||||||
HookEvent: &webhook_module.HookEvent{
|
HookEvent: &webhook_module.HookEvent{
|
||||||
ChooseEvents: true,
|
ChooseEvents: true,
|
||||||
HookEvents: webhook_module.HookEvents{
|
HookEvents: updateHookEvents(form.Events),
|
||||||
webhook_module.HookEventCreate: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true),
|
|
||||||
webhook_module.HookEventDelete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true),
|
|
||||||
webhook_module.HookEventFork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true),
|
|
||||||
webhook_module.HookEventIssues: issuesHook(form.Events, "issues_only"),
|
|
||||||
webhook_module.HookEventIssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)),
|
|
||||||
webhook_module.HookEventIssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)),
|
|
||||||
webhook_module.HookEventIssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)),
|
|
||||||
webhook_module.HookEventIssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)),
|
|
||||||
webhook_module.HookEventPush: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true),
|
|
||||||
webhook_module.HookEventPullRequest: pullHook(form.Events, "pull_request_only"),
|
|
||||||
webhook_module.HookEventPullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)),
|
|
||||||
webhook_module.HookEventPullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)),
|
|
||||||
webhook_module.HookEventPullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)),
|
|
||||||
webhook_module.HookEventPullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)),
|
|
||||||
webhook_module.HookEventPullRequestReview: pullHook(form.Events, "pull_request_review"),
|
|
||||||
webhook_module.HookEventPullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)),
|
|
||||||
webhook_module.HookEventPullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)),
|
|
||||||
webhook_module.HookEventWiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true),
|
|
||||||
webhook_module.HookEventRepository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true),
|
|
||||||
webhook_module.HookEventRelease: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true),
|
|
||||||
webhook_module.HookEventPackage: util.SliceContainsString(form.Events, string(webhook_module.HookEventPackage), true),
|
|
||||||
webhook_module.HookEventStatus: util.SliceContainsString(form.Events, string(webhook_module.HookEventStatus), true),
|
|
||||||
webhook_module.HookEventWorkflowRun: util.SliceContainsString(form.Events, string(webhook_module.HookEventWorkflowRun), true),
|
|
||||||
webhook_module.HookEventWorkflowJob: util.SliceContainsString(form.Events, string(webhook_module.HookEventWorkflowJob), true),
|
|
||||||
},
|
|
||||||
BranchFilter: form.BranchFilter,
|
BranchFilter: form.BranchFilter,
|
||||||
},
|
},
|
||||||
IsActive: form.Active,
|
IsActive: form.Active,
|
||||||
@ -325,6 +338,10 @@ func EditRepoHook(ctx *context.APIContext, form *api.EditHookOption, hookID int6
|
|||||||
func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webhook) bool {
|
func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webhook) bool {
|
||||||
if form.Config != nil {
|
if form.Config != nil {
|
||||||
if url, ok := form.Config["url"]; ok {
|
if url, ok := form.Config["url"]; ok {
|
||||||
|
if !validation.IsValidURL(url) {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, "Invalid url")
|
||||||
|
return false
|
||||||
|
}
|
||||||
w.URL = url
|
w.URL = url
|
||||||
}
|
}
|
||||||
if ct, ok := form.Config["content_type"]; ok {
|
if ct, ok := form.Config["content_type"]; ok {
|
||||||
@ -353,19 +370,10 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update events
|
// Update events
|
||||||
if len(form.Events) == 0 {
|
w.HookEvents = updateHookEvents(form.Events)
|
||||||
form.Events = []string{"push"}
|
|
||||||
}
|
|
||||||
w.PushOnly = false
|
w.PushOnly = false
|
||||||
w.SendEverything = false
|
w.SendEverything = false
|
||||||
w.ChooseEvents = true
|
w.ChooseEvents = true
|
||||||
w.HookEvents[webhook_module.HookEventCreate] = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true)
|
|
||||||
w.HookEvents[webhook_module.HookEventPush] = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true)
|
|
||||||
w.HookEvents[webhook_module.HookEventDelete] = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true)
|
|
||||||
w.HookEvents[webhook_module.HookEventFork] = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true)
|
|
||||||
w.HookEvents[webhook_module.HookEventRepository] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true)
|
|
||||||
w.HookEvents[webhook_module.HookEventWiki] = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true)
|
|
||||||
w.HookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true)
|
|
||||||
w.BranchFilter = form.BranchFilter
|
w.BranchFilter = form.BranchFilter
|
||||||
|
|
||||||
err := w.SetHeaderAuthorization(form.AuthorizationHeader)
|
err := w.SetHeaderAuthorization(form.AuthorizationHeader)
|
||||||
@ -374,23 +382,6 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issues
|
|
||||||
w.HookEvents[webhook_module.HookEventIssues] = issuesHook(form.Events, "issues_only")
|
|
||||||
w.HookEvents[webhook_module.HookEventIssueAssign] = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign))
|
|
||||||
w.HookEvents[webhook_module.HookEventIssueLabel] = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel))
|
|
||||||
w.HookEvents[webhook_module.HookEventIssueMilestone] = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone))
|
|
||||||
w.HookEvents[webhook_module.HookEventIssueComment] = issuesHook(form.Events, string(webhook_module.HookEventIssueComment))
|
|
||||||
|
|
||||||
// Pull requests
|
|
||||||
w.HookEvents[webhook_module.HookEventPullRequest] = pullHook(form.Events, "pull_request_only")
|
|
||||||
w.HookEvents[webhook_module.HookEventPullRequestAssign] = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign))
|
|
||||||
w.HookEvents[webhook_module.HookEventPullRequestLabel] = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel))
|
|
||||||
w.HookEvents[webhook_module.HookEventPullRequestMilestone] = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone))
|
|
||||||
w.HookEvents[webhook_module.HookEventPullRequestComment] = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment))
|
|
||||||
w.HookEvents[webhook_module.HookEventPullRequestReview] = pullHook(form.Events, "pull_request_review")
|
|
||||||
w.HookEvents[webhook_module.HookEventPullRequestReviewRequest] = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest))
|
|
||||||
w.HookEvents[webhook_module.HookEventPullRequestSync] = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync))
|
|
||||||
|
|
||||||
if err := w.UpdateEvent(); err != nil {
|
if err := w.UpdateEvent(); err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return false
|
return false
|
||||||
|
82
routers/api/v1/utils/hook_test.go
Normal file
82
routers/api/v1/utils/hook_test.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTestHookValidation(t *testing.T) {
|
||||||
|
unittest.PrepareTestEnv(t)
|
||||||
|
|
||||||
|
t.Run("Test Validation", func(t *testing.T) {
|
||||||
|
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks")
|
||||||
|
contexttest.LoadRepo(t, ctx, 1)
|
||||||
|
contexttest.LoadRepoCommit(t, ctx)
|
||||||
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
|
|
||||||
|
checkCreateHookOption(ctx, &structs.CreateHookOption{
|
||||||
|
Type: "gitea",
|
||||||
|
Config: map[string]string{
|
||||||
|
"content_type": "json",
|
||||||
|
"url": "https://example.com/webhook",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, 0, ctx.Resp.WrittenStatus()) // not written yet
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test Validation with invalid URL", func(t *testing.T) {
|
||||||
|
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks")
|
||||||
|
contexttest.LoadRepo(t, ctx, 1)
|
||||||
|
contexttest.LoadRepoCommit(t, ctx)
|
||||||
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
|
|
||||||
|
checkCreateHookOption(ctx, &structs.CreateHookOption{
|
||||||
|
Type: "gitea",
|
||||||
|
Config: map[string]string{
|
||||||
|
"content_type": "json",
|
||||||
|
"url": "example.com/webhook",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test Validation with invalid webhook type", func(t *testing.T) {
|
||||||
|
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks")
|
||||||
|
contexttest.LoadRepo(t, ctx, 1)
|
||||||
|
contexttest.LoadRepoCommit(t, ctx)
|
||||||
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
|
|
||||||
|
checkCreateHookOption(ctx, &structs.CreateHookOption{
|
||||||
|
Type: "unknown",
|
||||||
|
Config: map[string]string{
|
||||||
|
"content_type": "json",
|
||||||
|
"url": "example.com/webhook",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test Validation with empty content type", func(t *testing.T) {
|
||||||
|
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks")
|
||||||
|
contexttest.LoadRepo(t, ctx, 1)
|
||||||
|
contexttest.LoadRepoCommit(t, ctx)
|
||||||
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
|
|
||||||
|
checkCreateHookOption(ctx, &structs.CreateHookOption{
|
||||||
|
Type: "unknown",
|
||||||
|
Config: map[string]string{
|
||||||
|
"url": "https://example.com/webhook",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
|
||||||
|
})
|
||||||
|
}
|
21
routers/api/v1/utils/main_test.go
Normal file
21
routers/api/v1/utils/main_test.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
webhook_service "code.gitea.io/gitea/services/webhook"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
unittest.MainTest(m, &unittest.TestOptions{
|
||||||
|
SetUp: func() error {
|
||||||
|
setting.LoadQueueSettings()
|
||||||
|
return webhook_service.Init()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
// ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed
|
// ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed
|
||||||
func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
|
func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
|
||||||
commits, err := ctx.Repo.Commit.CommitsByRange(0, 10, "")
|
commits, err := ctx.Repo.Commit.CommitsByRange(0, 10, "", "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("ShowBranchFeed", err)
|
ctx.ServerError("ShowBranchFeed", err)
|
||||||
return
|
return
|
||||||
|
@ -633,20 +633,20 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
|
|||||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, util.ErrNotExist) {
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
ctx.HTTPError(http.StatusNotFound, err.Error())
|
ctx.NotFound(nil)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
ctx.ServerError("GetRunByIndex", err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
run.Repo = ctx.Repo.Repository
|
run.Repo = ctx.Repo.Repository
|
||||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
ctx.ServerError("GetRunJobsByRunID", err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if len(jobs) == 0 {
|
if len(jobs) == 0 {
|
||||||
ctx.HTTPError(http.StatusNotFound)
|
ctx.NotFound(nil)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ func Commits(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Both `git log branchName` and `git log commitId` work.
|
// Both `git log branchName` and `git log commitId` work.
|
||||||
commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "")
|
commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "", "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("CommitsByRange", err)
|
ctx.ServerError("CommitsByRange", err)
|
||||||
return
|
return
|
||||||
|
@ -155,6 +155,22 @@ func CleanupEphemeralRunners(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CleanupEphemeralRunnersByPickedTaskOfRepo removes all ephemeral runners that have active/finished tasks on the given repository
|
||||||
|
func CleanupEphemeralRunnersByPickedTaskOfRepo(ctx context.Context, repoID int64) error {
|
||||||
|
subQuery := builder.Select("`action_runner`.id").
|
||||||
|
From(builder.Select("*").From("`action_runner`"), "`action_runner`"). // mysql needs this redundant subquery
|
||||||
|
Join("INNER", "`action_task`", "`action_task`.`runner_id` = `action_runner`.`id`").
|
||||||
|
Where(builder.And(builder.Eq{"`action_runner`.`ephemeral`": true}, builder.Eq{"`action_task`.`repo_id`": repoID}))
|
||||||
|
b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`")
|
||||||
|
res, err := db.GetEngine(ctx).Exec(b)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("find runners: %w", err)
|
||||||
|
}
|
||||||
|
affected, _ := res.RowsAffected()
|
||||||
|
log.Info("Removed %d runners", affected)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteRun deletes workflow run, including all logs and artifacts.
|
// DeleteRun deletes workflow run, including all logs and artifacts.
|
||||||
func DeleteRun(ctx context.Context, run *actions_model.ActionRun) error {
|
func DeleteRun(ctx context.Context, run *actions_model.ActionRun) error {
|
||||||
if !run.Status.IsDone() {
|
if !run.Status.IsDone() {
|
||||||
|
@ -15,11 +15,15 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GiteaContext map[string]any
|
||||||
|
|
||||||
// GenerateGiteaContext generate the gitea context without token and gitea_runtime_token
|
// GenerateGiteaContext generate the gitea context without token and gitea_runtime_token
|
||||||
// job can be nil when generating a context for parsing workflow-level expressions
|
// job can be nil when generating a context for parsing workflow-level expressions
|
||||||
func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.ActionRunJob) map[string]any {
|
func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.ActionRunJob) GiteaContext {
|
||||||
event := map[string]any{}
|
event := map[string]any{}
|
||||||
_ = json.Unmarshal([]byte(run.EventPayload), &event)
|
_ = json.Unmarshal([]byte(run.EventPayload), &event)
|
||||||
|
|
||||||
@ -42,7 +46,7 @@ func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.Actio
|
|||||||
|
|
||||||
refName := git.RefName(ref)
|
refName := git.RefName(ref)
|
||||||
|
|
||||||
gitContext := map[string]any{
|
gitContext := GiteaContext{
|
||||||
// standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
// standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
||||||
"action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2.
|
"action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2.
|
||||||
"action_path": "", // string, The path where an action is located. This property is only supported in composite actions. You can use this path to access files located in the same repository as the action.
|
"action_path": "", // string, The path where an action is located. This property is only supported in composite actions. You can use this path to access files located in the same repository as the action.
|
||||||
@ -160,3 +164,37 @@ func mergeTwoOutputs(o1, o2 map[string]string) map[string]string {
|
|||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *GiteaContext) ToGitHubContext() *model.GithubContext {
|
||||||
|
return &model.GithubContext{
|
||||||
|
Event: (*g)["event"].(map[string]any),
|
||||||
|
EventPath: (*g)["event_path"].(string),
|
||||||
|
Workflow: (*g)["workflow"].(string),
|
||||||
|
RunID: (*g)["run_id"].(string),
|
||||||
|
RunNumber: (*g)["run_number"].(string),
|
||||||
|
Actor: (*g)["actor"].(string),
|
||||||
|
Repository: (*g)["repository"].(string),
|
||||||
|
EventName: (*g)["event_name"].(string),
|
||||||
|
Sha: (*g)["sha"].(string),
|
||||||
|
Ref: (*g)["ref"].(string),
|
||||||
|
RefName: (*g)["ref_name"].(string),
|
||||||
|
RefType: (*g)["ref_type"].(string),
|
||||||
|
HeadRef: (*g)["head_ref"].(string),
|
||||||
|
BaseRef: (*g)["base_ref"].(string),
|
||||||
|
Token: "", // deliberately omitted for security
|
||||||
|
Workspace: (*g)["workspace"].(string),
|
||||||
|
Action: (*g)["action"].(string),
|
||||||
|
ActionPath: (*g)["action_path"].(string),
|
||||||
|
ActionRef: (*g)["action_ref"].(string),
|
||||||
|
ActionRepository: (*g)["action_repository"].(string),
|
||||||
|
Job: (*g)["job"].(string),
|
||||||
|
JobName: "", // not present in GiteaContext
|
||||||
|
RepositoryOwner: (*g)["repository_owner"].(string),
|
||||||
|
RetentionDays: (*g)["retention_days"].(string),
|
||||||
|
RunnerPerflog: "", // not present in GiteaContext
|
||||||
|
RunnerTrackingID: "", // not present in GiteaContext
|
||||||
|
ServerURL: (*g)["server_url"].(string),
|
||||||
|
APIURL: (*g)["api_url"].(string),
|
||||||
|
GraphQLURL: (*g)["graphql_url"].(string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -323,9 +323,11 @@ func handleWorkflows(
|
|||||||
run := &actions_model.ActionRun{
|
run := &actions_model.ActionRun{
|
||||||
Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
|
Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
|
||||||
RepoID: input.Repo.ID,
|
RepoID: input.Repo.ID,
|
||||||
|
Repo: input.Repo,
|
||||||
OwnerID: input.Repo.OwnerID,
|
OwnerID: input.Repo.OwnerID,
|
||||||
WorkflowID: dwf.EntryName,
|
WorkflowID: dwf.EntryName,
|
||||||
TriggerUserID: input.Doer.ID,
|
TriggerUserID: input.Doer.ID,
|
||||||
|
TriggerUser: input.Doer,
|
||||||
Ref: ref,
|
Ref: ref,
|
||||||
CommitSHA: commit.ID.String(),
|
CommitSHA: commit.ID.String(),
|
||||||
IsForkPullRequest: isForkPullRequest,
|
IsForkPullRequest: isForkPullRequest,
|
||||||
@ -354,12 +356,18 @@ func handleWorkflows(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars))
|
giteaCtx := GenerateGiteaContext(run, nil)
|
||||||
|
|
||||||
|
jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("jobparser.Parse: %v", err)
|
log.Error("jobparser.Parse: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(jobs) > 0 && jobs[0].RunName != "" {
|
||||||
|
run.Title = jobs[0].RunName
|
||||||
|
}
|
||||||
|
|
||||||
// cancel running jobs if the event is push or pull_request_sync
|
// cancel running jobs if the event is push or pull_request_sync
|
||||||
if run.Event == webhook_module.HookEventPush ||
|
if run.Event == webhook_module.HookEventPush ||
|
||||||
run.Event == webhook_module.HookEventPullRequestSync {
|
run.Event == webhook_module.HookEventPullRequestSync {
|
||||||
@ -538,9 +546,11 @@ func handleSchedules(
|
|||||||
run := &actions_model.ActionSchedule{
|
run := &actions_model.ActionSchedule{
|
||||||
Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
|
Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
|
||||||
RepoID: input.Repo.ID,
|
RepoID: input.Repo.ID,
|
||||||
|
Repo: input.Repo,
|
||||||
OwnerID: input.Repo.OwnerID,
|
OwnerID: input.Repo.OwnerID,
|
||||||
WorkflowID: dwf.EntryName,
|
WorkflowID: dwf.EntryName,
|
||||||
TriggerUserID: user_model.ActionsUserID,
|
TriggerUserID: user_model.ActionsUserID,
|
||||||
|
TriggerUser: user_model.NewActionsUser(),
|
||||||
Ref: ref,
|
Ref: ref,
|
||||||
CommitSHA: commit.ID.String(),
|
CommitSHA: commit.ID.String(),
|
||||||
Event: input.Event,
|
Event: input.Event,
|
||||||
@ -548,6 +558,25 @@ func handleSchedules(
|
|||||||
Specs: schedules,
|
Specs: schedules,
|
||||||
Content: dwf.Content,
|
Content: dwf.Content,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vars, err := actions_model.GetVariablesOfRun(ctx, run.ToActionRun())
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetVariablesOfRun: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
giteaCtx := GenerateGiteaContext(run.ToActionRun(), nil)
|
||||||
|
|
||||||
|
jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext()))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("jobparser.Parse: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(jobs) > 0 && jobs[0].RunName != "" {
|
||||||
|
run.Title = jobs[0].RunName
|
||||||
|
}
|
||||||
|
|
||||||
crons = append(crons, run)
|
crons = append(crons, run)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,22 +98,55 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
|
|
||||||
// find workflow from commit
|
// find workflow from commit
|
||||||
var workflows []*jobparser.SingleWorkflow
|
var workflows []*jobparser.SingleWorkflow
|
||||||
for _, entry := range entries {
|
var entry *git.TreeEntry
|
||||||
if entry.Name() != workflowID {
|
|
||||||
|
run := &actions_model.ActionRun{
|
||||||
|
Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Repo: repo,
|
||||||
|
OwnerID: repo.OwnerID,
|
||||||
|
WorkflowID: workflowID,
|
||||||
|
TriggerUserID: doer.ID,
|
||||||
|
TriggerUser: doer,
|
||||||
|
Ref: string(refName),
|
||||||
|
CommitSHA: runTargetCommit.ID.String(),
|
||||||
|
IsForkPullRequest: false,
|
||||||
|
Event: "workflow_dispatch",
|
||||||
|
TriggerEvent: "workflow_dispatch",
|
||||||
|
Status: actions_model.StatusWaiting,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.Name() != workflowID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
entry = e
|
||||||
content, err := actions.GetContentFromEntry(entry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
workflows, err = jobparser.Parse(content)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if entry == nil {
|
||||||
|
return util.ErrorWrapLocale(
|
||||||
|
util.NewNotExistErrorf("workflow %q doesn't exist", workflowID),
|
||||||
|
"actions.workflow.not_found", workflowID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := actions.GetContentFromEntry(entry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
giteaCtx := GenerateGiteaContext(run, nil)
|
||||||
|
|
||||||
|
workflows, err = jobparser.Parse(content, jobparser.WithGitContext(giteaCtx.ToGitHubContext()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(workflows) > 0 && workflows[0].RunName != "" {
|
||||||
|
run.Title = workflows[0].RunName
|
||||||
|
}
|
||||||
|
|
||||||
if len(workflows) == 0 {
|
if len(workflows) == 0 {
|
||||||
return util.ErrorWrapLocale(
|
return util.ErrorWrapLocale(
|
||||||
util.NewNotExistErrorf("workflow %q doesn't exist", workflowID),
|
util.NewNotExistErrorf("workflow %q doesn't exist", workflowID),
|
||||||
@ -142,25 +175,12 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
Inputs: inputsWithDefaults,
|
Inputs: inputsWithDefaults,
|
||||||
Sender: convert.ToUserWithAccessMode(ctx, doer, perm.AccessModeNone),
|
Sender: convert.ToUserWithAccessMode(ctx, doer, perm.AccessModeNone),
|
||||||
}
|
}
|
||||||
|
|
||||||
var eventPayload []byte
|
var eventPayload []byte
|
||||||
if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
|
if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
|
||||||
return fmt.Errorf("JSONPayload: %w", err)
|
return fmt.Errorf("JSONPayload: %w", err)
|
||||||
}
|
}
|
||||||
|
run.EventPayload = string(eventPayload)
|
||||||
run := &actions_model.ActionRun{
|
|
||||||
Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
|
|
||||||
RepoID: repo.ID,
|
|
||||||
OwnerID: repo.OwnerID,
|
|
||||||
WorkflowID: workflowID,
|
|
||||||
TriggerUserID: doer.ID,
|
|
||||||
Ref: string(refName),
|
|
||||||
CommitSHA: runTargetCommit.ID.String(),
|
|
||||||
IsForkPullRequest: false,
|
|
||||||
Event: "workflow_dispatch",
|
|
||||||
TriggerEvent: "workflow_dispatch",
|
|
||||||
EventPayload: string(eventPayload),
|
|
||||||
Status: actions_model.StatusWaiting,
|
|
||||||
}
|
|
||||||
|
|
||||||
// cancel running jobs of the same workflow
|
// cancel running jobs of the same workflow
|
||||||
if err := CancelPreviousJobs(
|
if err := CancelPreviousJobs(
|
||||||
@ -195,6 +215,5 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
for _, job := range allJobs {
|
for _, job := range allJobs {
|
||||||
notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil)
|
notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,8 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||||||
allowSquash := false
|
allowSquash := false
|
||||||
allowFastForwardOnly := false
|
allowFastForwardOnly := false
|
||||||
allowRebaseUpdate := false
|
allowRebaseUpdate := false
|
||||||
|
allowManualMerge := true
|
||||||
|
autodetectManualMerge := false
|
||||||
defaultDeleteBranchAfterMerge := false
|
defaultDeleteBranchAfterMerge := false
|
||||||
defaultMergeStyle := repo_model.MergeStyleMerge
|
defaultMergeStyle := repo_model.MergeStyleMerge
|
||||||
defaultAllowMaintainerEdit := false
|
defaultAllowMaintainerEdit := false
|
||||||
@ -111,6 +113,8 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||||||
allowSquash = config.AllowSquash
|
allowSquash = config.AllowSquash
|
||||||
allowFastForwardOnly = config.AllowFastForwardOnly
|
allowFastForwardOnly = config.AllowFastForwardOnly
|
||||||
allowRebaseUpdate = config.AllowRebaseUpdate
|
allowRebaseUpdate = config.AllowRebaseUpdate
|
||||||
|
allowManualMerge = config.AllowManualMerge
|
||||||
|
autodetectManualMerge = config.AutodetectManualMerge
|
||||||
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
|
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
|
||||||
defaultMergeStyle = config.GetDefaultMergeStyle()
|
defaultMergeStyle = config.GetDefaultMergeStyle()
|
||||||
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
|
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
|
||||||
@ -235,6 +239,8 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||||||
AllowSquash: allowSquash,
|
AllowSquash: allowSquash,
|
||||||
AllowFastForwardOnly: allowFastForwardOnly,
|
AllowFastForwardOnly: allowFastForwardOnly,
|
||||||
AllowRebaseUpdate: allowRebaseUpdate,
|
AllowRebaseUpdate: allowRebaseUpdate,
|
||||||
|
AllowManualMerge: allowManualMerge,
|
||||||
|
AutodetectManualMerge: autodetectManualMerge,
|
||||||
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
|
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
|
||||||
DefaultMergeStyle: string(defaultMergeStyle),
|
DefaultMergeStyle: string(defaultMergeStyle),
|
||||||
DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,
|
DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/lfs"
|
"code.gitea.io/gitea/modules/lfs"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
|
actions_service "code.gitea.io/gitea/services/actions"
|
||||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
@ -133,6 +134,14 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CleanupEphemeralRunnersByPickedTaskOfRepo deletes ephemeral global/org/user that have started any task of this repo
|
||||||
|
// The cannot pick a second task hardening for ephemeral runners expect that task objects remain available until runner deletion
|
||||||
|
// This method will delete affected ephemeral global/org/user runners
|
||||||
|
// &actions_model.ActionRunner{RepoID: repoID} does only handle ephemeral repository runners
|
||||||
|
if err := actions_service.CleanupEphemeralRunnersByPickedTaskOfRepo(ctx, repoID); err != nil {
|
||||||
|
return fmt.Errorf("cleanupEphemeralRunners: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := db.DeleteBeans(ctx,
|
if err := db.DeleteBeans(ctx,
|
||||||
&access_model.Access{RepoID: repo.ID},
|
&access_model.Access{RepoID: repo.ID},
|
||||||
&activities_model.Action{RepoID: repo.ID},
|
&activities_model.Action{RepoID: repo.ID},
|
||||||
|
@ -311,7 +311,7 @@ func parseHookPullRequestEventType(event webhook_module.HookEventType) (string,
|
|||||||
case webhook_module.HookEventPullRequestReviewApproved:
|
case webhook_module.HookEventPullRequestReviewApproved:
|
||||||
return "approved", nil
|
return "approved", nil
|
||||||
case webhook_module.HookEventPullRequestReviewRejected:
|
case webhook_module.HookEventPullRequestReviewRejected:
|
||||||
return "rejected", nil
|
return "requested changes", nil
|
||||||
case webhook_module.HookEventPullRequestReviewComment:
|
case webhook_module.HookEventPullRequestReviewComment:
|
||||||
return "comment", nil
|
return "comment", nil
|
||||||
default:
|
default:
|
||||||
|
@ -226,11 +226,19 @@
|
|||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else if .Permission.IsAdmin}}
|
{{else}}
|
||||||
<div class="overflow-menu-items">
|
<div class="overflow-menu-items">
|
||||||
|
{{if(and .Repository.IsBeingCreated (.Permission.CanRead ctx.Consts.RepoUnitTypeCode))}}
|
||||||
|
<a class="{{if not .PageIsRepoSettings}}active {{end}}item" href="{{.RepoLink}}">
|
||||||
|
{{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.migrating_status"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Permission.IsAdmin}}
|
||||||
<a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings">
|
<a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings">
|
||||||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
|
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
|
||||||
</a>
|
</a>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</overflow-menu>
|
</overflow-menu>
|
||||||
|
22
templates/swagger/v1_json.tmpl
generated
22
templates/swagger/v1_json.tmpl
generated
@ -7130,6 +7130,20 @@
|
|||||||
"name": "path",
|
"name": "path",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "Only commits after this date will be returned (ISO 8601 format)",
|
||||||
|
"name": "since",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "Only commits before this date will be returned (ISO 8601 format)",
|
||||||
|
"name": "until",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "include diff stats for every commit (disable for speedup, default 'true')",
|
"description": "include diff stats for every commit (disable for speedup, default 'true')",
|
||||||
@ -27048,6 +27062,10 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"x-go-name": "AllowFastForwardOnly"
|
"x-go-name": "AllowFastForwardOnly"
|
||||||
},
|
},
|
||||||
|
"allow_manual_merge": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "AllowManualMerge"
|
||||||
|
},
|
||||||
"allow_merge_commits": {
|
"allow_merge_commits": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"x-go-name": "AllowMerge"
|
"x-go-name": "AllowMerge"
|
||||||
@ -27077,6 +27095,10 @@
|
|||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"x-go-name": "ArchivedAt"
|
"x-go-name": "ArchivedAt"
|
||||||
},
|
},
|
||||||
|
"autodetect_manual_merge": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "AutodetectManualMerge"
|
||||||
|
},
|
||||||
"avatar_url": {
|
"avatar_url": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "AvatarURL"
|
"x-go-name": "AvatarURL"
|
||||||
|
@ -1156,6 +1156,7 @@ jobs:
|
|||||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
||||||
Title: "add workflow",
|
Title: "add workflow",
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
|
Repo: repo,
|
||||||
Event: "workflow_dispatch",
|
Event: "workflow_dispatch",
|
||||||
Ref: "refs/heads/dispatch",
|
Ref: "refs/heads/dispatch",
|
||||||
WorkflowID: "dispatch.yml",
|
WorkflowID: "dispatch.yml",
|
||||||
@ -1448,3 +1449,157 @@ jobs:
|
|||||||
assert.Equal(t, pullRequest.MergedCommitID, actionRun.CommitSHA)
|
assert.Equal(t, pullRequest.MergedCommitID, actionRun.CommitSHA)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestActionRunNameWithContextVariables(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
|
// create the repo
|
||||||
|
repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
|
||||||
|
Name: "action-run-name-with-variables",
|
||||||
|
Description: "test action run name",
|
||||||
|
AutoInit: true,
|
||||||
|
Gitignores: "Go",
|
||||||
|
License: "MIT",
|
||||||
|
Readme: "Default",
|
||||||
|
DefaultBranch: "main",
|
||||||
|
IsPrivate: false,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, repo)
|
||||||
|
|
||||||
|
// add workflow file to the repo
|
||||||
|
addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "create",
|
||||||
|
TreePath: ".gitea/workflows/runname.yml",
|
||||||
|
ContentReader: strings.NewReader(`name: test
|
||||||
|
on:
|
||||||
|
[create,delete]
|
||||||
|
run-name: ${{ gitea.actor }} is running this workflow
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo helloworld
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Message: "add workflow with run-name",
|
||||||
|
OldBranch: "main",
|
||||||
|
NewBranch: "main",
|
||||||
|
Author: &files_service.IdentityOptions{
|
||||||
|
GitUserName: user2.Name,
|
||||||
|
GitUserEmail: user2.Email,
|
||||||
|
},
|
||||||
|
Committer: &files_service.IdentityOptions{
|
||||||
|
GitUserName: user2.Name,
|
||||||
|
GitUserEmail: user2.Email,
|
||||||
|
},
|
||||||
|
Dates: &files_service.CommitDateOptions{
|
||||||
|
Author: time.Now(),
|
||||||
|
Committer: time.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, addWorkflowToBaseResp)
|
||||||
|
|
||||||
|
// Get the commit ID of the default branch
|
||||||
|
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer gitRepo.Close()
|
||||||
|
branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// create a branch
|
||||||
|
err = repo_service.CreateNewBranchFromCommit(db.DefaultContext, user2, repo, gitRepo, branch.CommitID, "test-action-run-name-with-variables")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
||||||
|
Title: user2.LoginName + " is running this workflow",
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Event: "create",
|
||||||
|
Ref: "refs/heads/test-action-run-name-with-variables",
|
||||||
|
WorkflowID: "runname.yml",
|
||||||
|
CommitSHA: branch.CommitID,
|
||||||
|
})
|
||||||
|
assert.NotNil(t, run)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionRunName(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
|
// create the repo
|
||||||
|
repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
|
||||||
|
Name: "action-run-name",
|
||||||
|
Description: "test action run-name",
|
||||||
|
AutoInit: true,
|
||||||
|
Gitignores: "Go",
|
||||||
|
License: "MIT",
|
||||||
|
Readme: "Default",
|
||||||
|
DefaultBranch: "main",
|
||||||
|
IsPrivate: false,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, repo)
|
||||||
|
|
||||||
|
// add workflow file to the repo
|
||||||
|
addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "create",
|
||||||
|
TreePath: ".gitea/workflows/runname.yml",
|
||||||
|
ContentReader: strings.NewReader(`name: test
|
||||||
|
on:
|
||||||
|
[create,delete]
|
||||||
|
run-name: run name without variables
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo helloworld
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Message: "add workflow with run name",
|
||||||
|
OldBranch: "main",
|
||||||
|
NewBranch: "main",
|
||||||
|
Author: &files_service.IdentityOptions{
|
||||||
|
GitUserName: user2.Name,
|
||||||
|
GitUserEmail: user2.Email,
|
||||||
|
},
|
||||||
|
Committer: &files_service.IdentityOptions{
|
||||||
|
GitUserName: user2.Name,
|
||||||
|
GitUserEmail: user2.Email,
|
||||||
|
},
|
||||||
|
Dates: &files_service.CommitDateOptions{
|
||||||
|
Author: time.Now(),
|
||||||
|
Committer: time.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, addWorkflowToBaseResp)
|
||||||
|
|
||||||
|
// Get the commit ID of the default branch
|
||||||
|
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer gitRepo.Close()
|
||||||
|
branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// create a branch
|
||||||
|
err = repo_service.CreateNewBranchFromCommit(db.DefaultContext, user2, repo, gitRepo, branch.CommitID, "test-action-run-name")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
||||||
|
Title: "run name without variables",
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Event: "create",
|
||||||
|
Ref: "refs/heads/test-action-run-name",
|
||||||
|
WorkflowID: "runname.yml",
|
||||||
|
CommitSHA: branch.CommitID,
|
||||||
|
})
|
||||||
|
assert.NotNil(t, run)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -41,8 +41,6 @@ func testActionsRunnerAdmin(t *testing.T) {
|
|||||||
runnerList := api.ActionRunnersResponse{}
|
runnerList := api.ActionRunnersResponse{}
|
||||||
DecodeJSON(t, runnerListResp, &runnerList)
|
DecodeJSON(t, runnerListResp, &runnerList)
|
||||||
|
|
||||||
assert.Len(t, runnerList.Entries, 4)
|
|
||||||
|
|
||||||
idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34349 })
|
idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34349 })
|
||||||
require.NotEqual(t, -1, idx)
|
require.NotEqual(t, -1, idx)
|
||||||
expectedRunner := runnerList.Entries[idx]
|
expectedRunner := runnerList.Entries[idx]
|
||||||
@ -160,16 +158,20 @@ func testActionsRunnerOwner(t *testing.T) {
|
|||||||
runnerList := api.ActionRunnersResponse{}
|
runnerList := api.ActionRunnersResponse{}
|
||||||
DecodeJSON(t, runnerListResp, &runnerList)
|
DecodeJSON(t, runnerListResp, &runnerList)
|
||||||
|
|
||||||
assert.Len(t, runnerList.Entries, 1)
|
idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34347 })
|
||||||
assert.Equal(t, "runner_to_be_deleted-org", runnerList.Entries[0].Name)
|
require.NotEqual(t, -1, idx)
|
||||||
assert.Equal(t, int64(34347), runnerList.Entries[0].ID)
|
expectedRunner := runnerList.Entries[idx]
|
||||||
assert.False(t, runnerList.Entries[0].Ephemeral)
|
|
||||||
assert.Len(t, runnerList.Entries[0].Labels, 2)
|
require.NotNil(t, expectedRunner)
|
||||||
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
|
assert.Equal(t, "runner_to_be_deleted-org", expectedRunner.Name)
|
||||||
assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].Name)
|
assert.Equal(t, int64(34347), expectedRunner.ID)
|
||||||
|
assert.False(t, expectedRunner.Ephemeral)
|
||||||
|
assert.Len(t, expectedRunner.Labels, 2)
|
||||||
|
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
|
||||||
|
assert.Equal(t, "linux", expectedRunner.Labels[1].Name)
|
||||||
|
|
||||||
// Verify get the runner by id
|
// Verify get the runner by id
|
||||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
|
||||||
runnerResp := MakeRequest(t, req, http.StatusOK)
|
runnerResp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
runner := api.ActionRunner{}
|
runner := api.ActionRunner{}
|
||||||
@ -183,11 +185,11 @@ func testActionsRunnerOwner(t *testing.T) {
|
|||||||
assert.Equal(t, "linux", runner.Labels[1].Name)
|
assert.Equal(t, "linux", runner.Labels[1].Name)
|
||||||
|
|
||||||
// Verify delete the runner by id
|
// Verify delete the runner by id
|
||||||
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
|
||||||
MakeRequest(t, req, http.StatusNoContent)
|
MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
// Verify runner deletion
|
// Verify runner deletion
|
||||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -329,4 +331,12 @@ func testActionsRunnerRepo(t *testing.T) {
|
|||||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34349)).AddTokenAuth(token)
|
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34349)).AddTokenAuth(token)
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteAdminRunnerNotFoundUnknownID", func(t *testing.T) {
|
||||||
|
userUsername := "user2"
|
||||||
|
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
// Verify delete a runner by unknown id is not found
|
||||||
|
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 4384797347934)).AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
79
tests/integration/ephemeral_actions_runner_deletion_test.go
Normal file
79
tests/integration/ephemeral_actions_runner_deletion_test.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
|
user_service "code.gitea.io/gitea/services/user"
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEphemeralActionsRunnerDeletion(t *testing.T) {
|
||||||
|
t.Run("ByTaskCompletion", testEphemeralActionsRunnerDeletionByTaskCompletion)
|
||||||
|
t.Run("ByRepository", testEphemeralActionsRunnerDeletionByRepository)
|
||||||
|
t.Run("ByUser", testEphemeralActionsRunnerDeletionByUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that the ephemeral runner is deleted when the task is finished
|
||||||
|
func testEphemeralActionsRunnerDeletionByTaskCompletion(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
_, err := actions_model.GetRunnerByID(t.Context(), 34350)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 52})
|
||||||
|
assert.Equal(t, actions_model.StatusRunning, task.Status)
|
||||||
|
|
||||||
|
task.Status = actions_model.StatusSuccess
|
||||||
|
err = actions_model.UpdateTask(t.Context(), task, "status")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = actions_model.GetRunnerByID(t.Context(), 34350)
|
||||||
|
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEphemeralActionsRunnerDeletionByRepository(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
_, err := actions_model.GetRunnerByID(t.Context(), 34350)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 52})
|
||||||
|
assert.Equal(t, actions_model.StatusRunning, task.Status)
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
|
err = repo_service.DeleteRepositoryDirectly(t.Context(), user, task.RepoID, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = actions_model.GetRunnerByID(t.Context(), 34350)
|
||||||
|
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that the ephemeral runner is deleted when a user is deleted
|
||||||
|
func testEphemeralActionsRunnerDeletionByUser(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
_, err := actions_model.GetRunnerByID(t.Context(), 34350)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 52})
|
||||||
|
assert.Equal(t, actions_model.StatusRunning, task.Status)
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
|
err = user_service.DeleteUser(t.Context(), user, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = actions_model.GetRunnerByID(t.Context(), 34350)
|
||||||
|
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||||
|
}
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
.project-header {
|
.project-header {
|
||||||
padding: 0.5em 0;
|
padding: 0.5em 0;
|
||||||
overflow-x: auto; /* in fullscreen mode, the position is fixed, so we can't use "flex wrap" which would change the height */
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-header h2 {
|
.project-header h2 {
|
||||||
@ -101,17 +101,11 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen.projects-view .project-header {
|
.fullscreen.projects-view {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
z-index: 1000;
|
inset: 0;
|
||||||
top: 0;
|
display: flex;
|
||||||
left: 0;
|
flex-direction: column;
|
||||||
right: 0;
|
|
||||||
padding: 0.5em;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
background-color: var(--color-body);
|
|
||||||
border-bottom: 1px solid var(--color-secondary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide project-description in full-screen due to its variable height. No need to show it for better space use. */
|
/* Hide project-description in full-screen due to its variable height. No need to show it for better space use. */
|
||||||
@ -120,9 +114,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen.projects-view #project-board {
|
.fullscreen.projects-view #project-board {
|
||||||
position: absolute;
|
flex: 1;
|
||||||
top: 60px;
|
max-height: unset;
|
||||||
left: 0;
|
padding-bottom: 0.5em;
|
||||||
right: 0;
|
|
||||||
max-height: calc(100vh - 70px);
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user