Compare commits

...

4 Commits

Author SHA1 Message Date
wxiaoguang
2d36a0c9ff Fix various bugs (#35684)
1. Fix incorrect column in `applySubscribedCondition`, add a test
2. Fix debian version parsing, add more tests fix #35695
3. Fix log level for HTTP errors, fix #35651
4. Fix abused "panic" handler in API `Migrate`
5. Fix the redirection from PR to issue, add a test
6. Fix Actions variable & secret name validation, add more tests
    * envNameCIRegexMatch is unnecessary, removed
    * validating in "delete" function doesn't make sense, removed
7. Fix incorrect link in release email

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
2025-10-19 00:37:50 +08:00
ChristopherHX
322cb048e7 Fix workflow run event status while rerunning a failed job (#35689)
The event reported a completion status instead of requested, therefore
sent an email
2025-10-18 03:31:34 +00:00
Lunny Xiao
a7eceb57a9 Use gitrepo.Repository instead of wikipath (#35398)
Now the wikipath will not be referenced directly.
2025-10-17 20:00:44 -07:00
GiteaBot
ebd88af075 [skip ci] Updated translations via Crowdin 2025-10-17 00:34:59 +00:00
35 changed files with 303 additions and 238 deletions

View File

@@ -476,7 +476,7 @@ func applySubscribedCondition(sess *xorm.Session, subscriberID int64) {
),
builder.Eq{"issue.poster_id": subscriberID},
builder.In("issue.repo_id", builder.
Select("id").
Select("repo_id").
From("watch").
Where(builder.And(builder.Eq{"user_id": subscriberID},
builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto))),

View File

@@ -197,6 +197,12 @@ func TestIssues(t *testing.T) {
},
[]int64{2},
},
{
issues_model.IssuesOptions{
SubscriberID: 11,
},
[]int64{11, 5, 9, 8, 3, 2, 1},
},
} {
issues, err := issues_model.Issues(t.Context(), &test.Opts)
assert.NoError(t, err)

View File

@@ -229,10 +229,6 @@ func RelativePath(ownerName, repoName string) string {
return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git"
}
func RelativeWikiPath(ownerName, repoName string) string {
return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git"
}
// RelativePath should be an unix style path like username/reponame.git
func (repo *Repository) RelativePath() string {
return RelativePath(repo.OwnerName, repo.Name)
@@ -245,12 +241,6 @@ func (sr StorageRepo) RelativePath() string {
return string(sr)
}
// WikiStorageRepo returns the storage repo for the wiki
// The wiki repository should have the same object format as the code repository
func (repo *Repository) WikiStorageRepo() StorageRepo {
return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name))
}
// SanitizedOriginalURL returns a sanitized OriginalURL
func (repo *Repository) SanitizedOriginalURL() string {
if repo.OriginalURL == "" {

View File

@@ -7,7 +7,6 @@ package repo
import (
"context"
"fmt"
"path/filepath"
"strings"
user_model "code.gitea.io/gitea/models/user"
@@ -76,12 +75,12 @@ func (repo *Repository) WikiCloneLink(ctx context.Context, doer *user_model.User
return repo.cloneLink(ctx, doer, repo.Name+".wiki")
}
// WikiPath returns wiki data path by given user and repository name.
func WikiPath(userName, repoName string) string {
return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".wiki.git")
func RelativeWikiPath(ownerName, repoName string) string {
return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git"
}
// WikiPath returns wiki data path for given repository.
func (repo *Repository) WikiPath() string {
return WikiPath(repo.OwnerName, repo.Name)
// WikiStorageRepo returns the storage repo for the wiki
// The wiki repository should have the same object format as the code repository
func (repo *Repository) WikiStorageRepo() StorageRepo {
return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name))
}

View File

@@ -4,12 +4,10 @@
package repo_test
import (
"path/filepath"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
@@ -23,15 +21,10 @@ func TestRepository_WikiCloneLink(t *testing.T) {
assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS)
}
func TestWikiPath(t *testing.T) {
func TestRepository_RelativeWikiPath(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git")
assert.Equal(t, expected, repo_model.WikiPath("user2", "repo1"))
}
func TestRepository_WikiPath(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git")
assert.Equal(t, expected, repo.WikiPath())
assert.Equal(t, "user2/repo1.wiki.git", repo_model.RelativeWikiPath(repo.OwnerName, repo.Name))
assert.Equal(t, "user2/repo1.wiki.git", repo.WikiStorageRepo().RelativePath())
}

View File

@@ -3,7 +3,13 @@
package git
import "code.gitea.io/gitea/modules/setting"
import (
"context"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/setting"
)
// Based on https://git-scm.com/docs/git-config#Documentation/git-config.txt-gpgformat
const (
@@ -24,3 +30,48 @@ func (s *SigningKey) String() string {
setting.PanicInDevOrTesting("don't call SigningKey.String() - it exposes the KeyID which might be a local file path")
return "SigningKey:" + s.Format
}
// GetSigningKey returns the KeyID and git Signature for the repo
func GetSigningKey(ctx context.Context, repoPath string) (*SigningKey, *Signature) {
if setting.Repository.Signing.SigningKey == "none" {
return nil, nil
}
if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
// Can ignore the error here as it means that commit.gpgsign is not set
value, _, _ := gitcmd.NewCommand("config", "--get", "commit.gpgsign").WithDir(repoPath).RunStdString(ctx)
sign, valid := ParseBool(strings.TrimSpace(value))
if !sign || !valid {
return nil, nil
}
format, _, _ := gitcmd.NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").WithDir(repoPath).RunStdString(ctx)
signingKey, _, _ := gitcmd.NewCommand("config", "--get", "user.signingkey").WithDir(repoPath).RunStdString(ctx)
signingName, _, _ := gitcmd.NewCommand("config", "--get", "user.name").WithDir(repoPath).RunStdString(ctx)
signingEmail, _, _ := gitcmd.NewCommand("config", "--get", "user.email").WithDir(repoPath).RunStdString(ctx)
if strings.TrimSpace(signingKey) == "" {
return nil, nil
}
return &SigningKey{
KeyID: strings.TrimSpace(signingKey),
Format: strings.TrimSpace(format),
}, &Signature{
Name: strings.TrimSpace(signingName),
Email: strings.TrimSpace(signingEmail),
}
}
if setting.Repository.Signing.SigningKey == "" {
return nil, nil
}
return &SigningKey{
KeyID: setting.Repository.Signing.SigningKey,
Format: setting.Repository.Signing.SigningFormat,
}, &Signature{
Name: setting.Repository.Signing.SigningName,
Email: setting.Repository.Signing.SigningEmail,
}
}

20
modules/gitrepo/clone.go Normal file
View File

@@ -0,0 +1,20 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"context"
"code.gitea.io/gitea/modules/git"
)
// CloneExternalRepo clones an external repository to the managed repository.
func CloneExternalRepo(ctx context.Context, fromRemoteURL string, toRepo Repository, opts git.CloneRepoOptions) error {
return git.Clone(ctx, fromRemoteURL, repoPath(toRepo), opts)
}
// CloneRepoToLocal clones a managed repository to a local path.
func CloneRepoToLocal(ctx context.Context, fromRepo Repository, toLocalPath string, opts git.CloneRepoOptions) error {
return git.Clone(ctx, repoPath(fromRepo), toLocalPath, opts)
}

View File

@@ -0,0 +1,14 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"context"
"code.gitea.io/gitea/modules/git"
)
func WriteCommitGraph(ctx context.Context, repo Repository) error {
return git.WriteCommitGraph(ctx, repoPath(repo))
}

View File

@@ -7,9 +7,12 @@ import (
"context"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@@ -86,3 +89,12 @@ func RenameRepository(ctx context.Context, repo, newRepo Repository) error {
func InitRepository(ctx context.Context, repo Repository, objectFormatName string) error {
return git.InitRepository(ctx, repoPath(repo), true, objectFormatName)
}
func UpdateServerInfo(ctx context.Context, repo Repository) error {
_, _, err := RunCmdBytes(ctx, repo, gitcmd.NewCommand("update-server-info"))
return err
}
func GetRepoFS(repo Repository) fs.FS {
return os.DirFS(repoPath(repo))
}

14
modules/gitrepo/push.go Normal file
View File

@@ -0,0 +1,14 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"context"
"code.gitea.io/gitea/modules/git"
)
func Push(ctx context.Context, repo Repository, opts git.PushOptions) error {
return git.Push(ctx, repoPath(repo), opts)
}

View File

@@ -0,0 +1,14 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"context"
"code.gitea.io/gitea/modules/git"
)
func GetSigningKey(ctx context.Context, repo Repository) (*git.SigningKey, *git.Signature) {
return git.GetSigningKey(ctx, repoPath(repo))
}

View File

@@ -46,7 +46,7 @@ var (
// https://www.debian.org/doc/debian-policy/ch-controlfields.html#source
namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`)
// https://www.debian.org/doc/debian-policy/ch-controlfields.html#version
versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`)
versionPattern = regexp.MustCompile(`\A(?:(0|[1-9][0-9]*):)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`)
)
type Package struct {

View File

@@ -176,4 +176,12 @@ func TestParseControlFile(t *testing.T) {
assert.Equal(t, []string{"a", "b"}, p.Metadata.Dependencies)
assert.Equal(t, full, p.Control)
})
t.Run("ValidVersions", func(t *testing.T) {
for _, version := range []string{"1.0", "0:1.2", "9:1.0", "10:1.0", "900:1a.2b-x-y_z~1+2"} {
p, err := ParseControlFile(buildContent("testpkg", version, "amd64"))
assert.NoError(t, err, "ParseControlFile with version %q", version)
assert.NotNil(t, p)
}
})
}

View File

@@ -104,7 +104,8 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) {
}
logf := logInfo
// lower the log level for some specific requests, in most cases these logs are not useful
if strings.HasPrefix(req.RequestURI, "/assets/") /* static assets */ ||
if status > 0 && status < 400 &&
strings.HasPrefix(req.RequestURI, "/assets/") /* static assets */ ||
req.RequestURI == "/user/events" /* Server-Sent Events (SSE) handler */ ||
req.RequestURI == "/api/actions/runner.v1.RunnerService/FetchTask" /* Actions Runner polling */ {
logf = logTrace

View File

@@ -2434,6 +2434,9 @@ settings.event_workflow_job_desc=Gitea Actions のワークフロージョブが
settings.event_package=パッケージ
settings.event_package_desc=リポジトリにパッケージが作成または削除されたとき。
settings.branch_filter=ブランチ フィルター
settings.branch_filter_desc_1=プッシュ、ブランチ作成、ブランチ削除イベントに対するブランチ(およびref名)の許可リストで、globパターンで指定します。 空または<code>*</code>の場合、すべてのブランチとタグのイベントが報告されます。
settings.branch_filter_desc_2=完全なref名にマッチさせるには、 <code>refs/heads/</code> または <code>refs/tags/</code> を前に付けてください。
settings.branch_filter_desc_doc=書き方についてはドキュメント <a href="%[1]s">%[2]s</a> を参照してください。
settings.authorization_header=Authorizationヘッダー
settings.authorization_header_desc=入力した場合、リクエストにAuthorizationヘッダーとして付加します。 例: %s
settings.active=有効

View File

@@ -4,7 +4,7 @@
package repo
import (
"bytes"
gocontext "context"
"errors"
"fmt"
"net/http"
@@ -173,7 +173,7 @@ func Migrate(ctx *context.APIContext) {
opts.AWSSecretAccessKey = form.AWSSecretAccessKey
}
repo, err := repo_service.CreateRepositoryDirectly(ctx, ctx.Doer, repoOwner, repo_service.CreateRepoOptions{
createdRepo, err := repo_service.CreateRepositoryDirectly(ctx, ctx.Doer, repoOwner, repo_service.CreateRepoOptions{
Name: opts.RepoName,
Description: opts.Description,
OriginalURL: form.CloneAddr,
@@ -187,35 +187,37 @@ func Migrate(ctx *context.APIContext) {
return
}
opts.MigrateToRepoID = repo.ID
opts.MigrateToRepoID = createdRepo.ID
defer func() {
if e := recover(); e != nil {
var buf bytes.Buffer
fmt.Fprintf(&buf, "Handler crashed with error: %v", log.Stack(2))
err = errors.New(buf.String())
}
if err == nil {
notify_service.MigrateRepository(ctx, ctx.Doer, repoOwner, repo)
return
}
if repo != nil {
if errDelete := repo_service.DeleteRepositoryDirectly(ctx, repo.ID); errDelete != nil {
log.Error("DeleteRepository: %v", errDelete)
doLongTimeMigrate := func(ctx gocontext.Context, doer *user_model.User) (migratedRepo *repo_model.Repository, retErr error) {
defer func() {
if e := recover(); e != nil {
log.Error("MigrateRepository panic: %v\n%s", e, log.Stack(2))
if errDelete := repo_service.DeleteRepositoryDirectly(ctx, createdRepo.ID); errDelete != nil {
log.Error("Unable to delete repo after MigrateRepository panic: %v", errDelete)
}
retErr = errors.New("MigrateRepository panic") // no idea why it would happen, just legacy code
}
}
}()
}()
if repo, err = migrations.MigrateRepository(graceful.GetManager().HammerContext(), ctx.Doer, repoOwner.Name, opts, nil); err != nil {
migratedRepo, err := migrations.MigrateRepository(ctx, doer, repoOwner.Name, opts, nil)
if err != nil {
return nil, err
}
notify_service.MigrateRepository(ctx, doer, repoOwner, migratedRepo)
return migratedRepo, nil
}
// use a background context, don't cancel the migration even if the client goes away
// HammerContext doesn't seem right (from https://github.com/go-gitea/gitea/pull/9335/files)
// There are other abuses, maybe most HammerContext abuses should be fixed together in the future.
migratedRepo, err := doLongTimeMigrate(graceful.GetManager().HammerContext(), ctx.Doer)
if err != nil {
handleMigrateError(ctx, repoOwner, err)
return
}
log.Trace("Repository migrated: %s/%s", repoOwner.Name, form.RepoName)
ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeAdmin}))
ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, migratedRepo, access_model.Permission{AccessMode: perm.AccessModeAdmin}))
}
func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err error) {

View File

@@ -432,6 +432,7 @@ func Rerun(ctx *context_module.Context) {
run.PreviousDuration = run.Duration()
run.Started = 0
run.Stopped = 0
run.Status = actions_model.StatusWaiting
vars, err := actions_model.GetVariablesOfRun(ctx, run)
if err != nil {

View File

@@ -7,11 +7,10 @@ package repo
import (
"bytes"
"compress/gzip"
gocontext "context"
"fmt"
"net/http"
"os"
"path/filepath"
"path"
"regexp"
"slices"
"strconv"
@@ -27,6 +26,7 @@ import (
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
@@ -342,11 +342,11 @@ type serviceHandler struct {
environ []string
}
func (h *serviceHandler) getRepoDir() string {
func (h *serviceHandler) getStorageRepo() gitrepo.Repository {
if h.isWiki {
return h.repo.WikiPath()
return h.repo.WikiStorageRepo()
}
return h.repo.RepoPath()
return h.repo
}
func setHeaderNoCache(ctx *context.Context) {
@@ -378,19 +378,10 @@ func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
reqFile := filepath.Join(h.getRepoDir(), filepath.Clean(file))
fi, err := os.Stat(reqFile)
if os.IsNotExist(err) {
ctx.Resp.WriteHeader(http.StatusNotFound)
return
}
fs := gitrepo.GetRepoFS(h.getStorageRepo())
ctx.Resp.Header().Set("Content-Type", contentType)
ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10))
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
ctx.Resp.Header().Set("Last-Modified", fi.ModTime().UTC().Format(http.TimeFormat))
http.ServeFile(ctx.Resp, ctx.Req, reqFile)
http.ServeFileFS(ctx.Resp, ctx.Req, fs, path.Clean(file))
}
// one or more key=value pairs separated by colons
@@ -416,6 +407,7 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
expectedContentType := fmt.Sprintf("application/x-git-%s-request", service)
if ctx.Req.Header.Get("Content-Type") != expectedContentType {
log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
// FIXME: why it's 401 if the content type is unexpected?
ctx.Resp.WriteHeader(http.StatusUnauthorized)
return
}
@@ -423,6 +415,7 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
cmd, err := prepareGitCmdWithAllowedService(service)
if err != nil {
log.Error("Failed to prepareGitCmdWithService: %v", err)
// FIXME: why it's 401 if the service type doesn't supported?
ctx.Resp.WriteHeader(http.StatusUnauthorized)
return
}
@@ -449,17 +442,14 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
}
var stderr bytes.Buffer
if err := cmd.AddArguments("--stateless-rpc").
AddDynamicArguments(h.getRepoDir()).
WithDir(h.getRepoDir()).
if err := gitrepo.RunCmd(ctx, h.getStorageRepo(), cmd.AddArguments("--stateless-rpc", ".").
WithEnv(append(os.Environ(), h.environ...)).
WithStderr(&stderr).
WithStdin(reqBody).
WithStdout(ctx.Resp).
WithUseContextTimeout(true).
Run(ctx); err != nil {
WithUseContextTimeout(true)); err != nil {
if !git.IsErrCanceledOrKilled(err) {
log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.getRepoDir(), err, stderr.String())
log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.getStorageRepo().RelativePath(), err, stderr.String())
}
return
}
@@ -496,14 +486,6 @@ func getServiceType(ctx *context.Context) string {
return ""
}
func updateServerInfo(ctx gocontext.Context, dir string) []byte {
out, _, err := gitcmd.NewCommand("update-server-info").WithDir(dir).RunStdBytes(ctx)
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(out)))
}
return out
}
func packetWrite(str string) []byte {
s := strconv.FormatInt(int64(len(str)+4), 16)
if len(s)%4 != 0 {
@@ -527,10 +509,8 @@ func GetInfoRefs(ctx *context.Context) {
}
h.environ = append(os.Environ(), h.environ...)
refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").
WithEnv(h.environ).
WithDir(h.getRepoDir()).
RunStdBytes(ctx)
refs, _, err := gitrepo.RunCmdBytes(ctx, h.getStorageRepo(), cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").
WithEnv(h.environ))
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
}
@@ -541,7 +521,9 @@ func GetInfoRefs(ctx *context.Context) {
_, _ = ctx.Resp.Write([]byte("0000"))
_, _ = ctx.Resp.Write(refs)
} else {
updateServerInfo(ctx, h.getRepoDir())
if err := gitrepo.UpdateServerInfo(ctx, h.getStorageRepo()); err != nil {
log.Error("Failed to update server info: %v", err)
}
h.sendFile(ctx, "text/plain; charset=utf-8", "info/refs")
}
}

View File

@@ -131,7 +131,7 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
ctx.Data["Issue"] = issue
if !issue.IsPull {
ctx.NotFound(nil)
ctx.Redirect(issue.Link())
return nil, false
}

View File

@@ -29,7 +29,6 @@ import (
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
actions_service "code.gitea.io/gitea/services/actions"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"
@@ -62,7 +61,7 @@ func SettingsCtxData(ctx *context.Context) {
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner)
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
signing, _ := gitrepo.GetSigningKey(ctx, ctx.Repo.Repository)
ctx.Data["SigningKeyAvailable"] = signing != nil
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
@@ -105,7 +104,7 @@ func SettingsPost(ctx *context.Context) {
ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
signing, _ := gitrepo.GetSigningKey(ctx, ctx.Repo.Repository)
ctx.Data["SigningKeyAvailable"] = signing != nil
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled

View File

@@ -5,10 +5,8 @@ package actions
import (
"context"
"regexp"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
secret_service "code.gitea.io/gitea/services/secrets"
)
@@ -18,10 +16,6 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data, desc
return nil, err
}
if err := envNameCIRegexMatch(name); err != nil {
return nil, err
}
v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data), description)
if err != nil {
return nil, err
@@ -35,10 +29,6 @@ func UpdateVariableNameData(ctx context.Context, variable *actions_model.ActionV
return false, err
}
if err := envNameCIRegexMatch(variable.Name); err != nil {
return false, err
}
variable.Data = util.ReserveLineBreakForTextarea(variable.Data)
return actions_model.UpdateVariableCols(ctx, variable, "name", "data", "description")
@@ -49,14 +39,6 @@ func DeleteVariableByID(ctx context.Context, variableID int64) error {
}
func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error {
if err := secret_service.ValidateName(name); err != nil {
return err
}
if err := envNameCIRegexMatch(name); err != nil {
return err
}
v, err := GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ownerID,
RepoID: repoID,
@@ -79,19 +61,3 @@ func GetVariable(ctx context.Context, opts actions_model.FindVariablesOpts) (*ac
}
return vars[0], nil
}
// some regular expression of `variables` and `secrets`
// reference to:
// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
var (
forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
)
func envNameCIRegexMatch(name string) error {
if forbiddenEnvNameCIRx.MatchString(name) {
log.Error("Env Name cannot be ci")
return util.NewInvalidArgumentErrorf("env name cannot be ci")
}
return nil
}

View File

@@ -17,7 +17,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
@@ -109,54 +108,9 @@ func IsErrWontSign(err error) bool {
return ok
}
// SigningKey returns the KeyID and git Signature for the repo
func SigningKey(ctx context.Context, repoPath string) (*git.SigningKey, *git.Signature) {
if setting.Repository.Signing.SigningKey == "none" {
return nil, nil
}
if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
// Can ignore the error here as it means that commit.gpgsign is not set
value, _, _ := gitcmd.NewCommand("config", "--get", "commit.gpgsign").WithDir(repoPath).RunStdString(ctx)
sign, valid := git.ParseBool(strings.TrimSpace(value))
if !sign || !valid {
return nil, nil
}
format, _, _ := gitcmd.NewCommand("config", "--default", git.SigningKeyFormatOpenPGP, "--get", "gpg.format").WithDir(repoPath).RunStdString(ctx)
signingKey, _, _ := gitcmd.NewCommand("config", "--get", "user.signingkey").WithDir(repoPath).RunStdString(ctx)
signingName, _, _ := gitcmd.NewCommand("config", "--get", "user.name").WithDir(repoPath).RunStdString(ctx)
signingEmail, _, _ := gitcmd.NewCommand("config", "--get", "user.email").WithDir(repoPath).RunStdString(ctx)
if strings.TrimSpace(signingKey) == "" {
return nil, nil
}
return &git.SigningKey{
KeyID: strings.TrimSpace(signingKey),
Format: strings.TrimSpace(format),
}, &git.Signature{
Name: strings.TrimSpace(signingName),
Email: strings.TrimSpace(signingEmail),
}
}
if setting.Repository.Signing.SigningKey == "" {
return nil, nil
}
return &git.SigningKey{
KeyID: setting.Repository.Signing.SigningKey,
Format: setting.Repository.Signing.SigningFormat,
}, &git.Signature{
Name: setting.Repository.Signing.SigningName,
Email: setting.Repository.Signing.SigningEmail,
}
}
// PublicSigningKey gets the public signing key within a provided repository directory
func PublicSigningKey(ctx context.Context, repoPath string) (content, format string, err error) {
signingKey, _ := SigningKey(ctx, repoPath)
signingKey, _ := git.GetSigningKey(ctx, repoPath)
if signingKey == nil {
return "", "", nil
}
@@ -181,7 +135,7 @@ func PublicSigningKey(ctx context.Context, repoPath string) (content, format str
// SignInitialCommit determines if we should sign the initial commit to this repository
func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
signingKey, sig := SigningKey(ctx, repoPath)
signingKey, sig := git.GetSigningKey(ctx, repoPath)
if signingKey == nil {
return false, nil, nil, &ErrWontSign{noKey}
}
@@ -216,9 +170,8 @@ Loop:
// SignWikiCommit determines if we should sign the commits to this repository wiki
func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
repoWikiPath := repo.WikiPath()
rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
signingKey, sig := SigningKey(ctx, repoWikiPath)
signingKey, sig := gitrepo.GetSigningKey(ctx, repo.WikiStorageRepo())
if signingKey == nil {
return false, nil, nil, &ErrWontSign{noKey}
}
@@ -271,7 +224,7 @@ Loop:
// SignCRUDAction determines if we should sign a CRUD commit to this repository
func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, *git.SigningKey, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
signingKey, sig := SigningKey(ctx, repoPath)
signingKey, sig := git.GetSigningKey(ctx, repoPath)
if signingKey == nil {
return false, nil, nil, &ErrWontSign{noKey}
}
@@ -335,7 +288,7 @@ func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.
}
repo := pr.BaseRepo
signingKey, signer := SigningKey(ctx, repo.RepoPath())
signingKey, signer := gitrepo.GetSigningKey(ctx, repo)
if signingKey == nil {
return false, nil, nil, &ErrWontSign{noKey}
}

View File

@@ -249,8 +249,6 @@ func checkRecoverableSyncError(stderrMessage string) bool {
// runSync returns true if sync finished without error.
func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bool) {
repoPath := m.Repo.RepoPath()
wikiPath := m.Repo.WikiPath()
timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
log.Trace("SyncMirrors [repo: %-v]: running git remote update...", m.Repo)
@@ -311,7 +309,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
// If there is still an error (or there always was an error)
if err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to update mirror repository:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, stderrMessage)
desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", m.Repo.RelativePath(), stderrMessage)
if err = system_model.CreateRepositoryNotice(desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err)
}
@@ -320,7 +318,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
}
output := stderrBuilder.String()
if err := git.WriteCommitGraph(ctx, repoPath); err != nil {
if err := gitrepo.WriteCommitGraph(ctx, m.Repo); err != nil {
log.Error("SyncMirrors [repo: %-v]: %v", m.Repo, err)
}
@@ -394,14 +392,14 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
// If there is still an error (or there always was an error)
if err != nil {
log.Error("SyncMirrors [repo: %-v Wiki]: failed to update mirror repository wiki:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", wikiPath, stderrMessage)
desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", m.Repo.WikiStorageRepo().RelativePath(), stderrMessage)
if err = system_model.CreateRepositoryNotice(desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err)
}
return nil, false
}
if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
if err := gitrepo.WriteCommitGraph(ctx, m.Repo.WikiStorageRepo()); err != nil {
log.Error("SyncMirrors [repo: %-v]: %v", m.Repo, err)
}
}

View File

@@ -124,14 +124,12 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
performPush := func(repo *repo_model.Repository, isWiki bool) error {
var storageRepo gitrepo.Repository = repo
path := repo.RepoPath()
if isWiki {
storageRepo = repo.WikiStorageRepo()
path = repo.WikiPath()
}
remoteURL, err := gitrepo.GitRemoteGetURL(ctx, storageRepo, m.RemoteName)
if err != nil {
log.Error("GetRemoteURL(%s) Error %v", path, err)
log.Error("GetRemoteURL(%s) Error %v", storageRepo.RelativePath(), err)
return errors.New("Unexpected error")
}
@@ -152,17 +150,17 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
}
}
log.Trace("Pushing %s mirror[%d] remote %s", path, m.ID, m.RemoteName)
log.Trace("Pushing %s mirror[%d] remote %s", storageRepo.RelativePath(), m.ID, m.RemoteName)
envs := proxy.EnvWithProxy(remoteURL.URL)
if err := git.Push(ctx, path, git.PushOptions{
if err := gitrepo.Push(ctx, storageRepo, git.PushOptions{
Remote: m.RemoteName,
Force: true,
Mirror: true,
Timeout: timeout,
Env: envs,
}); err != nil {
log.Error("Error pushing %s mirror[%d] remote %s: %v", path, m.ID, m.RemoteName, err)
log.Error("Error pushing %s mirror[%d] remote %s: %v", storageRepo.RelativePath(), m.ID, m.RemoteName, err)
return util.SanitizeErrorCredentialURLs(err)
}

View File

@@ -28,22 +28,23 @@ import (
)
func cloneWiki(ctx context.Context, repo *repo_model.Repository, opts migration.MigrateOptions, migrateTimeout time.Duration) (string, error) {
wikiPath := repo.WikiPath()
wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr)
if wikiRemotePath == "" {
wikiRemoteURL := repo_module.WikiRemoteURL(ctx, opts.CloneAddr)
if wikiRemoteURL == "" {
return "", nil
}
if err := util.RemoveAll(wikiPath); err != nil {
return "", fmt.Errorf("failed to remove existing wiki dir %q, err: %w", wikiPath, err)
storageRepo := repo.WikiStorageRepo()
if err := gitrepo.DeleteRepository(ctx, storageRepo); err != nil {
return "", fmt.Errorf("failed to remove existing wiki dir %q, err: %w", storageRepo.RelativePath(), err)
}
cleanIncompleteWikiPath := func() {
if err := util.RemoveAll(wikiPath); err != nil {
log.Error("Failed to remove incomplete wiki dir %q, err: %v", wikiPath, err)
if err := gitrepo.DeleteRepository(ctx, storageRepo); err != nil {
log.Error("Failed to remove incomplete wiki dir %q, err: %v", storageRepo.RelativePath(), err)
}
}
if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
if err := gitrepo.CloneExternalRepo(ctx, wikiRemoteURL, storageRepo, git.CloneRepoOptions{
Mirror: true,
Quiet: true,
Timeout: migrateTimeout,
@@ -54,15 +55,15 @@ func cloneWiki(ctx context.Context, repo *repo_model.Repository, opts migration.
return "", err
}
if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
if err := gitrepo.WriteCommitGraph(ctx, storageRepo); err != nil {
cleanIncompleteWikiPath()
return "", err
}
defaultBranch, err := gitrepo.GetDefaultBranch(ctx, repo.WikiStorageRepo())
defaultBranch, err := gitrepo.GetDefaultBranch(ctx, storageRepo)
if err != nil {
cleanIncompleteWikiPath()
return "", fmt.Errorf("failed to get wiki repo default branch for %q, err: %w", wikiPath, err)
return "", fmt.Errorf("failed to get wiki repo default branch for %q, err: %w", storageRepo.RelativePath(), err)
}
return defaultBranch, nil

View File

@@ -107,16 +107,18 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
}
if repoRenamed {
if err := util.Rename(repo_model.RepoPath(newOwnerName, repo.Name), repo_model.RepoPath(oldOwnerName, repo.Name)); err != nil {
oldRelativePath, newRelativePath := repo_model.RelativePath(newOwnerName, repo.Name), repo_model.RelativePath(oldOwnerName, repo.Name)
if err := gitrepo.RenameRepository(ctx, repo_model.StorageRepo(oldRelativePath), repo_model.StorageRepo(newRelativePath)); err != nil {
log.Critical("Unable to move repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name,
repo_model.RepoPath(newOwnerName, repo.Name), repo_model.RepoPath(oldOwnerName, repo.Name), err)
oldRelativePath, newRelativePath, err)
}
}
if wikiRenamed {
if err := util.Rename(repo_model.WikiPath(newOwnerName, repo.Name), repo_model.WikiPath(oldOwnerName, repo.Name)); err != nil {
oldRelativePath, newRelativePath := repo_model.RelativeWikiPath(newOwnerName, repo.Name), repo_model.RelativeWikiPath(oldOwnerName, repo.Name)
if err := gitrepo.RenameRepository(ctx, repo_model.StorageRepo(oldRelativePath), repo_model.StorageRepo(newRelativePath)); err != nil {
log.Critical("Unable to move wiki for repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name,
repo_model.WikiPath(newOwnerName, repo.Name), repo_model.WikiPath(oldOwnerName, repo.Name), err)
oldRelativePath, newRelativePath, err)
}
}
@@ -289,12 +291,12 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
repoRenamed = true
// Rename remote wiki repository to new path and delete local copy.
wikiPath := repo_model.WikiPath(oldOwner.Name, repo.Name)
if isExist, err := util.IsExist(wikiPath); err != nil {
log.Error("Unable to check if %s exists. Error: %v", wikiPath, err)
wikiStorageRepo := repo_model.StorageRepo(repo_model.RelativeWikiPath(oldOwner.Name, repo.Name))
if isExist, err := gitrepo.IsRepositoryExist(ctx, wikiStorageRepo); err != nil {
log.Error("Unable to check if %s exists. Error: %v", wikiStorageRepo.RelativePath(), err)
return err
} else if isExist {
if err := util.Rename(wikiPath, repo_model.WikiPath(newOwner.Name, repo.Name)); err != nil {
if err := gitrepo.RenameRepository(ctx, wikiStorageRepo, repo_model.StorageRepo(repo_model.RelativeWikiPath(newOwner.Name, repo.Name))); err != nil {
return fmt.Errorf("rename repository wiki: %w", err)
}
wikiRenamed = true

View File

@@ -56,10 +56,6 @@ func DeleteSecretByID(ctx context.Context, ownerID, repoID, secretID int64) erro
}
func DeleteSecretByName(ctx context.Context, ownerID, repoID int64, name string) error {
if err := ValidateName(name); err != nil {
return err
}
s, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{
OwnerID: ownerID,
RepoID: repoID,

View File

@@ -5,21 +5,29 @@ package secrets
import (
"regexp"
"strings"
"sync"
"code.gitea.io/gitea/modules/util"
)
// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
var (
namePattern = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$")
forbiddenPrefixPattern = regexp.MustCompile("(?i)^GIT(EA|HUB)_")
ErrInvalidName = util.NewInvalidArgumentErrorf("invalid secret name")
)
var globalVars = sync.OnceValue(func() (ret struct {
namePattern, forbiddenPrefixPattern *regexp.Regexp
},
) {
ret.namePattern = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$")
ret.forbiddenPrefixPattern = regexp.MustCompile("(?i)^GIT(EA|HUB)_")
return ret
})
func ValidateName(name string) error {
if !namePattern.MatchString(name) || forbiddenPrefixPattern.MatchString(name) {
return ErrInvalidName
vars := globalVars()
if !vars.namePattern.MatchString(name) ||
vars.forbiddenPrefixPattern.MatchString(name) ||
strings.EqualFold(name, "CI") /* CI is always set to true in GitHub Actions*/ {
return util.NewInvalidArgumentErrorf("invalid variable or secret name")
}
return nil
}

View File

@@ -0,0 +1,29 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package secrets
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidateName(t *testing.T) {
cases := []struct {
name string
valid bool
}{
{"FOO", true},
{"FOO1_BAR2", true},
{"_FOO", true}, // really? why support this
{"1FOO", false},
{"giteA_xx", false},
{"githuB_xx", false},
{"cI", false},
}
for _, c := range cases {
err := ValidateName(c.name)
assert.Equal(t, c.valid, err == nil, "ValidateName(%q)", c.name)
}
}

View File

@@ -120,7 +120,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
cloneOpts.Branch = repo.DefaultWikiBranch
}
if err := git.Clone(ctx, repo.WikiPath(), basePath, cloneOpts); err != nil {
if err := gitrepo.CloneRepoToLocal(ctx, repo.WikiStorageRepo(), basePath, cloneOpts); err != nil {
log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err)
}
@@ -269,7 +269,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
}
defer cleanup()
if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{
if err := gitrepo.CloneRepoToLocal(ctx, repo.WikiStorageRepo(), basePath, git.CloneRepoOptions{
Bare: true,
Shared: true,
Branch: repo.DefaultWikiBranch,

View File

@@ -32,10 +32,10 @@
<ul>
{{if not .DisableDownloadSourceArchives}}
<li>
<a href="{{.Release.Repo.Link}}/archive/{{.Release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{.locale.Tr "mail.release.download.zip"}}</strong></a>
<a href="{{.Release.Repo.HTMLURL}}/archive/{{.Release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{.locale.Tr "mail.release.download.zip"}}</strong></a>
</li>
<li>
<a href="{{.Release.Repo.Link}}/archive/{{.Release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{.locale.Tr "mail.release.download.targz"}}</strong></a>
<a href="{{.Release.Repo.HTMLURL}}/archive/{{.Release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{.locale.Tr "mail.release.download.targz"}}</strong></a>
</li>
{{end}}
{{if .Release.Attachments}}

View File

@@ -131,9 +131,5 @@ func TestAPIRepoSecrets(t *testing.T) {
req = NewRequest(t, "DELETE", url).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/secrets/000", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusBadRequest)
})
}

View File

@@ -92,9 +92,5 @@ func TestAPIUserSecrets(t *testing.T) {
req = NewRequest(t, "DELETE", url).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "DELETE", "/api/v1/user/actions/secrets/000").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusBadRequest)
})
}

View File

@@ -497,9 +497,13 @@ func TestIssueRedirect(t *testing.T) {
resp = session.MakeRequest(t, req, http.StatusSeeOther)
assert.Equal(t, "/user2/repo1/pulls/2", test.RedirectURL(resp))
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: 1, Type: unit.TypeIssues})
// by the way, test PR redirection
req = NewRequest(t, "GET", "/user2/repo1/pulls/1")
resp = session.MakeRequest(t, req, http.StatusSeeOther)
assert.Equal(t, "/user2/repo1/issues/1", test.RedirectURL(resp))
// disable issue unit, it will be reset
// disable issue unit
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: 1, Type: unit.TypeIssues})
_, err := db.DeleteByID[repo_model.RepoUnit](t.Context(), repoUnit.ID)
assert.NoError(t, err)

View File

@@ -1418,7 +1418,7 @@ jobs:
assert.Equal(t, "user2/repo1", webhookData.payloads[1].Repo.FullName)
// Call rerun ui api
// Only a web UI API exists for cancelling workflow runs, so use the UI endpoint.
// Only a web UI API exists for rerunning workflow runs, so use the UI endpoint.
rerunURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/rerun", webhookData.payloads[0].WorkflowRun.RunNumber)
req = NewRequestWithValues(t, "POST", rerunURL, map[string]string{
"_csrf": GetUserCSRFToken(t, session),
@@ -1426,6 +1426,15 @@ jobs:
session.MakeRequest(t, req, http.StatusOK)
assert.Len(t, webhookData.payloads, 3)
// 5. Validate the third webhook payload
assert.Equal(t, "workflow_run", webhookData.triggeredEvent)
assert.Equal(t, "requested", webhookData.payloads[2].Action)
assert.Equal(t, "queued", webhookData.payloads[2].WorkflowRun.Status)
assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[2].WorkflowRun.HeadBranch)
assert.Equal(t, commitID, webhookData.payloads[2].WorkflowRun.HeadSha)
assert.Equal(t, "repo1", webhookData.payloads[2].Repo.Name)
assert.Equal(t, "user2/repo1", webhookData.payloads[2].Repo.FullName)
}
func testWorkflowRunEventsOnCancellingAbandonedRun(t *testing.T, webhookData *workflowRunWebhook, allJobsAbandoned bool) {