chore: initialize Gitea v1.27.2 fork
giteabot backport / giteabot (push) Canceled after 0s
giteabot / giteabot (push) Canceled after 0s
release-nightly / nightly-binary (push) Canceled after 0s
release-nightly / nightly-container (push) Canceled after 0s
cache-seeder / gobuild (push) Canceled after 0s
cache-seeder / lint (bindata, lint-backend) (push) Canceled after 0s
release-nightly-snapcraft / build-and-publish (push) Canceled after 0s

Includes direct password setup links in registration emails.

Assisted-by: Codex:GPT-5
This commit is contained in:
2026-08-15 22:56:18 +08:00
commit e4afba3416
6210 changed files with 802505 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"testing"
"gitea.dev/models/migrations/migrationtest"
)
func TestMain(m *testing.M) {
migrationtest.MainTest(m)
}
+159
View File
@@ -0,0 +1,159 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"context"
"time"
"gitea.dev/models/db"
"gitea.dev/models/migrations/base"
"gitea.dev/modules/timeutil"
"xorm.io/xorm"
)
type actionRunAttempt struct {
ID int64
RepoID int64 `xorm:"index(repo_concurrency_status)"`
RunID int64 `xorm:"UNIQUE(run_attempt)"`
Attempt int64 `xorm:"UNIQUE(run_attempt)"`
TriggerUserID int64
ConcurrencyGroup string `xorm:"index(repo_concurrency_status) NOT NULL DEFAULT ''"`
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"`
Status int `xorm:"index(repo_concurrency_status)"`
Started timeutil.TimeStamp
Stopped timeutil.TimeStamp
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
}
func (actionRunAttempt) TableName() string {
return "action_run_attempt"
}
type actionArtifact struct {
ID int64 `xorm:"pk autoincr"`
RunID int64 `xorm:"index unique(runid_attempt_name_path)"`
RunAttemptID int64 `xorm:"index unique(runid_attempt_name_path) NOT NULL DEFAULT 0"`
RunnerID int64
RepoID int64 `xorm:"index"`
OwnerID int64
CommitSHA string
StoragePath string
FileSize int64
FileCompressedSize int64
ContentEncoding string `xorm:"content_encoding"`
ArtifactPath string `xorm:"index unique(runid_attempt_name_path)"`
ArtifactName string `xorm:"index unique(runid_attempt_name_path)"`
Status int `xorm:"index"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
ExpiredUnix timeutil.TimeStamp `xorm:"index"`
}
func (actionArtifact) TableName() string {
return "action_artifact"
}
// actionRun mirrors the post-migration action_run schema.
type actionRun struct {
ID int64
Title string
RepoID int64 `xorm:"unique(repo_index)"`
OwnerID int64 `xorm:"index"`
WorkflowID string `xorm:"index"`
Index int64 `xorm:"index unique(repo_index)"`
TriggerUserID int64 `xorm:"index"`
ScheduleID int64
Ref string `xorm:"index"`
CommitSHA string
IsForkPullRequest bool
NeedApproval bool
ApprovedBy int64 `xorm:"index"`
Event string
EventPayload string `xorm:"LONGTEXT"`
TriggerEvent string
Status int `xorm:"index"`
Version int `xorm:"version default 0"`
RawConcurrency string
Started timeutil.TimeStamp
Stopped timeutil.TimeStamp
PreviousDuration time.Duration
LatestAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"`
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
}
func (actionRun) TableName() string {
return "action_run"
}
// AddActionRunAttemptModel adds the ActionRunAttempt table and the supporting ActionRun/ActionRunJob fields.
func AddActionRunAttemptModel(x db.EngineMigration) error {
// add "action_run_attempt"
if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
}, new(actionRunAttempt)); err != nil {
return err
}
// update "action_run_job"
type ActionRunJob struct {
RunAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"`
AttemptJobID int64 `xorm:"index NOT NULL DEFAULT 0"`
SourceTaskID int64 `xorm:"NOT NULL DEFAULT 0"`
}
if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
}, new(ActionRunJob)); err != nil {
return err
}
// update "action_artifact": let xorm sync add the new 4-column unique index (runid_attempt_name_path) and drop the old 3-column unique (runid_name_path)
if err := x.Sync(new(actionArtifact)); err != nil {
return err
}
// update "action_run"
//
// This migration intentionally removes the legacy run-level concurrency columns after
// introducing attempt-level concurrency on action_run_attempt.
//
// Existing values from action_run.concurrency_group / action_run.concurrency_cancel are
// not backfilled into action_run_attempt:
// - the old fields are only meaningful while a run is actively participating in
// concurrency scheduling
// - for completed legacy runs, keeping or backfilling those values has no practical
// effect on future scheduling behavior
// - scanning and backfilling old runs would add significant migration cost for little value
//
// This means the schema change is destructive for those two legacy columns by design.
//
// Let xorm sync add the latest_attempt_id column and drop the now-orphan (repo_id, concurrency_group) index.
if err := x.Sync(new(actionRun)); err != nil {
return err
}
concurrencyColumns := make([]string, 0, 2)
for _, col := range []string{"concurrency_group", "concurrency_cancel"} {
exist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "action_run", col)
if err != nil {
return err
}
if exist {
concurrencyColumns = append(concurrencyColumns, col)
}
}
if len(concurrencyColumns) == 0 {
return nil
}
sess := x.NewSession()
defer sess.Close()
if err := base.DropTableColumns(sess, "action_run", concurrencyColumns...); err != nil {
return err
}
// DropTableColumns rebuilds the table on SQLite, which drops all existing indexes.
// Re-sync to restore the indexes defined on actionRun.
return x.Sync(new(actionRun))
}
+156
View File
@@ -0,0 +1,156 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"context"
"slices"
"testing"
"gitea.dev/models/migrations/migrationtest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/xorm/schemas"
)
type actionRunBeforeV331 struct {
ID int64 `xorm:"pk autoincr"`
ConcurrencyGroup string
ConcurrencyCancel bool
LatestAttemptID int64 `xorm:"-"`
}
func (actionRunBeforeV331) TableName() string {
return "action_run"
}
type actionRunJobBeforeV331 struct {
ID int64 `xorm:"pk autoincr"`
RunID int64 `xorm:"index"`
RepoID int64 `xorm:"index"`
}
func (actionRunJobBeforeV331) TableName() string {
return "action_run_job"
}
type actionArtifactBeforeV331 struct {
ID int64 `xorm:"pk autoincr"`
RunID int64 `xorm:"index unique(runid_name_path)"`
RepoID int64 `xorm:"index"`
ArtifactPath string `xorm:"index unique(runid_name_path)"`
ArtifactName string `xorm:"index unique(runid_name_path)"`
}
func (actionArtifactBeforeV331) TableName() string {
return "action_artifact"
}
func Test_AddActionRunAttemptModel(t *testing.T) {
x, deferable := migrationtest.PrepareTestEnv(t, 0,
new(actionRunBeforeV331),
new(actionRunJobBeforeV331),
new(actionArtifactBeforeV331),
)
defer deferable()
if x == nil || t.Failed() {
return
}
_, err := x.Insert(&actionArtifactBeforeV331{
RunID: 1,
RepoID: 1,
ArtifactPath: "artifact/path",
ArtifactName: "artifact-name",
})
require.NoError(t, err)
require.NoError(t, AddActionRunAttemptModel(x))
tableMap := migrationtest.LoadTableSchemasMap(t, x)
attemptTable := tableMap["action_run_attempt"]
require.NotNil(t, attemptTable)
attemptTablCols := []string{"id", "repo_id", "run_id", "attempt", "trigger_user_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "created", "updated"}
require.ElementsMatch(t, attemptTable.ColumnsSeq(), attemptTablCols)
runTable := tableMap["action_run"]
require.NotNil(t, runTable)
require.Contains(t, runTable.ColumnsSeq(), "latest_attempt_id")
require.NotContains(t, runTable.ColumnsSeq(), "concurrency_group")
require.NotContains(t, runTable.ColumnsSeq(), "concurrency_cancel")
jobTable := tableMap["action_run_job"]
require.NotNil(t, jobTable)
require.Contains(t, jobTable.ColumnsSeq(), "run_attempt_id")
require.Contains(t, jobTable.ColumnsSeq(), "attempt_job_id")
require.Contains(t, jobTable.ColumnsSeq(), "source_task_id")
attemptIndexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action_run_attempt")
require.NoError(t, err)
assert.True(t, hasIndexWithColumns(attemptIndexes, []string{"run_id", "attempt"}, true))
assert.True(t, hasIndexWithColumns(attemptIndexes, []string{"repo_id", "concurrency_group", "status"}, false))
runIndexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action_run")
require.NoError(t, err)
assert.True(t, hasIndexWithColumns(runIndexes, []string{"latest_attempt_id"}, false))
assert.False(t, hasIndexWithColumns(runIndexes, []string{"repo_id", "concurrency_group"}, false))
jobIndexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action_run_job")
require.NoError(t, err)
assert.True(t, hasIndexWithColumns(jobIndexes, []string{"run_attempt_id"}, false))
assert.True(t, hasIndexWithColumns(jobIndexes, []string{"attempt_job_id"}, false))
indexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action_artifact")
require.NoError(t, err)
assert.False(t, hasIndexWithColumns(indexes, []string{"run_id", "artifact_path", "artifact_name"}, true))
assert.True(t, hasIndexWithColumns(indexes, []string{"run_id", "run_attempt_id", "artifact_path", "artifact_name"}, true))
_, err = x.Insert(&actionArtifact{
RunID: 1,
RunAttemptID: 2,
RepoID: 1,
ArtifactPath: "artifact/path",
ArtifactName: "artifact-name",
})
require.NoError(t, err)
_, err = x.Insert(&actionArtifact{
RunID: 1,
RunAttemptID: 2,
RepoID: 1,
ArtifactPath: "artifact/path",
ArtifactName: "artifact-name",
})
require.Error(t, err)
_, err = x.Insert(&actionRunAttempt{
RepoID: 1,
RunID: 1,
Attempt: 2,
TriggerUserID: 1,
Status: 1,
})
require.NoError(t, err)
_, err = x.Insert(&actionRunAttempt{
RepoID: 1,
RunID: 1,
Attempt: 2,
TriggerUserID: 2,
Status: 1,
})
require.Error(t, err)
}
func hasIndexWithColumns(indexes map[string]*schemas.Index, cols []string, isUnique bool) bool {
for _, index := range indexes {
if isUnique && index.Type != schemas.UniqueType {
continue
}
if slices.Equal(index.Cols, cols) {
return true
}
}
return false
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"xorm.io/xorm"
)
type mirrorWithLastSyncUnix struct {
LastSyncUnix int64 `xorm:"INDEX"`
}
func (mirrorWithLastSyncUnix) TableName() string {
return "mirror"
}
func AddLastSyncUnixToMirror(x db.EngineMigration) error {
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
}, new(mirrorWithLastSyncUnix))
return err
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"xorm.io/xorm"
)
func AddBranchProtectionBypassAllowlist(x db.EngineMigration) error {
type ProtectedBranch struct {
EnableBypassAllowlist bool `xorm:"NOT NULL DEFAULT false"`
BypassAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
BypassAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreIndices: true,
}, new(ProtectedBranch))
return err
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"testing"
"gitea.dev/models/migrations/migrationtest"
"github.com/stretchr/testify/require"
)
func Test_AddBranchProtectionBypassAllowlist(t *testing.T) {
type ProtectedBranch struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
BranchName string `xorm:"INDEX"`
EnableBypassAllowlist bool `xorm:"NOT NULL DEFAULT false"`
BypassAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
BypassAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
}
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ProtectedBranch))
defer deferable()
// Test with default values
_, err := x.Insert(&ProtectedBranch{RepoID: 1, BranchName: "main"})
require.NoError(t, err)
// Test with populated allowlist
_, err = x.Insert(&ProtectedBranch{
RepoID: 1,
BranchName: "develop",
EnableBypassAllowlist: true,
BypassAllowlistUserIDs: []int64{1, 2, 3},
BypassAllowlistTeamIDs: []int64{10, 20},
})
require.NoError(t, err)
require.NoError(t, AddBranchProtectionBypassAllowlist(x))
// Verify the default values record
var pb ProtectedBranch
has, err := x.Where("repo_id = ? AND branch_name = ?", 1, "main").Get(&pb)
require.NoError(t, err)
require.True(t, has)
require.False(t, pb.EnableBypassAllowlist)
require.Nil(t, pb.BypassAllowlistUserIDs)
require.Nil(t, pb.BypassAllowlistTeamIDs)
// Verify the populated allowlist record
var pb2 ProtectedBranch
has, err = x.Where("repo_id = ? AND branch_name = ?", 1, "develop").Get(&pb2)
require.NoError(t, err)
require.True(t, has)
require.True(t, pb2.EnableBypassAllowlist)
require.Equal(t, []int64{1, 2, 3}, pb2.BypassAllowlistUserIDs)
require.Equal(t, []int64{10, 20}, pb2.BypassAllowlistTeamIDs)
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"xorm.io/xorm"
)
func AddCancellingSupportToActionRunner(x db.EngineMigration) error {
type ActionRunner struct {
HasCancellingSupport bool `xorm:"has_cancelling_support NOT NULL DEFAULT false"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreDropIndices: true,
}, new(ActionRunner))
return err
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"testing"
"gitea.dev/models/migrations/migrationtest"
"github.com/stretchr/testify/require"
)
func TestAddCancellingSupportToActionRunner(t *testing.T) {
type ActionRunner struct {
ID int64 `xorm:"pk autoincr"`
Name string
}
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ActionRunner))
defer deferable()
if x == nil || t.Failed() {
return
}
_, err := x.Insert(&ActionRunner{Name: "runner"})
require.NoError(t, err)
require.NoError(t, AddCancellingSupportToActionRunner(x))
var hasCancellingSupport bool
has, err := x.SQL("SELECT has_cancelling_support FROM action_runner WHERE id = ?", 1).Get(&hasCancellingSupport)
require.NoError(t, err)
require.True(t, has)
require.False(t, hasCancellingSupport)
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"xorm.io/xorm"
)
// AddReusableWorkflowFieldsToActionRunJob adds the ActionRunJob columns that describe the reusable workflow caller hierarchy,
// and the ActionRunAttemptJobIDIndex table backing run-wide AttemptJobID allocation.
func AddReusableWorkflowFieldsToActionRunJob(x db.EngineMigration) error {
type ActionRunJob struct {
WorkflowSourceRepoID int64 `xorm:"NOT NULL DEFAULT 0"`
WorkflowSourceCommitSHA string `xorm:"VARCHAR(64) NOT NULL DEFAULT ''"`
IsReusableCaller bool `xorm:"index NOT NULL DEFAULT FALSE"`
ParentJobID int64 `xorm:"index NOT NULL DEFAULT 0"`
CallUses string `xorm:"VARCHAR(512) NOT NULL DEFAULT ''"`
CallSecrets string `xorm:"LONGTEXT"`
CallPayload string `xorm:"LONGTEXT"`
IsExpanded bool `xorm:"NOT NULL DEFAULT FALSE"`
ReusableWorkflowContent []byte `xorm:"LONGBLOB"`
}
type ActionRunAttemptJobIDIndex db.ResourceIndex
if _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionRunJob)); err != nil {
return err
}
return x.Sync(new(ActionRunAttemptJobIDIndex))
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
)
func AddActionRunJobSummaryTable(x db.EngineMigration) error {
type ActionRunJobSummary struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(summary_key)"`
RunID int64 `xorm:"UNIQUE(summary_key)"`
RunAttemptID int64 `xorm:"UNIQUE(summary_key) NOT NULL DEFAULT 0"`
JobID int64 `xorm:"UNIQUE(summary_key)"`
StepIndex int64 `xorm:"UNIQUE(summary_key)"`
Content string `xorm:"LONGTEXT"`
ContentType string `xorm:"VARCHAR(255) NOT NULL DEFAULT 'text/markdown'"`
ContentSize int64 `xorm:"NOT NULL DEFAULT 0"`
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(ActionRunJobSummary))
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"xorm.io/xorm"
)
type VisibleType int
type teamWithVisibility struct {
Visibility VisibleType `xorm:"NOT NULL DEFAULT 2"`
}
func (teamWithVisibility) TableName() string {
return "team"
}
func AddVisibilityToTeam(x db.EngineMigration) error {
if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(teamWithVisibility)); err != nil {
return err
}
// Owner teams must remain listable to all org members; new orgs create
// them as "limited", so make existing owner teams limited too.
// Filter on authorize=4 (AccessModeOwner) so a user-created team that
// happens to share the name "owners" is not accidentally affected.
_, err := x.Exec("UPDATE `team` SET visibility = ? WHERE lower_name = ? AND authorize = ?", 1, "owners", 4)
return err
}
+73
View File
@@ -0,0 +1,73 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"fmt"
"strings"
"gitea.dev/models/db"
"gitea.dev/models/migrations/base"
"xorm.io/xorm/schemas"
)
type issueWithLongTextContent struct {
Content string `xorm:"LONGTEXT"`
}
func (issueWithLongTextContent) TableName() string {
return "issue"
}
type commentWithLongTextFields struct {
Content string `xorm:"LONGTEXT"`
PatchQuoted string `xorm:"LONGTEXT patch"`
}
func (commentWithLongTextFields) TableName() string {
return "comment"
}
func isMSSQLMaxTextColumn(column *schemas.Column) bool {
if column.Length != -1 {
return false
}
return strings.EqualFold(column.SQLType.Name, schemas.Varchar) || strings.EqualFold(column.SQLType.Name, schemas.NVarchar)
}
func modifyLongTextColumnsForMSSQL(x db.EngineMigration, bean any, columnNames ...string) error {
table, err := x.TableInfo(bean)
if err != nil {
return err
}
for _, columnName := range columnNames {
column := table.GetColumn(columnName)
if column == nil {
return fmt.Errorf("column %s does not exist in table %s", columnName, table.Name)
}
if isMSSQLMaxTextColumn(column) {
continue
}
if err := base.ModifyColumn(x, table.Name, column); err != nil {
return fmt.Errorf("modify %s.%s: %w", table.Name, columnName, err)
}
}
return nil
}
// ExpandIssueAndCommentLongTextFieldsForMSSQL expands legacy MSSQL nvarchar(4000)
// columns to nvarchar(max) so PR push comments and long issue content are not truncated.
func ExpandIssueAndCommentLongTextFieldsForMSSQL(x db.EngineMigration) error {
if x.Dialect().URI().DBType != schemas.MSSQL {
return nil
}
if err := modifyLongTextColumnsForMSSQL(x, new(issueWithLongTextContent), "content"); err != nil {
return err
}
return modifyLongTextColumnsForMSSQL(x, new(commentWithLongTextFields), "content", "patch")
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"strings"
"testing"
"gitea.dev/models/migrations/migrationtest"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/require"
)
type issueBeforeLongTextMSSQLMigration struct {
ID int64 `xorm:"pk autoincr"`
Content string `xorm:"VARCHAR(4000)"`
}
func (issueBeforeLongTextMSSQLMigration) TableName() string {
return "issue"
}
type commentBeforeLongTextMSSQLMigration struct {
ID int64 `xorm:"pk autoincr"`
Content string `xorm:"VARCHAR(4000)"`
Patch string `xorm:"VARCHAR(4000) patch"`
}
func (commentBeforeLongTextMSSQLMigration) TableName() string {
return "comment"
}
func Test_ExpandIssueAndCommentLongTextFieldsForMSSQL(t *testing.T) {
if !setting.Database.Type.IsMSSQL() {
t.Skip("Only MSSQL needs to expand legacy nvarchar(4000) long-text columns")
}
x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(issueBeforeLongTextMSSQLMigration), new(commentBeforeLongTextMSSQLMigration))
defer deferrable()
require.NoError(t, ExpandIssueAndCommentLongTextFieldsForMSSQL(x))
require.NoError(t, ExpandIssueAndCommentLongTextFieldsForMSSQL(x))
longText := strings.Repeat("x", 5000)
_, err := x.Insert(&issueBeforeLongTextMSSQLMigration{Content: longText})
require.NoError(t, err)
_, err = x.Insert(&commentBeforeLongTextMSSQLMigration{Content: longText, Patch: longText})
require.NoError(t, err)
}
+40
View File
@@ -0,0 +1,40 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"context"
"gitea.dev/models/db"
"xorm.io/xorm/schemas"
)
// AddCreatedUnixToActionUserIsDeletedIndex extends the c_u composite index on
// the action table to include created_unix, enabling efficient ORDER BY on the
// dashboard feed query without a full sort of all matching rows.
func AddCreatedUnixToActionUserIsDeletedIndex(x db.EngineMigration) error {
// xorm Sync cannot reliably update an index when another index already
// covers the same columns in a different order (Equal() is order-insensitive).
// Drop the old c_u index explicitly, then recreate it with the new column set.
indexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action")
if err != nil {
return err
}
for _, idx := range indexes {
if idx.Name == "c_u" {
if _, err := x.Exec(x.Dialect().DropIndexSQL("action", idx)); err != nil {
return err
}
break
}
}
newIndex := schemas.NewIndex("c_u", schemas.IndexType)
newIndex.AddColumn("user_id", "is_deleted", "created_unix")
if _, err := x.Exec(x.Dialect().CreateIndexSQL("action", newIndex)); err != nil {
return err
}
return nil
}
+72
View File
@@ -0,0 +1,72 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"context"
"testing"
"gitea.dev/models/migrations/migrationtest"
"gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/xorm/schemas"
)
type actionBeforeV339 struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX"`
OpType int
ActUserID int64
RepoID int64
CommentID int64 `xorm:"INDEX"`
IsDeleted bool `xorm:"NOT NULL DEFAULT false"`
RefName string
IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
Content string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
}
func (actionBeforeV339) TableName() string { return "action" }
func (actionBeforeV339) TableIndices() []*schemas.Index {
repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType)
repoIndex.AddColumn("repo_id", "user_id", "is_deleted")
actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType)
actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted")
cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType)
cudIndex.AddColumn("created_unix", "user_id", "is_deleted")
// old 2-column index, before the migration
cuIndex := schemas.NewIndex("c_u", schemas.IndexType)
cuIndex.AddColumn("user_id", "is_deleted")
actUserUserIndex := schemas.NewIndex("au_c_u", schemas.IndexType)
actUserUserIndex.AddColumn("act_user_id", "created_unix", "user_id")
return []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex, actUserUserIndex}
}
func Test_AddCreatedUnixToActionUserIsDeletedIndex(t *testing.T) {
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(actionBeforeV339))
defer deferable()
if x == nil || t.Failed() {
return
}
indexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action")
require.NoError(t, err)
assert.True(t, hasIndexWithColumns(indexes, []string{"user_id", "is_deleted"}, false), "old c_u index should exist before migration")
assert.False(t, hasIndexWithColumns(indexes, []string{"user_id", "is_deleted", "created_unix"}, false), "new c_u index should not exist before migration")
require.NoError(t, AddCreatedUnixToActionUserIsDeletedIndex(x))
indexes, err = x.Dialect().GetIndexes(x.DB(), context.Background(), "action")
require.NoError(t, err)
assert.False(t, hasIndexWithColumns(indexes, []string{"user_id", "is_deleted"}, false), "old 2-column c_u index should be gone after migration")
assert.True(t, hasIndexWithColumns(indexes, []string{"user_id", "is_deleted", "created_unix"}, false), "new 3-column c_u index must exist after migration")
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"xorm.io/xorm"
)
// AddContinueOnErrorToActionRunJob adds the ContinueOnError column to ActionRunJob,
// storing the job-level continue-on-error value from the workflow YAML.
func AddContinueOnErrorToActionRunJob(x db.EngineMigration) error {
type ActionRunJob struct {
ContinueOnError bool `xorm:"NOT NULL DEFAULT FALSE"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(ActionRunJob))
return err
}
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"fmt"
"strings"
"time"
"gitea.dev/models/db"
"gitea.dev/models/migrations/base"
"xorm.io/xorm/schemas"
)
// legacyDateTimeColumns are the persisted real datetime columns that old Gitea
// versions created as MSSQL DATETIME. Every other time value is stored as a
// unix timestamp integer, so these are the only columns affected.
var legacyDateTimeColumns = []struct {
bean any
column string
}{
{new(externalLoginUserWithExpiresAt), "expires_at"},
{new(lfsLockWithCreated), "created"},
}
type externalLoginUserWithExpiresAt struct {
ExpiresAt time.Time
}
func (externalLoginUserWithExpiresAt) TableName() string {
return "external_login_user"
}
type lfsLockWithCreated struct {
Created time.Time `xorm:"created"`
}
func (lfsLockWithCreated) TableName() string {
return "lfs_lock"
}
// FixLegacyMSSQLDateTimeColumns converts legacy locale-dependent DATETIME columns
// to DATETIME2. Databases created by old Gitea versions stored these columns as
// DATETIME, which fails to parse ISO datetime strings ('YYYY-MM-DD HH:MM:SS')
// when the MSSQL session language is not English, breaking external account
// linking and LFS lock creation. New installs already use DATETIME2, so only
// legacy MSSQL columns need converting.
func FixLegacyMSSQLDateTimeColumns(x db.EngineMigration) error {
if x.Dialect().URI().DBType != schemas.MSSQL {
return nil
}
for _, c := range legacyDateTimeColumns {
table, err := x.TableInfo(c.bean)
if err != nil {
return err
}
var dataType string
has, err := x.SQL("SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?", table.Name, c.column).Get(&dataType)
if err != nil {
return err
}
if !has || !strings.EqualFold(dataType, "datetime") {
continue
}
column := table.GetColumn(c.column)
if column == nil {
return fmt.Errorf("column %s does not exist in table %s", c.column, table.Name)
}
if err := base.ModifyColumn(x, table.Name, column); err != nil {
return fmt.Errorf("modify %s.%s: %w", table.Name, c.column, err)
}
}
return nil
}
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"testing"
"time"
"gitea.dev/models/db"
"gitea.dev/models/migrations/migrationtest"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/require"
)
type externalLoginUserBeforeDateTimeMigration struct {
ExternalID string `xorm:"pk NOT NULL"`
LoginSourceID int64 `xorm:"pk NOT NULL"`
ExpiresAt time.Time // sync creates DATETIME2; downgraded to legacy DATETIME via raw SQL below
}
func (externalLoginUserBeforeDateTimeMigration) TableName() string {
return "external_login_user"
}
type lfsLockBeforeDateTimeMigration struct {
ID int64 `xorm:"pk autoincr"`
Created time.Time `xorm:"created"`
}
func (lfsLockBeforeDateTimeMigration) TableName() string {
return "lfs_lock"
}
func Test_FixLegacyMSSQLDateTimeColumns(t *testing.T) {
if !setting.Database.Type.IsMSSQL() {
t.Skip("Only MSSQL needs to convert the legacy locale-dependent DATETIME columns")
}
x, deferrable := migrationtest.PrepareTestEnv(t, 0,
new(externalLoginUserBeforeDateTimeMigration),
new(lfsLockBeforeDateTimeMigration),
)
defer deferrable()
// Force the legacy DATETIME column type that old Gitea versions created.
_, err := x.Exec("ALTER TABLE [external_login_user] ALTER COLUMN [expires_at] DATETIME")
require.NoError(t, err)
_, err = x.Exec("ALTER TABLE [lfs_lock] ALTER COLUMN [created] DATETIME")
require.NoError(t, err)
require.Equal(t, "datetime", mssqlColumnType(t, x, "external_login_user", "expires_at"))
require.Equal(t, "datetime", mssqlColumnType(t, x, "lfs_lock", "created"))
require.NoError(t, FixLegacyMSSQLDateTimeColumns(x))
require.NoError(t, FixLegacyMSSQLDateTimeColumns(x)) // idempotent
require.Equal(t, "datetime2", mssqlColumnType(t, x, "external_login_user", "expires_at"))
require.Equal(t, "datetime2", mssqlColumnType(t, x, "lfs_lock", "created"))
// Inserting an ISO-formatted datetime must succeed even under a non-English
// locale, which is the failure the legacy DATETIME columns produced. The
// SET LANGUAGE and INSERT run in one Exec so they share a single connection.
_, err = x.Exec("SET LANGUAGE German; " +
"INSERT INTO [external_login_user] ([external_id], [login_source_id], [expires_at]) " +
"VALUES ('ext-id', 1, '2026-06-25 11:58:39')")
require.NoError(t, err)
_, err = x.Exec("SET LANGUAGE German; " +
"INSERT INTO [lfs_lock] ([created]) VALUES ('2026-06-25 11:58:39')")
require.NoError(t, err)
}
func mssqlColumnType(t *testing.T, x db.EngineMigration, table, column string) string {
t.Helper()
var dataType string
has, err := x.SQL("SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?", table, column).Get(&dataType)
require.NoError(t, err)
require.True(t, has)
return dataType
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
"xorm.io/xorm"
)
func AddScopedWorkflowsSchema(x db.EngineMigration) error {
// Create the action_scoped_workflow_source table
type ScopedWorkflowConfig struct {
Required bool `json:"required"`
Patterns []string `json:"patterns"`
}
type ActionScopedWorkflowSource struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
SourceRepoID int64 `xorm:"INDEX UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
WorkflowConfigs map[string]*ScopedWorkflowConfig `xorm:"JSON TEXT 'workflow_configs'"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
if err := x.Sync(new(ActionScopedWorkflowSource)); err != nil {
return err
}
// Add the columns that record where a run's workflow content came from
type ActionRun struct {
WorkflowRepoID int64 `xorm:"NOT NULL DEFAULT 0"`
WorkflowCommitSHA string `xorm:"VARCHAR(64) NOT NULL DEFAULT ''"`
IsScopedRun bool `xorm:"NOT NULL DEFAULT false"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(ActionRun))
return err
}