mirror of
https://github.com/go-gitea/gitea.git
synced 2026-02-24 06:13:00 +01:00
Merge branch 'main' into lunny/project_workflow
This commit is contained in:
commit
4c4a16ccb5
@ -121,6 +121,12 @@ func globalBool(c *cli.Command, name string) bool {
|
||||
// Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever.
|
||||
func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cli.Command) (context.Context, error) {
|
||||
return func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
||||
if setting.InstallLock {
|
||||
// During config loading, there might also be logs (for example: deprecation warnings).
|
||||
// It must make sure that console logger is set up before config is loaded.
|
||||
log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it.")
|
||||
return nil, errors.New("console logger must be setup before config is loaded")
|
||||
}
|
||||
level := defaultLevel
|
||||
if globalBool(c, "quiet") {
|
||||
level = log.FATAL
|
||||
|
||||
@ -19,7 +19,7 @@ import (
|
||||
var CmdKeys = &cli.Command{
|
||||
Name: "keys",
|
||||
Usage: "(internal) Should only be called by SSH server",
|
||||
Hidden: true, // internal commands shouldn't not be visible
|
||||
Hidden: true, // internal commands shouldn't be visible
|
||||
Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint",
|
||||
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
||||
Action: runKeys,
|
||||
|
||||
10
cmd/main.go
10
cmd/main.go
@ -50,11 +50,15 @@ DEFAULT CONFIGURATION:
|
||||
|
||||
func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) {
|
||||
originBefore := originCmd.Before
|
||||
originCmd.Before = func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
||||
prepareWorkPathAndCustomConf(cmd)
|
||||
originCmd.Before = func(ctxOrig context.Context, cmd *cli.Command) (ctx context.Context, err error) {
|
||||
ctx = ctxOrig
|
||||
if originBefore != nil {
|
||||
return originBefore(ctx, cmd)
|
||||
ctx, err = originBefore(ctx, cmd)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
}
|
||||
prepareWorkPathAndCustomConf(cmd)
|
||||
return ctx, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
@ -28,11 +29,11 @@ func makePathOutput(workPath, customPath, customConf string) string {
|
||||
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
|
||||
}
|
||||
|
||||
func newTestApp(testCmdAction cli.ActionFunc) *cli.Command {
|
||||
func newTestApp(testCmd cli.Command) *cli.Command {
|
||||
app := NewMainApp(AppVersion{})
|
||||
testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction}
|
||||
prepareSubcommandWithGlobalFlags(testCmd)
|
||||
app.Commands = append(app.Commands, testCmd)
|
||||
testCmd.Name = util.IfZero(testCmd.Name, "test-cmd")
|
||||
prepareSubcommandWithGlobalFlags(&testCmd)
|
||||
app.Commands = append(app.Commands, &testCmd)
|
||||
app.DefaultCommand = testCmd.Name
|
||||
return app
|
||||
}
|
||||
@ -156,9 +157,11 @@ func TestCliCmd(t *testing.T) {
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.cmd, func(t *testing.T) {
|
||||
app := newTestApp(func(ctx context.Context, cmd *cli.Command) error {
|
||||
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
||||
return nil
|
||||
app := newTestApp(cli.Command{
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
for k, v := range c.env {
|
||||
t.Setenv(k, v)
|
||||
@ -173,31 +176,54 @@ func TestCliCmd(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCliCmdError(t *testing.T) {
|
||||
app := newTestApp(func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") })
|
||||
app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }})
|
||||
r, err := runTestApp(app, "./gitea", "test-cmd")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "Command error: normal error\n", r.Stderr)
|
||||
|
||||
app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) })
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }})
|
||||
r, err = runTestApp(app, "./gitea", "test-cmd")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 2, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "exit error\n", r.Stderr)
|
||||
|
||||
app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil })
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
|
||||
|
||||
app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil })
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||
r, err = runTestApp(app, "./gitea", "test-cmd")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Empty(t, r.Stderr)
|
||||
}
|
||||
|
||||
func TestCliCmdBefore(t *testing.T) {
|
||||
ctxNew := context.WithValue(context.Background(), any("key"), "value")
|
||||
configValues := map[string]string{}
|
||||
setting.CustomConf = "/tmp/any.ini"
|
||||
var actionCtx context.Context
|
||||
app := newTestApp(cli.Command{
|
||||
Before: func(context.Context, *cli.Command) (context.Context, error) {
|
||||
configValues["before"] = setting.CustomConf
|
||||
return ctxNew, nil
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
configValues["action"] = setting.CustomConf
|
||||
actionCtx = ctx
|
||||
return nil
|
||||
},
|
||||
})
|
||||
_, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ctxNew, actionCtx)
|
||||
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
|
||||
assert.Equal(t, "/dev/null", configValues["action"])
|
||||
}
|
||||
|
||||
@ -924,6 +924,7 @@ export default defineConfig([
|
||||
'vue/html-closing-bracket-spacing': [2, {startTag: 'never', endTag: 'never', selfClosingTag: 'never'}],
|
||||
'vue/max-attributes-per-line': [0],
|
||||
'vue/singleline-html-element-content-newline': [0],
|
||||
'vue/require-typed-ref': [2],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -19,6 +19,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// FIXME: this file shouldn't be in a normal package, it should only be compiled for tests
|
||||
@ -88,6 +89,16 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
|
||||
return x, deferFn
|
||||
}
|
||||
|
||||
func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table {
|
||||
tables, err := x.DBMetas()
|
||||
require.NoError(t, err)
|
||||
tableMap := make(map[string]*schemas.Table)
|
||||
for _, table := range tables {
|
||||
tableMap[table.Name] = table
|
||||
}
|
||||
return tableMap
|
||||
}
|
||||
|
||||
func MainTest(m *testing.M) {
|
||||
testlogger.Init()
|
||||
|
||||
|
||||
@ -380,8 +380,8 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices),
|
||||
newMigration(310, "Add Priority to ProtectedBranch", v1_23.AddPriorityToProtectedBranch),
|
||||
newMigration(311, "Add TimeEstimate to Issue table", v1_23.AddTimeEstimateColumnToIssueTable),
|
||||
|
||||
// Gitea 1.23.0-rc0 ends at migration ID number 311 (database version 312)
|
||||
|
||||
newMigration(312, "Add DeleteBranchAfterMerge to AutoMerge", v1_24.AddDeleteBranchAfterMergeForAutoMerge),
|
||||
newMigration(313, "Move PinOrder from issue table to a new table issue_pin", v1_24.MovePinOrderToTableIssuePin),
|
||||
newMigration(314, "Update OwnerID as zero for repository level action tables", v1_24.UpdateOwnerIDOfRepoLevelActionsTables),
|
||||
@ -391,12 +391,12 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
|
||||
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
|
||||
newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor),
|
||||
// Gitea 1.24.0 ends at migration ID number 320 (database version 321)
|
||||
|
||||
// Gitea 1.24.0 ends at database version 321
|
||||
newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs),
|
||||
newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength),
|
||||
// Gitea 1.25.0 ends at migration ID number 322 (database version 323)
|
||||
|
||||
// Gitea 1.25.0 ends at database version 323
|
||||
newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency),
|
||||
newMigration(324, "Add new table project_workflow", v1_26.AddProjectWorkflow),
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_UseLongTextInSomeColumnsAndFixBugs(t *testing.T) {
|
||||
@ -38,33 +39,26 @@ func Test_UseLongTextInSomeColumnsAndFixBugs(t *testing.T) {
|
||||
type Notice struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type int
|
||||
Description string `xorm:"LONGTEXT"`
|
||||
Description string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
// Prepare and load the testing database
|
||||
x, deferable := base.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice))
|
||||
defer deferable()
|
||||
x, deferrable := base.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice))
|
||||
defer deferrable()
|
||||
|
||||
assert.NoError(t, UseLongTextInSomeColumnsAndFixBugs(x))
|
||||
require.NoError(t, UseLongTextInSomeColumnsAndFixBugs(x))
|
||||
|
||||
tables, err := x.DBMetas()
|
||||
assert.NoError(t, err)
|
||||
tables := base.LoadTableSchemasMap(t, x)
|
||||
table := tables["review_state"]
|
||||
column := table.GetColumn("updated_files")
|
||||
assert.Equal(t, "LONGTEXT", column.SQLType.Name)
|
||||
|
||||
for _, table := range tables {
|
||||
switch table.Name {
|
||||
case "review_state":
|
||||
column := table.GetColumn("updated_files")
|
||||
assert.NotNil(t, column)
|
||||
assert.Equal(t, "LONGTEXT", column.SQLType.Name)
|
||||
case "package_property":
|
||||
column := table.GetColumn("value")
|
||||
assert.NotNil(t, column)
|
||||
assert.Equal(t, "LONGTEXT", column.SQLType.Name)
|
||||
case "notice":
|
||||
column := table.GetColumn("description")
|
||||
assert.NotNil(t, column)
|
||||
assert.Equal(t, "LONGTEXT", column.SQLType.Name)
|
||||
}
|
||||
}
|
||||
table = tables["package_property"]
|
||||
column = table.GetColumn("value")
|
||||
assert.Equal(t, "LONGTEXT", column.SQLType.Name)
|
||||
|
||||
table = tables["notice"]
|
||||
column = table.GetColumn("description")
|
||||
assert.Equal(t, "LONGTEXT", column.SQLType.Name)
|
||||
}
|
||||
|
||||
34
models/migrations/v1_25/v322_test.go
Normal file
34
models/migrations/v1_25/v322_test.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_25
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_ExtendCommentTreePathLength(t *testing.T) {
|
||||
if setting.Database.Type.IsSQLite3() {
|
||||
t.Skip("For SQLITE, varchar or char will always be represented as TEXT")
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
TreePath string `xorm:"VARCHAR(255)"`
|
||||
}
|
||||
|
||||
x, deferrable := base.PrepareTestEnv(t, 0, new(Comment))
|
||||
defer deferrable()
|
||||
|
||||
require.NoError(t, ExtendCommentTreePathLength(x))
|
||||
table := base.LoadTableSchemasMap(t, x)["comment"]
|
||||
column := table.GetColumn("tree_path")
|
||||
assert.Contains(t, []string{"NVARCHAR", "VARCHAR"}, column.SQLType.Name)
|
||||
assert.EqualValues(t, 4000, column.Length)
|
||||
}
|
||||
14
models/migrations/v1_26/main_test.go
Normal file
14
models/migrations/v1_26/main_test.go
Normal file
@ -0,0 +1,14 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
base.MainTest(m)
|
||||
}
|
||||
@ -30,6 +30,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
// Chroma always uses 1-2 letters for style names, we could tolerate it at the moment
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^\w{0,2}$`)).OnElements("span")
|
||||
|
||||
// Line numbers on codepreview
|
||||
policy.AllowAttrs("data-line-number").OnElements("span")
|
||||
|
||||
// Custom URL-Schemes
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
|
||||
|
||||
@ -22,6 +22,8 @@ func GetContextData(c context.Context) reqctx.ContextData {
|
||||
|
||||
func CommonTemplateContextData() reqctx.ContextData {
|
||||
return reqctx.ContextData{
|
||||
"PageTitleCommon": setting.AppName,
|
||||
|
||||
"IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations,
|
||||
|
||||
"ShowRegistrationButton": setting.Service.ShowRegistrationButton,
|
||||
|
||||
@ -1969,6 +1969,9 @@ pulls.status_checks_requested=Requis
|
||||
pulls.status_checks_details=Détails
|
||||
pulls.status_checks_hide_all=Masquer toutes les vérifications
|
||||
pulls.status_checks_show_all=Afficher toutes les vérifications
|
||||
pulls.status_checks_approve_all=Accepter tous les flux de travail
|
||||
pulls.status_checks_need_approvals=%d flux de travail en attente d’approbation
|
||||
pulls.status_checks_need_approvals_helper=Ce flux de travail ne s’exécutera qu’après l’approbation par le mainteneur du dépôt.
|
||||
pulls.update_branch=Actualiser la branche par fusion
|
||||
pulls.update_branch_rebase=Actualiser la branche par rebasage
|
||||
pulls.update_branch_success=La mise à jour de la branche a réussi
|
||||
@ -2434,6 +2437,9 @@ settings.event_workflow_job_desc=Travaux du flux de travail Gitea Actions en fil
|
||||
settings.event_package=Paquet
|
||||
settings.event_package_desc=Paquet créé ou supprimé.
|
||||
settings.branch_filter=Filtre de branche
|
||||
settings.branch_filter_desc_1=Liste de branches et références autorisées pour la soumission, la création et la suppression de branches, sous forme de glob. En utilisant <code>*</code> ou en laissant vide, cela inclue toutes les branches et étiquettes git.
|
||||
settings.branch_filter_desc_2=Utilisez le préfixe <code>refs/heads/</code> ou <code>refs/tags/</code> pour faire correspondre les noms complets des références.
|
||||
settings.branch_filter_desc_doc=Consultez <a href="%[1]s">la documentation %[2]s</a> pour utiliser sa syntaxe.
|
||||
settings.authorization_header=En-tête « Authorization »
|
||||
settings.authorization_header_desc=Si présent, sera ajouté aux requêtes comme en-tête d’authentification. Exemples : %s.
|
||||
settings.active=Actif
|
||||
@ -3729,6 +3735,7 @@ swift.install=Ajoutez le paquet dans votre fichier <code>Package.swift</code>:
|
||||
swift.install2=et exécutez la commande suivante :
|
||||
vagrant.install=Pour ajouter une machine Vagrant, exécutez la commande suivante :
|
||||
settings.link=Lier ce paquet à un dépôt
|
||||
settings.link.description=Si vous associez un paquet à un dépôt, le paquet sera inclus dans sa liste des paquets. Seul les dépôts d’un même propriétaire peuvent être associés. Laisser ce champ vide supprimera le lien.
|
||||
settings.link.select=Sélectionner un dépôt
|
||||
settings.link.button=Actualiser le lien du dépôt
|
||||
settings.link.success=Le lien du dépôt a été mis à jour avec succès.
|
||||
@ -3886,6 +3893,7 @@ workflow.has_workflow_dispatch=Ce flux de travail a un déclencheur d’événem
|
||||
workflow.has_no_workflow_dispatch=Le flux de travail %s n’a pas de déclencheur d’événement workflow_dispatch.
|
||||
|
||||
need_approval_desc=Besoin d’approbation pour exécuter des flux de travail pour une demande d’ajout de bifurcation.
|
||||
approve_all_success=Tous les flux de travail ont été acceptés.
|
||||
|
||||
variables=Variables
|
||||
variables.management=Gestion des variables
|
||||
@ -3906,6 +3914,14 @@ variables.update.success=La variable a bien été modifiée.
|
||||
logs.always_auto_scroll=Toujours faire défiler les journaux automatiquement
|
||||
logs.always_expand_running=Toujours développer les journaux en cours
|
||||
|
||||
general=Général
|
||||
general.enable_actions=Activer les actions
|
||||
general.collaborative_owners_management=Gestion des collaborateurs
|
||||
general.collaborative_owners_management_help=Un collaborateur est un utilisateur ou une organisation dont le dépôt privé peut accéder aux actions et flux de travail de ce dépôt.
|
||||
general.add_collaborative_owner=Ajouter un collaborateur
|
||||
general.collaborative_owner_not_exist=Le collaborateur n’existe pas.
|
||||
general.remove_collaborative_owner=Supprimer le collaborateur
|
||||
general.remove_collaborative_owner_desc=Supprimer un collaborateur empêchera les dépôts de cet utilisateur d’accéder aux actions dans ce dépôt. Continuer ?
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Projet supprimé
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
@ -412,6 +412,12 @@ func Rerun(ctx *context_module.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// rerun is not allowed if the run is not done
|
||||
if !run.Status.IsDone() {
|
||||
ctx.JSONError(ctx.Locale.Tr("actions.runs.not_done"))
|
||||
return
|
||||
}
|
||||
|
||||
// can not rerun job when workflow is disabled
|
||||
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
|
||||
cfg := cfgUnit.ActionsConfig()
|
||||
@ -420,57 +426,53 @@ func Rerun(ctx *context_module.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// check run (workflow-level) concurrency
|
||||
// reset run's start and stop time
|
||||
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 {
|
||||
ctx.ServerError("GetVariablesOfRun", fmt.Errorf("get run %d variables: %w", run.ID, err))
|
||||
return
|
||||
}
|
||||
|
||||
if run.RawConcurrency != "" {
|
||||
var rawConcurrency model.RawConcurrency
|
||||
if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil {
|
||||
ctx.ServerError("UnmarshalRawConcurrency", fmt.Errorf("unmarshal raw concurrency: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = actions_service.EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars)
|
||||
if err != nil {
|
||||
ctx.ServerError("EvaluateRunConcurrencyFillModel", err)
|
||||
return
|
||||
}
|
||||
|
||||
run.Status, err = actions_service.PrepareToStartRunWithConcurrency(ctx, run)
|
||||
if err != nil {
|
||||
ctx.ServerError("PrepareToStartRunWithConcurrency", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil {
|
||||
ctx.ServerError("UpdateRun", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := run.LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("run.LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
|
||||
|
||||
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
// reset run's start and stop time when it is done
|
||||
if run.Status.IsDone() {
|
||||
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 {
|
||||
ctx.ServerError("GetVariablesOfRun", fmt.Errorf("get run %d variables: %w", run.ID, err))
|
||||
return
|
||||
}
|
||||
|
||||
if run.RawConcurrency != "" {
|
||||
var rawConcurrency model.RawConcurrency
|
||||
if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil {
|
||||
ctx.ServerError("UnmarshalRawConcurrency", fmt.Errorf("unmarshal raw concurrency: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = actions_service.EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars)
|
||||
if err != nil {
|
||||
ctx.ServerError("EvaluateRunConcurrencyFillModel", err)
|
||||
return
|
||||
}
|
||||
|
||||
run.Status, err = actions_service.PrepareToStartRunWithConcurrency(ctx, run)
|
||||
if err != nil {
|
||||
ctx.ServerError("PrepareToStartRunWithConcurrency", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil {
|
||||
ctx.ServerError("UpdateRun", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := run.LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("run.LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
|
||||
}
|
||||
|
||||
isRunBlocked := run.Status == actions_model.StatusBlocked
|
||||
if jobIndexStr == "" { // rerun all jobs
|
||||
for _, j := range jobs {
|
||||
@ -501,7 +503,7 @@ func Rerun(ctx *context_module.Context) {
|
||||
|
||||
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
|
||||
status := job.Status
|
||||
if !status.IsDone() || !job.Run.Status.IsDone() {
|
||||
if !status.IsDone() {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -172,7 +172,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
|
||||
|
||||
blob := entry.Blob()
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
|
||||
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+ctx.Repo.TreePath, ctx.Repo.RefFullName.ShortName())
|
||||
ctx.Data["FileIsSymlink"] = entry.IsLink()
|
||||
ctx.Data["FileTreePath"] = ctx.Repo.TreePath
|
||||
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
||||
|
||||
@ -7,7 +7,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -146,7 +145,7 @@ func prepareToRenderDirectory(ctx *context.Context) {
|
||||
|
||||
if ctx.Repo.TreePath != "" {
|
||||
ctx.Data["HideRepoInfo"] = true
|
||||
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
|
||||
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+ctx.Repo.TreePath, ctx.Repo.RefFullName.ShortName())
|
||||
}
|
||||
|
||||
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, ctx.Repo.TreePath, entries, true)
|
||||
|
||||
@ -234,12 +234,12 @@ func notify(ctx context.Context, input *notifyInput) error {
|
||||
}
|
||||
|
||||
if shouldDetectSchedules {
|
||||
if err := handleSchedules(ctx, schedules, commit, input, ref.String()); err != nil {
|
||||
if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return handleWorkflows(ctx, detectedWorkflows, commit, input, ref.String())
|
||||
return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)
|
||||
}
|
||||
|
||||
func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) bool {
|
||||
@ -291,7 +291,7 @@ func handleWorkflows(
|
||||
detectedWorkflows []*actions_module.DetectedWorkflow,
|
||||
commit *git.Commit,
|
||||
input *notifyInput,
|
||||
ref string,
|
||||
ref git.RefName,
|
||||
) error {
|
||||
if len(detectedWorkflows) == 0 {
|
||||
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RelativePath(), commit.ID)
|
||||
@ -327,7 +327,7 @@ func handleWorkflows(
|
||||
WorkflowID: dwf.EntryName,
|
||||
TriggerUserID: input.Doer.ID,
|
||||
TriggerUser: input.Doer,
|
||||
Ref: ref,
|
||||
Ref: ref.String(),
|
||||
CommitSHA: commit.ID.String(),
|
||||
IsForkPullRequest: isForkPullRequest,
|
||||
Event: input.Event,
|
||||
@ -442,13 +442,9 @@ func handleSchedules(
|
||||
detectedWorkflows []*actions_module.DetectedWorkflow,
|
||||
commit *git.Commit,
|
||||
input *notifyInput,
|
||||
ref string,
|
||||
ref git.RefName,
|
||||
) error {
|
||||
branch, err := commit.GetBranchName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if branch != input.Repo.DefaultBranch {
|
||||
if ref.BranchName() != input.Repo.DefaultBranch {
|
||||
log.Trace("commit branch is not default branch in repo")
|
||||
return nil
|
||||
}
|
||||
@ -494,7 +490,7 @@ func handleSchedules(
|
||||
WorkflowID: dwf.EntryName,
|
||||
TriggerUserID: user_model.ActionsUserID,
|
||||
TriggerUser: user_model.NewActionsUser(),
|
||||
Ref: ref,
|
||||
Ref: ref.String(),
|
||||
CommitSHA: commit.ID.String(),
|
||||
Event: input.Event,
|
||||
EventPayload: string(p),
|
||||
@ -538,5 +534,5 @@ func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository)
|
||||
// so we use action user as the Doer of the notifyInput
|
||||
notifyInput := newNotifyInputForSchedules(repo)
|
||||
|
||||
return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, repo.DefaultBranch)
|
||||
return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, git.RefNameFromBranch(repo.DefaultBranch))
|
||||
}
|
||||
|
||||
@ -537,6 +537,7 @@ func RepoAssignment(ctx *Context) {
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = repo.Owner.Name + "/" + repo.Name
|
||||
ctx.Data["PageTitleCommon"] = repo.Name + " - " + setting.AppName
|
||||
ctx.Data["Repository"] = repo
|
||||
ctx.Data["Owner"] = ctx.Repo.Repository.Owner
|
||||
ctx.Data["CanWriteCode"] = ctx.Repo.CanWrite(unit_model.TypeCode)
|
||||
|
||||
@ -110,6 +110,7 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie
|
||||
"FilePath": opts.FilePath,
|
||||
"LineStart": opts.LineStart,
|
||||
"LineStop": realLineStop,
|
||||
"RepoName": opts.RepoName,
|
||||
"RepoLink": dbRepo.Link(),
|
||||
"CommitID": opts.CommitID,
|
||||
"HighlightLines": highlightLines,
|
||||
|
||||
@ -24,15 +24,15 @@ func TestRenderHelperCodePreview(t *testing.T) {
|
||||
OwnerName: "user2",
|
||||
RepoName: "repo1",
|
||||
CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
||||
FilePath: "/README.md",
|
||||
FilePath: "README.md",
|
||||
LineStart: 1,
|
||||
LineStop: 2,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<div class="code-preview-container file-content">
|
||||
<div class="code-preview-header">
|
||||
<a href="http://full" class="muted" rel="nofollow">/README.md</a>
|
||||
repo.code_preview_line_from_to:1,2,<a href="/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow">65f1bf27bc</a>
|
||||
<a href="http://full" class="tw-font-semibold" rel="nofollow">repo1/README.md</a>
|
||||
repo.code_preview_line_from_to:1,2,<a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" class="muted tw-font-mono tw-text-text" rel="nofollow">65f1bf27bc</a>
|
||||
</div>
|
||||
<table class="file-view">
|
||||
<tbody><tr>
|
||||
@ -52,14 +52,14 @@ func TestRenderHelperCodePreview(t *testing.T) {
|
||||
OwnerName: "user2",
|
||||
RepoName: "repo1",
|
||||
CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
||||
FilePath: "/README.md",
|
||||
FilePath: "README.md",
|
||||
LineStart: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<div class="code-preview-container file-content">
|
||||
<div class="code-preview-header">
|
||||
<a href="http://full" class="muted" rel="nofollow">/README.md</a>
|
||||
repo.code_preview_line_in:1,<a href="/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow">65f1bf27bc</a>
|
||||
<a href="http://full" class="tw-font-semibold" rel="nofollow">repo1/README.md</a>
|
||||
repo.code_preview_line_in:1,<a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" class="muted tw-font-mono tw-text-text" rel="nofollow">65f1bf27bc</a>
|
||||
</div>
|
||||
<table class="file-view">
|
||||
<tbody><tr>
|
||||
@ -76,7 +76,7 @@ func TestRenderHelperCodePreview(t *testing.T) {
|
||||
OwnerName: "user15",
|
||||
RepoName: "big_test_private_1",
|
||||
CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
||||
FilePath: "/README.md",
|
||||
FilePath: "README.md",
|
||||
LineStart: 1,
|
||||
LineStop: 10,
|
||||
})
|
||||
|
||||
@ -68,6 +68,7 @@ parts:
|
||||
override-build: |
|
||||
set -x
|
||||
sed -i 's/os.Getuid()/1/g' modules/setting/setting.go
|
||||
npm install -g pnpm
|
||||
TAGS="bindata sqlite sqlite_unlock_notify pam cert" make build
|
||||
install -D gitea "${SNAPCRAFT_PART_INSTALL}/gitea"
|
||||
cp -r options "${SNAPCRAFT_PART_INSTALL}/"
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<html lang="{{ctx.Locale.Lang}}" data-theme="{{ctx.CurrentWebTheme.InternalName}}">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{if .Title}}{{.Title}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title>
|
||||
<title>{{if .Title}}{{.Title}} - {{end}}{{.PageTitleCommon}}</title>
|
||||
{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
|
||||
<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">
|
||||
<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<div class="code-preview-container file-content">
|
||||
<div class="code-preview-header">
|
||||
<a href="{{.FullURL}}" class="muted" rel="nofollow">{{.FilePath}}</a>
|
||||
{{$link := HTMLFormat `<a href="%s/src/commit/%s" rel="nofollow">%s</a>` .RepoLink .CommitID (.CommitID | ShortSha) -}}
|
||||
<a href="{{.FullURL}}" class="tw-font-semibold" rel="nofollow">{{.RepoName}}/{{.FilePath}}</a>
|
||||
{{$link := HTMLFormat `<a href="%s/commit/%s" class="muted tw-font-mono tw-text-text" rel="nofollow">%s</a>` .RepoLink .CommitID (.CommitID | ShortSha) -}}
|
||||
{{- if eq .LineStart .LineStop -}}
|
||||
{{ctx.Locale.Tr "repo.code_preview_line_in" .LineStart $link}}
|
||||
{{- else -}}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{{$isTreePathRoot := not .TreeNames}}
|
||||
|
||||
<div class="repo-view-content-data tw-hidden" data-document-title="{{ctx.RootData.Title}}" data-document-title-common="{{ctx.RootData.PageTitleCommon}}"></div>
|
||||
{{template "repo/sub_menu" .}}
|
||||
<div class="repo-button-row">
|
||||
<div class="repo-button-row-left">
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
{{if eq .HookType "gitea"}}
|
||||
{{svg "gitea-gitea" $size "img"}}
|
||||
{{else if eq .HookType "gogs"}}
|
||||
<img alt width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gogs.ico">
|
||||
<img alt width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gogs.png">
|
||||
{{else if eq .HookType "slack"}}
|
||||
<img alt width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/slack.png">
|
||||
{{else if eq .HookType "discord"}}
|
||||
|
||||
118
tests/integration/actions_rerun_test.go
Normal file
118
tests/integration/actions_rerun_test.go
Normal file
@ -0,0 +1,118 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
)
|
||||
|
||||
func TestActionsRerun(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-rerun", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(httpContext)(t)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
wfTreePath := ".gitea/workflows/actions-rerun-workflow-1.yml"
|
||||
wfFileContent := `name: actions-rerun-workflow-1
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/actions-rerun-workflow-1.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'job1'
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo 'job2'
|
||||
`
|
||||
|
||||
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create"+wfTreePath, wfFileContent)
|
||||
createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts)
|
||||
|
||||
// fetch and exec job1
|
||||
job1Task := runner.fetchTask(t)
|
||||
_, _, run := getTaskAndJobAndRunByTaskID(t, job1Task.Id)
|
||||
runner.execTask(t, job1Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
// RERUN-FAILURE: the run is not done
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.Index), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusBadRequest)
|
||||
// fetch and exec job2
|
||||
job2Task := runner.fetchTask(t)
|
||||
runner.execTask(t, job2Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
|
||||
// RERUN-1: rerun the run
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.Index), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
// fetch and exec job1
|
||||
job1TaskR1 := runner.fetchTask(t)
|
||||
runner.execTask(t, job1TaskR1, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
// fetch and exec job2
|
||||
job2TaskR1 := runner.fetchTask(t)
|
||||
runner.execTask(t, job2TaskR1, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
|
||||
// RERUN-2: rerun job1
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.Index, 0), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
// job2 needs job1, so rerunning job1 will also rerun job2
|
||||
// fetch and exec job1
|
||||
job1TaskR2 := runner.fetchTask(t)
|
||||
runner.execTask(t, job1TaskR2, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
// fetch and exec job2
|
||||
job2TaskR2 := runner.fetchTask(t)
|
||||
runner.execTask(t, job2TaskR2, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
|
||||
// RERUN-3: rerun job2
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.Index, 1), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
// only job2 will rerun
|
||||
// fetch and exec job2
|
||||
job2TaskR3 := runner.fetchTask(t)
|
||||
runner.execTask(t, job2TaskR3, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
runner.fetchNoTask(t)
|
||||
})
|
||||
}
|
||||
296
tests/integration/actions_schedule_test.go
Normal file
296
tests/integration/actions_schedule_test.go
Normal file
@ -0,0 +1,296 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/migration"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
mirror_service "code.gitea.io/gitea/services/mirror"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScheduleUpdate(t *testing.T) {
|
||||
t.Run("Push", testScheduleUpdatePush)
|
||||
t.Run("PullMerge", testScheduleUpdatePullMerge)
|
||||
t.Run("DisableAndEnableActionsUnit", testScheduleUpdateDisableAndEnableActionsUnit)
|
||||
t.Run("ArchiveAndUnarchive", testScheduleUpdateArchiveAndUnarchive)
|
||||
t.Run("MirrorSync", testScheduleUpdateMirrorSync)
|
||||
}
|
||||
|
||||
func testScheduleUpdatePush(t *testing.T) {
|
||||
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
|
||||
newCron := "30 5 * * 1,3"
|
||||
pushScheduleChange(t, u, repo, newCron)
|
||||
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
return branch.CommitID, newCron
|
||||
})
|
||||
}
|
||||
|
||||
func testScheduleUpdatePullMerge(t *testing.T) {
|
||||
newBranchName := "feat1"
|
||||
workflowTreePath := ".gitea/workflows/actions-schedule.yml"
|
||||
workflowContent := `name: actions-schedule
|
||||
on:
|
||||
schedule:
|
||||
- cron: '@every 2m' # update to 2m
|
||||
jobs:
|
||||
job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'schedule workflow'
|
||||
`
|
||||
|
||||
mergeStyles := []repo_model.MergeStyle{
|
||||
repo_model.MergeStyleMerge,
|
||||
repo_model.MergeStyleRebase,
|
||||
repo_model.MergeStyleRebaseMerge,
|
||||
repo_model.MergeStyleSquash,
|
||||
repo_model.MergeStyleFastForwardOnly,
|
||||
}
|
||||
|
||||
for _, mergeStyle := range mergeStyles {
|
||||
t.Run(string(mergeStyle), func(t *testing.T) {
|
||||
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
|
||||
// update workflow file
|
||||
_, err := files_service.ChangeRepoFiles(t.Context(), repo, user, &files_service.ChangeRepoFilesOptions{
|
||||
NewBranch: newBranchName,
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "update",
|
||||
TreePath: workflowTreePath,
|
||||
ContentReader: strings.NewReader(workflowContent),
|
||||
},
|
||||
},
|
||||
Message: "update workflow schedule",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create pull request
|
||||
apiPull, err := doAPICreatePullRequest(testContext, repo.OwnerName, repo.Name, repo.DefaultBranch, newBranchName)(t)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// merge pull request
|
||||
testPullMerge(t, testContext.Session, repo.OwnerName, repo.Name, strconv.FormatInt(apiPull.Index, 10), MergeOptions{
|
||||
Style: mergeStyle,
|
||||
})
|
||||
|
||||
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
|
||||
return pull.MergedCommitID, "@every 2m"
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run(string(repo_model.MergeStyleManuallyMerged), func(t *testing.T) {
|
||||
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
|
||||
// enable manual-merge
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
HasPullRequests: util.ToPointer(true),
|
||||
AllowManualMerge: util.ToPointer(true),
|
||||
})(t)
|
||||
|
||||
// update workflow file
|
||||
fileResp, err := files_service.ChangeRepoFiles(t.Context(), repo, user, &files_service.ChangeRepoFilesOptions{
|
||||
NewBranch: newBranchName,
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "update",
|
||||
TreePath: workflowTreePath,
|
||||
ContentReader: strings.NewReader(workflowContent),
|
||||
},
|
||||
},
|
||||
Message: "update workflow schedule",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// merge and push
|
||||
dstPath := t.TempDir()
|
||||
u.Path = repo.FullName() + ".git"
|
||||
u.User = url.UserPassword(repo.OwnerName, userPassword)
|
||||
doGitClone(dstPath, u)(t)
|
||||
doGitMerge(dstPath, "origin/"+newBranchName)(t)
|
||||
doGitPushTestRepository(dstPath, "origin", repo.DefaultBranch)(t)
|
||||
|
||||
// create pull request
|
||||
apiPull, err := doAPICreatePullRequest(testContext, repo.OwnerName, repo.Name, repo.DefaultBranch, newBranchName)(t)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// merge pull request manually
|
||||
doAPIManuallyMergePullRequest(testContext, repo.OwnerName, repo.Name, fileResp.Commit.SHA, apiPull.Index)(t)
|
||||
|
||||
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
|
||||
assert.Equal(t, issues_model.PullRequestStatusManuallyMerged, pull.Status)
|
||||
return pull.MergedCommitID, "@every 2m"
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testScheduleUpdateMirrorSync(t *testing.T) {
|
||||
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
|
||||
// create mirror repo
|
||||
opts := migration.MigrateOptions{
|
||||
RepoName: "actions-schedule-mirror",
|
||||
Description: "Test mirror for actions-schedule",
|
||||
Private: false,
|
||||
Mirror: true,
|
||||
CloneAddr: repo.CloneLinkGeneral(t.Context()).HTTPS,
|
||||
}
|
||||
mirrorRepo, err := repo_service.CreateRepositoryDirectly(t.Context(), user, user, repo_service.CreateRepoOptions{
|
||||
Name: opts.RepoName,
|
||||
Description: opts.Description,
|
||||
IsPrivate: opts.Private,
|
||||
IsMirror: opts.Mirror,
|
||||
DefaultBranch: repo.DefaultBranch,
|
||||
Status: repo_model.RepositoryBeingMigrated,
|
||||
}, false)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, mirrorRepo.IsMirror)
|
||||
mirrorRepo, err = repo_service.MigrateRepositoryGitData(t.Context(), user, mirrorRepo, opts, nil)
|
||||
assert.NoError(t, err)
|
||||
mirrorContext := NewAPITestContext(t, user.Name, mirrorRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
// enable actions unit for mirror repo
|
||||
assert.False(t, mirrorRepo.UnitEnabled(t.Context(), unit_model.TypeActions))
|
||||
doAPIEditRepository(mirrorContext, &api.EditRepoOption{
|
||||
HasActions: util.ToPointer(true),
|
||||
})(t)
|
||||
actionSchedule := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: mirrorRepo.ID})
|
||||
scheduleSpec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: mirrorRepo.ID, ScheduleID: actionSchedule.ID})
|
||||
assert.Equal(t, "@every 1m", scheduleSpec.Spec)
|
||||
|
||||
// update remote repo
|
||||
newCron := "30 5,17 * * 2,4"
|
||||
pushScheduleChange(t, u, repo, newCron)
|
||||
repoDefaultBranch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// sync
|
||||
ok := mirror_service.SyncPullMirror(t.Context(), mirrorRepo.ID)
|
||||
assert.True(t, ok)
|
||||
mirrorRepoDefaultBranch, err := git_model.GetBranch(t.Context(), mirrorRepo.ID, mirrorRepo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, repoDefaultBranch.CommitID, mirrorRepoDefaultBranch.CommitID)
|
||||
|
||||
// check updated schedule
|
||||
actionSchedule = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: mirrorRepo.ID})
|
||||
scheduleSpec = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: mirrorRepo.ID, ScheduleID: actionSchedule.ID})
|
||||
assert.Equal(t, newCron, scheduleSpec.Spec)
|
||||
|
||||
return repoDefaultBranch.CommitID, newCron
|
||||
})
|
||||
}
|
||||
|
||||
func testScheduleUpdateArchiveAndUnarchive(t *testing.T) {
|
||||
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
Archived: util.ToPointer(true),
|
||||
})(t)
|
||||
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
Archived: util.ToPointer(false),
|
||||
})(t)
|
||||
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
return branch.CommitID, "@every 1m"
|
||||
})
|
||||
}
|
||||
|
||||
func testScheduleUpdateDisableAndEnableActionsUnit(t *testing.T) {
|
||||
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
HasActions: util.ToPointer(false),
|
||||
})(t)
|
||||
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
HasActions: util.ToPointer(true),
|
||||
})(t)
|
||||
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
return branch.CommitID, "@every 1m"
|
||||
})
|
||||
}
|
||||
|
||||
type scheduleUpdateTrigger func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string)
|
||||
|
||||
func doTestScheduleUpdate(t *testing.T, updateTrigger scheduleUpdateTrigger) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-schedule", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
assert.NoError(t, repo.LoadAttributes(t.Context()))
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(httpContext)(t)
|
||||
|
||||
wfTreePath := ".gitea/workflows/actions-schedule.yml"
|
||||
wfFileContent := `name: actions-schedule
|
||||
on:
|
||||
schedule:
|
||||
- cron: '@every 1m'
|
||||
jobs:
|
||||
job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'schedule workflow'
|
||||
`
|
||||
|
||||
opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wfTreePath, wfFileContent)
|
||||
apiFileResp := createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts1)
|
||||
|
||||
actionSchedule := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: repo.ID, CommitSHA: apiFileResp.Commit.SHA})
|
||||
scheduleSpec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: repo.ID, ScheduleID: actionSchedule.ID})
|
||||
assert.Equal(t, "@every 1m", scheduleSpec.Spec)
|
||||
|
||||
commitID, expectedSpec := updateTrigger(t, u, httpContext, user2, repo)
|
||||
|
||||
actionSchedule = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: repo.ID, CommitSHA: commitID})
|
||||
scheduleSpec = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: repo.ID, ScheduleID: actionSchedule.ID})
|
||||
assert.Equal(t, expectedSpec, scheduleSpec.Spec)
|
||||
})
|
||||
}
|
||||
|
||||
func pushScheduleChange(t *testing.T, u *url.URL, repo *repo_model.Repository, newCron string) {
|
||||
workflowTreePath := ".gitea/workflows/actions-schedule.yml"
|
||||
workflowContent := `name: actions-schedule
|
||||
on:
|
||||
schedule:
|
||||
- cron: '` + newCron + `'
|
||||
jobs:
|
||||
job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'schedule workflow'
|
||||
`
|
||||
|
||||
dstPath := t.TempDir()
|
||||
u.Path = repo.FullName() + ".git"
|
||||
u.User = url.UserPassword(repo.OwnerName, userPassword)
|
||||
doGitClone(dstPath, u)(t)
|
||||
doGitCheckoutWriteFileCommit(localGitAddCommitOptions{
|
||||
LocalRepoPath: dstPath,
|
||||
CheckoutBranch: repo.DefaultBranch,
|
||||
TreeFilePath: workflowTreePath,
|
||||
TreeFileContent: workflowContent,
|
||||
})(t)
|
||||
doGitPushTestRepository(dstPath, "origin", repo.DefaultBranch)(t)
|
||||
}
|
||||
@ -10,6 +10,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/cmd"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -36,13 +37,15 @@ func Test_CmdKeys(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// FIXME: this test is not quite right. Each "command run" always re-initializes settings
|
||||
defer test.MockVariableValue(&cmd.CmdKeys.Before, nil)() // don't re-initialize logger during the test
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
app := &cli.Command{
|
||||
Writer: &stdout,
|
||||
ErrWriter: &stderr,
|
||||
Commands: []*cli.Command{cmd.CmdKeys},
|
||||
}
|
||||
cmd.CmdKeys.HideHelp = true
|
||||
err := app.Run(t.Context(), append([]string{"prog"}, tt.args...))
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
}
|
||||
|
||||
.markup .code-preview-container .code-preview-header {
|
||||
color: var(--color-text-light-1);
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
padding: 0.5em;
|
||||
font-size: 12px;
|
||||
|
||||
@ -3,6 +3,7 @@ import {SvgIcon} from '../svg.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import {getIssueColor, getIssueIcon} from '../features/issue.ts';
|
||||
import {computed, onMounted, shallowRef} from 'vue';
|
||||
import type {Issue} from '../types.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
repoLink: string,
|
||||
@ -10,9 +11,9 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const loading = shallowRef(false);
|
||||
const issue = shallowRef(null);
|
||||
const issue = shallowRef<Issue>(null);
|
||||
const renderedLabels = shallowRef('');
|
||||
const errorMessage = shallowRef(null);
|
||||
const errorMessage = shallowRef('');
|
||||
|
||||
const createdAt = computed(() => {
|
||||
return new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'});
|
||||
@ -25,7 +26,7 @@ const body = computed(() => {
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
errorMessage.value = null;
|
||||
errorMessage.value = '';
|
||||
try {
|
||||
const resp = await GET(props.loadIssueInfoUrl);
|
||||
if (!resp.ok) {
|
||||
|
||||
@ -25,9 +25,16 @@ export function createViewFileTreeStore(props: {repoLink: string, treePath: stri
|
||||
},
|
||||
|
||||
async loadViewContent(url: string) {
|
||||
url = url.includes('?') ? url.replace('?', '?only_content=true') : `${url}?only_content=true`;
|
||||
const response = await GET(url);
|
||||
document.querySelector('.repo-view-content').innerHTML = await response.text();
|
||||
const u = new URL(url, window.origin);
|
||||
u.searchParams.set('only_content', 'true');
|
||||
const response = await GET(u.href);
|
||||
const elViewContent = document.querySelector('.repo-view-content');
|
||||
elViewContent.innerHTML = await response.text();
|
||||
const elViewContentData = elViewContent.querySelector('.repo-view-content-data');
|
||||
if (!elViewContentData) return; // if error occurs, there is no such element
|
||||
const t1 = elViewContentData.getAttribute('data-document-title');
|
||||
const t2 = elViewContentData.getAttribute('data-document-title-common');
|
||||
document.title = `${t1} - ${t2}`; // follow the format in head.tmpl: <head><title>...</title></head>
|
||||
},
|
||||
|
||||
async navigateTreeView(treePath: string) {
|
||||
|
||||
@ -56,7 +56,7 @@ const props = defineProps<{
|
||||
const store = createWorkflowStore(props);
|
||||
|
||||
// Track edit state directly on workflow objects
|
||||
const previousSelection = ref(null);
|
||||
const previousSelection = ref<{selectedItem: string | null, selectedWorkflow: any} | null>(null);
|
||||
|
||||
// Helper to check if current workflow is in edit mode
|
||||
const isInEditMode = computed(() => {
|
||||
|
||||
@ -52,14 +52,20 @@ export type IssuePageInfo = {
|
||||
};
|
||||
|
||||
export type Issue = {
|
||||
id: number;
|
||||
number: number;
|
||||
title: string;
|
||||
state: 'open' | 'closed';
|
||||
id: number,
|
||||
number: number,
|
||||
title: string,
|
||||
body: string,
|
||||
state: 'open' | 'closed',
|
||||
created_at: string,
|
||||
pull_request?: {
|
||||
draft: boolean;
|
||||
merged: boolean;
|
||||
};
|
||||
},
|
||||
repository: {
|
||||
full_name: string,
|
||||
},
|
||||
labels: Array<string>,
|
||||
};
|
||||
|
||||
export type FomanticInitFunction = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user