From 66b8178e59cf7b44528fecd93637d299582c991e Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 24 Mar 2026 17:49:29 +0100 Subject: [PATCH 01/16] Improve AGENTS.md (#36974) 1. Remove header line, useless context bloat 2. Reword all "before commiting" lines because some people may not be using the agent to commit, only to write changes. --- AGENTS.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 402a9d6945e..3db66637b71 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,8 @@ -# Instructions for agents - - Use `make help` to find available development targets -- Before committing `.go` changes, run `make fmt` to format, and run `make lint-go` to lint -- Before committing `.ts` changes, run `make lint-js` to lint -- Before committing `go.mod` changes, run `make tidy` -- Before committing new `.go` files, add the current year into the copyright header -- Before committing any files, remove all trailing whitespace from source code lines +- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them +- Run `make lint-js` to lint `.ts` files +- Run `make tidy` after any `go.mod` changes +- Add the current year into the copyright header of new `.go` files +- Ensure no trailing whitespace in edited files - Never force-push to pull request branches - Always start issue and pull request comments with an authorship attribution From c96cc701445cfe31fce65dc4ff667ab49f218857 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Wed, 25 Mar 2026 01:23:13 +0800 Subject: [PATCH 02/16] Add class "list-header-filters" to the div for projects (#36889) closes #36886 --- templates/projects/view.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index e1b7364f41c..3e1afab79f0 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -4,7 +4,7 @@

{{.Project.Title}}

- @@ -917,21 +904,8 @@
-
- -
-
- - -
- -
- - -
+ {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} + {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.convert_fork_confirm"))}}
@@ -949,25 +923,13 @@
-
- -
-
- - -
+ {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
-
- - -
+ {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.transfer_perform"))}}
@@ -986,49 +948,57 @@
-
- -
-
- - -
- -
- - -
+ {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} + {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_delete"))}}
{{if not .Repository.IsFork}} -
-
- -
-
- - -
- -
- - -
+ {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} + {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_wiki_delete"))}}
diff --git a/templates/repo/settings/repo_name_confirm_fields.tmpl b/templates/repo/settings/repo_name_confirm_fields.tmpl new file mode 100644 index 00000000000..a8f9800e715 --- /dev/null +++ b/templates/repo/settings/repo_name_confirm_fields.tmpl @@ -0,0 +1,10 @@ +
+ +
+
+ + +
diff --git a/tests/integration/repo_visibility_test.go b/tests/integration/repo_visibility_test.go new file mode 100644 index 00000000000..e7ad8ddd876 --- /dev/null +++ b/tests/integration/repo_visibility_test.go @@ -0,0 +1,59 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestRepositoryVisibilityChange(t *testing.T) { + defer tests.PrepareTestEnv(t)() + session := loginUser(t, "user2") + + t.Run("MakePrivateRequiresCorrectName", func(t *testing.T) { + // Wrong name should be rejected with a JSON error + req := NewRequestWithValues(t, "POST", "/user2/repo1/settings", map[string]string{ + "action": "visibility", + "private": "true", + "confirm_repo_name": "wrong-name", + }) + resp := session.MakeRequest(t, req, http.StatusBadRequest) + assert.NotEmpty(t, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.False(t, repo1.IsPrivate) + + // Correct full name (owner/repo) should succeed with a JSON redirect + req = NewRequestWithValues(t, "POST", "/user2/repo1/settings", map[string]string{ + "action": "visibility", + "private": "true", + "confirm_repo_name": "user2/repo1", + }) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect) + + repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.True(t, repo1.IsPrivate) + }) + + t.Run("MakePublicDoesNotRequireName", func(t *testing.T) { + req := NewRequestWithValues(t, "POST", "/user2/repo2/settings", map[string]string{ + "action": "visibility", + "private": "false", + }) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect) + + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + assert.False(t, repo2.IsPrivate) + }) +} From e24c3f7a40dade43ae7d9e3354491184ad64c141 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 25 Mar 2026 08:23:11 +0100 Subject: [PATCH 05/16] Fix org contact email not clearable once set (#36975) When the email field was submitted as empty in org settings (web and API), the previous guard `if form.Email != ""` silently skipped the update, making it impossible to remove a contact email after it was set. --------- Co-authored-by: wxiaoguang --- modules/structs/org.go | 16 +++--- routers/api/v1/org/org.go | 20 ++++--- routers/web/org/setting.go | 20 +++---- services/forms/org.go | 14 ++--- services/org/org.go | 17 ++++++ services/org/org_test.go | 54 ++++++++++++++----- services/user/email.go | 62 +++++++++++----------- services/user/email_test.go | 75 +++++++++++---------------- templates/swagger/v1_json.tmpl | 2 +- tests/integration/api_org_test.go | 55 ++++++++++++-------- tests/integration/integration_test.go | 3 +- tests/integration/org_test.go | 58 ++++++++++++++------- 12 files changed, 232 insertions(+), 164 deletions(-) diff --git a/modules/structs/org.go b/modules/structs/org.go index c3d70ebf000..d79b1d1d1c0 100644 --- a/modules/structs/org.go +++ b/modules/structs/org.go @@ -66,23 +66,21 @@ type CreateOrgOption struct { RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` } -// TODO: make EditOrgOption fields optional after https://gitea.com/go-chi/binding/pulls/5 got merged - // EditOrgOption options for editing an organization type EditOrgOption struct { // The full display name of the organization - FullName string `json:"full_name" binding:"MaxSize(100)"` - // The email address of the organization - Email string `json:"email" binding:"MaxSize(255)"` + FullName *string `json:"full_name" binding:"MaxSize(100)"` + // The email address of the organization; use empty string to clear + Email *string `json:"email" binding:"MaxSize(255)"` // The description of the organization - Description string `json:"description" binding:"MaxSize(255)"` + Description *string `json:"description" binding:"MaxSize(255)"` // The website URL of the organization - Website string `json:"website" binding:"ValidUrl;MaxSize(255)"` + Website *string `json:"website" binding:"ValidUrl;MaxSize(255)"` // The location of the organization - Location string `json:"location" binding:"MaxSize(50)"` + Location *string `json:"location" binding:"MaxSize(50)"` // possible values are `public`, `limited` or `private` // enum: public,limited,private - Visibility string `json:"visibility" binding:"In(,public,limited,private)"` + Visibility *string `json:"visibility" binding:"In(,public,limited,private)"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"` } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index d42241f0540..ce2a2e5580c 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -5,6 +5,7 @@ package org import ( + "errors" "net/http" activities_model "code.gitea.io/gitea/models/activities" @@ -14,6 +15,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/optional" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/utils" @@ -379,19 +381,21 @@ func Edit(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.EditOrgOption) - if form.Email != "" { - if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil { - ctx.APIErrorInternal(err) + if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.APIError(http.StatusUnprocessableEntity, err) return } + ctx.APIErrorInternal(err) + return } opts := &user_service.UpdateOptions{ - FullName: optional.Some(form.FullName), - Description: optional.Some(form.Description), - Website: optional.Some(form.Website), - Location: optional.Some(form.Location), - Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility), + FullName: optional.FromPtr(form.FullName), + Description: optional.FromPtr(form.Description), + Website: optional.FromPtr(form.Website), + Location: optional.FromPtr(form.Location), + Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()), RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), } if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil { diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 04baa58b734..ca1da6617e5 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -5,6 +5,7 @@ package org import ( + "errors" "net/http" "net/url" @@ -69,24 +70,25 @@ func SettingsPost(ctx *context.Context) { } org := ctx.Org.Organization - - if form.Email != "" { - if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { + if err := org_service.UpdateOrgEmailAddress(ctx, org, form.Email); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { ctx.Data["Err_Email"] = true ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form) return } + ctx.ServerError("UpdateOrgEmailAddress", err) + return } opts := &user_service.UpdateOptions{ - FullName: optional.Some(form.FullName), - Description: optional.Some(form.Description), - Website: optional.Some(form.Website), - Location: optional.Some(form.Location), - RepoAdminChangeTeamAccess: optional.Some(form.RepoAdminChangeTeamAccess), + FullName: optional.FromPtr(form.FullName), + Description: optional.FromPtr(form.Description), + Website: optional.FromPtr(form.Website), + Location: optional.FromPtr(form.Location), + RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), } if ctx.Doer.IsAdmin { - opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation) + opts.MaxRepoCreation = optional.FromPtr(form.MaxRepoCreation) } if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil { diff --git a/services/forms/org.go b/services/forms/org.go index 3997e1da845..8a8106e8cf1 100644 --- a/services/forms/org.go +++ b/services/forms/org.go @@ -36,13 +36,13 @@ func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding // UpdateOrgSettingForm form for updating organization settings type UpdateOrgSettingForm struct { - FullName string `binding:"MaxSize(100)"` - Email string `binding:"MaxSize(255)"` - Description string `binding:"MaxSize(255)"` - Website string `binding:"ValidUrl;MaxSize(255)"` - Location string `binding:"MaxSize(50)"` - MaxRepoCreation int - RepoAdminChangeTeamAccess bool + FullName *string `binding:"MaxSize(100)"` + Email *string `binding:"MaxSize(255)"` + Description *string `binding:"MaxSize(255)"` + Website *string `binding:"ValidUrl;MaxSize(255)"` + Location *string `binding:"MaxSize(50)"` + MaxRepoCreation *int + RepoAdminChangeTeamAccess *bool } // Validate validates the fields diff --git a/services/org/org.go b/services/org/org.go index 8da77c691c1..32c46d7cb9c 100644 --- a/services/org/org.go +++ b/services/org/org.go @@ -168,3 +168,20 @@ func ChangeOrganizationVisibility(ctx context.Context, org *org_model.Organizati return nil }) } + +// UpdateOrgEmailAddress validates and updates the organization's contact email. +// A nil email means no change. +func UpdateOrgEmailAddress(ctx context.Context, org *org_model.Organization, email *string) error { + if email == nil { + return nil + } + + if *email != "" { + if err := user_model.ValidateEmail(*email); err != nil { + return err + } + } + + org.Email = *email + return user_model.UpdateUserCols(ctx, org.AsUser(), "email") +} diff --git a/services/org/org_test.go b/services/org/org_test.go index 5fdc1a6fd5e..5253c739020 100644 --- a/services/org/org_test.go +++ b/services/org/org_test.go @@ -10,28 +10,54 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { unittest.MainTest(m) } -func TestDeleteOrganization(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6}) - assert.NoError(t, DeleteOrganization(t.Context(), org, false)) - unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6}) - unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6}) - unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6}) +func TestOrg(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) - org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) - err := DeleteOrganization(t.Context(), org, false) - assert.Error(t, err) - assert.True(t, repo_model.IsErrUserOwnRepos(err)) + t.Run("UpdateOrgEmailAddress", func(t *testing.T) { + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + originalEmail := org.Email - user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5}) - assert.Error(t, DeleteOrganization(t.Context(), user, false)) - unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) + require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, nil)) + unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: originalEmail}) + + newEmail := "contact@org3.example.com" + require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, &newEmail)) + unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: newEmail}) + + invalidEmail := "invalid email" + err := UpdateOrgEmailAddress(t.Context(), org, &invalidEmail) + require.ErrorIs(t, err, util.ErrInvalidArgument) + unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: newEmail}) + + require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, new(""))) + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: ""}) + assert.Empty(t, org.Email) + }) + + t.Run("DeleteOrganization", func(t *testing.T) { + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6}) + assert.NoError(t, DeleteOrganization(t.Context(), org, false)) + unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6}) + unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6}) + unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6}) + + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + err := DeleteOrganization(t.Context(), org, false) + assert.Error(t, err) + assert.True(t, repo_model.IsErrUserOwnRepos(err)) + + user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5}) + assert.Error(t, DeleteOrganization(t.Context(), user, false)) + unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) + }) } diff --git a/services/user/email.go b/services/user/email.go index c45b3b3ec9f..927e4b72341 100644 --- a/services/user/email.go +++ b/services/user/email.go @@ -14,7 +14,14 @@ import ( "code.gitea.io/gitea/modules/util" ) +// ReplacePrimaryEmailAddress replaces the user's primary email address with the given email address. +// It also updates the user's email field to match the new primary email address. func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error { + // FIXME: this check is from old logic, but it is not right, there are far more user types, not only "organization" + if u.IsOrganization() { + return util.NewInvalidArgumentErrorf("user %s is an organization", u.Name) + } + if strings.EqualFold(u.Email, emailStr) { return nil } @@ -24,41 +31,38 @@ func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailSt } return db.WithTx(ctx, func(ctx context.Context) error { - if !u.IsOrganization() { - // Check if address exists already - email, err := user_model.GetEmailAddressByEmail(ctx, emailStr) - if err != nil && !errors.Is(err, util.ErrNotExist) { - return err - } - if email != nil { - if email.IsPrimary && email.UID == u.ID { - return nil - } - return user_model.ErrEmailAlreadyUsed{Email: emailStr} + // Check if address exists already + email, err := user_model.GetEmailAddressByEmail(ctx, emailStr) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return err + } + if email != nil { + if email.IsPrimary && email.UID == u.ID { + return nil } + return user_model.ErrEmailAlreadyUsed{Email: emailStr} + } - // Remove old primary address - primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID) - if err != nil { - return err - } - if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil { - return err - } + // Remove old primary address + primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID) + if err != nil { + return err + } + if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil { + return err + } - // Insert new primary address - if _, err := user_model.InsertEmailAddress(ctx, &user_model.EmailAddress{ - UID: u.ID, - Email: emailStr, - IsActivated: true, - IsPrimary: true, - }); err != nil { - return err - } + // Insert new primary address + if _, err := user_model.InsertEmailAddress(ctx, &user_model.EmailAddress{ + UID: u.ID, + Email: emailStr, + IsActivated: true, + IsPrimary: true, + }); err != nil { + return err } u.Email = emailStr - return user_model.UpdateUserCols(ctx, u, "email") }) } diff --git a/services/user/email_test.go b/services/user/email_test.go index a031b12cadc..9fb0560b05e 100644 --- a/services/user/email_test.go +++ b/services/user/email_test.go @@ -6,17 +6,16 @@ package user import ( "testing" - organization_model "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" ) -func TestReplacePrimaryEmailAddress(t *testing.T) { +func TestUserEmail(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - t.Run("User", func(t *testing.T) { + t.Run("PrimaryEmailAddress", func(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13}) emails, err := user_model.GetEmailAddresses(t.Context(), user.ID) @@ -42,50 +41,36 @@ func TestReplacePrimaryEmailAddress(t *testing.T) { assert.NoError(t, ReplacePrimaryEmailAddress(t.Context(), user, "primary-13@example.com")) }) - t.Run("Organization", func(t *testing.T) { - org := unittest.AssertExistsAndLoadBean(t, &organization_model.Organization{ID: 3}) + t.Run("AddEmailAddresses", func(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - assert.Equal(t, "org3@example.com", org.Email) + assert.Error(t, AddEmailAddresses(t.Context(), user, []string{" invalid email "})) - assert.NoError(t, ReplacePrimaryEmailAddress(t.Context(), org.AsUser(), "primary-org@example.com")) + emails := []string{"user1234@example.com", "user5678@example.com"} - assert.Equal(t, "primary-org@example.com", org.Email) + assert.NoError(t, AddEmailAddresses(t.Context(), user, emails)) + + err := AddEmailAddresses(t.Context(), user, emails) + assert.Error(t, err) + assert.True(t, user_model.IsErrEmailAlreadyUsed(err)) + }) + + t.Run("DeleteEmailAddresses", func(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + emails := []string{"user2-2@example.com"} + + err := DeleteEmailAddresses(t.Context(), user, emails) + assert.NoError(t, err) + + err = DeleteEmailAddresses(t.Context(), user, emails) + assert.Error(t, err) + assert.True(t, user_model.IsErrEmailAddressNotExist(err)) + + emails = []string{"user2@example.com"} + + err = DeleteEmailAddresses(t.Context(), user, emails) + assert.Error(t, err) + assert.True(t, user_model.IsErrPrimaryEmailCannotDelete(err)) }) } - -func TestAddEmailAddresses(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - assert.Error(t, AddEmailAddresses(t.Context(), user, []string{" invalid email "})) - - emails := []string{"user1234@example.com", "user5678@example.com"} - - assert.NoError(t, AddEmailAddresses(t.Context(), user, emails)) - - err := AddEmailAddresses(t.Context(), user, emails) - assert.Error(t, err) - assert.True(t, user_model.IsErrEmailAlreadyUsed(err)) -} - -func TestDeleteEmailAddresses(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - emails := []string{"user2-2@example.com"} - - err := DeleteEmailAddresses(t.Context(), user, emails) - assert.NoError(t, err) - - err = DeleteEmailAddresses(t.Context(), user, emails) - assert.Error(t, err) - assert.True(t, user_model.IsErrEmailAddressNotExist(err)) - - emails = []string{"user2@example.com"} - - err = DeleteEmailAddresses(t.Context(), user, emails) - assert.Error(t, err) - assert.True(t, user_model.IsErrPrimaryEmailCannotDelete(err)) -} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 20db48b91a0..adc6c181755 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -24855,7 +24855,7 @@ "x-go-name": "Description" }, "email": { - "description": "The email address of the organization", + "description": "The email address of the organization; use empty string to clear", "type": "string", "x-go-name": "Email" }, diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go index 6b7826fbb8b..42f9e4cbf6f 100644 --- a/tests/integration/api_org_test.go +++ b/tests/integration/api_org_test.go @@ -137,34 +137,45 @@ func TestAPIOrgGeneral(t *testing.T) { }) t.Run("OrgEdit", func(t *testing.T) { - org := api.EditOrgOption{ - FullName: "Org3 organization new full name", - Description: "A new description", - Website: "https://try.gitea.io/new", - Location: "Beijing", - Visibility: "private", - } - req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token) - resp := MakeRequest(t, req, http.StatusOK) + org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"}) + assert.NotEqual(t, api.VisibleTypeLimited, org3.Visibility) - var apiOrg api.Organization - DecodeJSON(t, resp, &apiOrg) + org3Edit := api.EditOrgOption{ + FullName: new("new full name"), + Description: new("new description"), + Website: new("https://org3-new-website.example.com"), + Location: new("new location"), + Visibility: new("limited"), + Email: new("org3-new-email@example.com"), + } + req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org3Edit).AddTokenAuth(user1Token) + resp := MakeRequest(t, req, http.StatusOK) + apiOrg := DecodeJSON(t, resp, &api.Organization{}) assert.Equal(t, "org3", apiOrg.Name) - assert.Equal(t, org.FullName, apiOrg.FullName) - assert.Equal(t, org.Description, apiOrg.Description) - assert.Equal(t, org.Website, apiOrg.Website) - assert.Equal(t, org.Location, apiOrg.Location) - assert.Equal(t, org.Visibility, apiOrg.Visibility) + assert.Equal(t, *org3Edit.FullName, apiOrg.FullName) + assert.Equal(t, *org3Edit.Description, apiOrg.Description) + assert.Equal(t, *org3Edit.Website, apiOrg.Website) + assert.Equal(t, *org3Edit.Location, apiOrg.Location) + assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility) + assert.Equal(t, *org3Edit.Email, apiOrg.Email) + org3 = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"}) + assert.Equal(t, api.VisibleTypeLimited, org3.Visibility) + + // empty email can clear the email, nil fields won't change the settings + req = NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &api.EditOrgOption{ + Email: new(""), + }).AddTokenAuth(user1Token) + resp = MakeRequest(t, req, http.StatusOK) + apiOrg = DecodeJSON(t, resp, &api.Organization{}) + assert.Equal(t, *org3Edit.FullName, apiOrg.FullName) + assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility) + assert.Empty(t, apiOrg.Email) }) - t.Run("OrgEditBadVisibility", func(t *testing.T) { + t.Run("OrgEditInvalidVisibility", func(t *testing.T) { org := api.EditOrgOption{ - FullName: "Org3 organization new full name", - Description: "A new description", - Website: "https://try.gitea.io/new", - Location: "Beijing", - Visibility: "badvisibility", + Visibility: new("invalid-visibility"), } req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token) MakeRequest(t, req, http.StatusUnprocessableEntity) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index a4807814dfb..e8b0cbd6414 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -410,12 +410,13 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) { } } -func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) { +func DecodeJSON[T any](t testing.TB, resp *httptest.ResponseRecorder, v T) (ret T) { t.Helper() // FIXME: JSON-KEY-CASE: for testing purpose only, because many structs don't provide `json` tags, they just use capitalized field names decoder := json.NewDecoderCaseInsensitive(resp.Body) require.NoError(t, decoder.Decode(v)) + return v } func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) { diff --git a/tests/integration/org_test.go b/tests/integration/org_test.go index 3ed7baa5ba1..cedc0406ca8 100644 --- a/tests/integration/org_test.go +++ b/tests/integration/org_test.go @@ -23,9 +23,18 @@ import ( "github.com/stretchr/testify/require" ) -func TestOrgRepos(t *testing.T) { +func TestOrg(t *testing.T) { defer tests.PrepareTestEnv(t)() + t.Run("OrgRepos", testOrgRepos) + t.Run("PrivateOrg", testPrivateOrg) + t.Run("LimitedOrg", testLimitedOrg) + t.Run("OrgMembers", testOrgMembers) + t.Run("OrgRestrictedUser", testOrgRestrictedUser) + t.Run("TeamSearch", testTeamSearch) + t.Run("OrgSettings", testOrgSettings) +} +func testOrgRepos(t *testing.T) { var ( users = []string{"user1", "user2"} cases = map[string][]string{ @@ -53,10 +62,8 @@ func TestOrgRepos(t *testing.T) { } } -func TestLimitedOrg(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - // not logged in user +func testLimitedOrg(t *testing.T) { + // not logged-in user req := NewRequest(t, "GET", "/limited_org") MakeRequest(t, req, http.StatusNotFound) req = NewRequest(t, "GET", "/limited_org/public_repo_on_limited_org") @@ -83,10 +90,8 @@ func TestLimitedOrg(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } -func TestPrivateOrg(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - // not logged in user +func testPrivateOrg(t *testing.T) { + // not logged-in user req := NewRequest(t, "GET", "/privated_org") MakeRequest(t, req, http.StatusNotFound) req = NewRequest(t, "GET", "/privated_org/public_repo_on_private_org") @@ -122,10 +127,8 @@ func TestPrivateOrg(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } -func TestOrgMembers(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - // not logged in user +func testOrgMembers(t *testing.T) { + // not logged-in user req := NewRequest(t, "GET", "/org/org25/members") MakeRequest(t, req, http.StatusOK) @@ -140,9 +143,7 @@ func TestOrgMembers(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } -func TestOrgRestrictedUser(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testOrgRestrictedUser(t *testing.T) { // privated_org is a private org who has id 23 orgName := "privated_org" @@ -200,9 +201,7 @@ func TestOrgRestrictedUser(t *testing.T) { restrictedSession.MakeRequest(t, req, http.StatusOK) } -func TestTeamSearch(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testTeamSearch(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17}) @@ -251,3 +250,24 @@ func TestTeamSearch(t *testing.T) { assert.Len(t, teams, 1) // team permission is "write", so can write "code" }) } + +func testOrgSettings(t *testing.T) { + session := loginUser(t, "user2") + + req := NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{ + "full_name": "org3 new full name", + "email": "org3-new-email@example.com", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + assert.Equal(t, "org3 new full name", org.FullName) + assert.Equal(t, "org3-new-email@example.com", org.Email) + + req = NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{ + "email": "", // empty email means "clear email" + }) + session.MakeRequest(t, req, http.StatusSeeOther) + org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + assert.Equal(t, "org3 new full name", org.FullName) + assert.Empty(t, org.Email) +} From bb1e22bba4cbdf85e86b96cd39a82f2a1a4a8b8d Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 25 Mar 2026 08:40:46 +0100 Subject: [PATCH 06/16] Allow text selection on checkbox labels (#36970) Remove `user-select: none` from checkbox labels to allow text selection which is sometimes useful. --------- Signed-off-by: silverwind Co-authored-by: Claude (Opus 4.6) --- web_src/css/modules/checkbox.css | 1 - 1 file changed, 1 deletion(-) diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css index 558486e63a8..220abfc17d2 100644 --- a/web_src/css/modules/checkbox.css +++ b/web_src/css/modules/checkbox.css @@ -101,7 +101,6 @@ input[type="checkbox"]:indeterminate::before { cursor: auto; position: relative; display: block; - user-select: none; } .ui.checkbox label, From 435123fe65f7f0c94d5ee39a4b5d638803d4bd95 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Wed, 25 Mar 2026 10:53:13 -0400 Subject: [PATCH 07/16] Switch `cmd/` to use constructor functions. (#36962) This is a step towards potentially splitting command groups into their own folders to clean up `cmd/` as one folder for all cli commands. Returning fresh command instances will also aid in adding tests as you don't need to concern yourself with the whole command tree being one mutable variable. --------- Co-authored-by: wxiaoguang --- cmd/actions.go | 13 +- cmd/admin.go | 41 +++-- cmd/admin_auth.go | 11 +- cmd/admin_regenerate.go | 10 +- cmd/admin_user.go | 24 +-- cmd/admin_user_generate_access_token.go | 52 +++--- cmd/admin_user_list.go | 20 +-- cmd/docs.go | 33 ++-- cmd/doctor.go | 118 +++++++------- cmd/doctor_convert.go | 13 +- cmd/doctor_test.go | 2 +- cmd/dump.go | 143 ++++++++--------- cmd/dump_repo.go | 107 ++++++------- cmd/embedded.go | 29 ++-- cmd/generate.go | 31 ++-- cmd/hook.go | 36 +++-- cmd/keys.go | 68 ++++---- cmd/main.go | 35 +++-- cmd/manager.go | 44 ++++-- cmd/manager_logging.go | 14 +- cmd/migrate.go | 13 +- cmd/migrate_storage.go | 201 ++++++++++++------------ cmd/restore_repo.go | 65 ++++---- cmd/serv.go | 31 ++-- cmd/web.go | 69 ++++---- tests/integration/cmd_keys_test.go | 6 +- 26 files changed, 651 insertions(+), 578 deletions(-) diff --git a/cmd/actions.go b/cmd/actions.go index 2c51c6a1bcc..44b6b7b54cf 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -13,17 +13,18 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdActions represents the available actions sub-commands. - CmdActions = &cli.Command{ +func newActionsCommand() *cli.Command { + return &cli.Command{ Name: "actions", Usage: "Manage Gitea Actions", Commands: []*cli.Command{ - subcmdActionsGenRunnerToken, + newActionsGenerateRunnerTokenCommand(), }, } +} - subcmdActionsGenRunnerToken = &cli.Command{ +func newActionsGenerateRunnerTokenCommand() *cli.Command { + return &cli.Command{ Name: "generate-runner-token", Usage: "Generate a new token for a runner to use to register with the server", Action: runGenerateActionsRunnerToken, @@ -37,7 +38,7 @@ var ( }, }, } -) +} func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error { setting.MustInstalled() diff --git a/cmd/admin.go b/cmd/admin.go index dbd48e57279..c0e21731cbe 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -18,36 +18,41 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdAdmin represents the available admin sub-command. - CmdAdmin = &cli.Command{ +func newAdminCommand() *cli.Command { + return &cli.Command{ Name: "admin", Usage: "Perform common administrative operations", Commands: []*cli.Command{ - subcmdUser, - subcmdRepoSyncReleases, - subcmdRegenerate, - subcmdAuth, - subcmdSendMail, + newUserCommand(), + newRepoSyncReleasesCommand(), + newRegenerateCommand(), + newAuthCommand(), + newSendMailCommand(), }, } +} - subcmdRepoSyncReleases = &cli.Command{ +func newRepoSyncReleasesCommand() *cli.Command { + return &cli.Command{ Name: "repo-sync-releases", Usage: "Synchronize repository releases with tags", Action: runRepoSyncReleases, } +} - subcmdRegenerate = &cli.Command{ +func newRegenerateCommand() *cli.Command { + return &cli.Command{ Name: "regenerate", Usage: "Regenerate specific files", Commands: []*cli.Command{ - microcmdRegenHooks, - microcmdRegenKeys, + newRegenerateHooksCommand(), + newRegenerateKeysCommand(), }, } +} - subcmdAuth = &cli.Command{ +func newAuthCommand() *cli.Command { + return &cli.Command{ Name: "auth", Usage: "Modify external auth providers", Commands: []*cli.Command{ @@ -59,12 +64,14 @@ var ( microcmdAuthUpdateLdapSimpleAuth(), microcmdAuthAddSMTP(), microcmdAuthUpdateSMTP(), - microcmdAuthList, - microcmdAuthDelete, + newAuthListCommand(), + newAuthDeleteCommand(), }, } +} - subcmdSendMail = &cli.Command{ +func newSendMailCommand() *cli.Command { + return &cli.Command{ Name: "sendmail", Usage: "Send a message to all users", Action: runSendMail, @@ -86,7 +93,7 @@ var ( }, }, } -) +} func idFlag() *cli.Int64Flag { return &cli.Int64Flag{ diff --git a/cmd/admin_auth.go b/cmd/admin_auth.go index 1a093667229..b55bb1481cf 100644 --- a/cmd/admin_auth.go +++ b/cmd/admin_auth.go @@ -17,14 +17,17 @@ import ( "github.com/urfave/cli/v3" ) -var ( - microcmdAuthDelete = &cli.Command{ +func newAuthDeleteCommand() *cli.Command { + return &cli.Command{ Name: "delete", Usage: "Delete specific auth source", Flags: []cli.Flag{idFlag()}, Action: runDeleteAuth, } - microcmdAuthList = &cli.Command{ +} + +func newAuthListCommand() *cli.Command { + return &cli.Command{ Name: "list", Usage: "List auth sources", Action: runListAuth, @@ -55,7 +58,7 @@ var ( }, }, } -) +} func runListAuth(ctx context.Context, c *cli.Command) error { if err := initDB(ctx); err != nil { diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go index a5f1bd51059..aa235441bac 100644 --- a/cmd/admin_regenerate.go +++ b/cmd/admin_regenerate.go @@ -13,19 +13,21 @@ import ( "github.com/urfave/cli/v3" ) -var ( - microcmdRegenHooks = &cli.Command{ +func newRegenerateHooksCommand() *cli.Command { + return &cli.Command{ Name: "hooks", Usage: "Regenerate git-hooks", Action: runRegenerateHooks, } +} - microcmdRegenKeys = &cli.Command{ +func newRegenerateKeysCommand() *cli.Command { + return &cli.Command{ Name: "keys", Usage: "Regenerate authorized_keys file", Action: runRegenerateKeys, } -) +} func runRegenerateHooks(ctx context.Context, _ *cli.Command) error { if err := initDB(ctx); err != nil { diff --git a/cmd/admin_user.go b/cmd/admin_user.go index 3a24c3e56f1..8dd8bb4eca1 100644 --- a/cmd/admin_user.go +++ b/cmd/admin_user.go @@ -7,15 +7,17 @@ import ( "github.com/urfave/cli/v3" ) -var subcmdUser = &cli.Command{ - Name: "user", - Usage: "Modify users", - Commands: []*cli.Command{ - microcmdUserCreate(), - microcmdUserList, - microcmdUserChangePassword(), - microcmdUserDelete(), - microcmdUserGenerateAccessToken, - microcmdUserMustChangePassword(), - }, +func newUserCommand() *cli.Command { + return &cli.Command{ + Name: "user", + Usage: "Modify users", + Commands: []*cli.Command{ + microcmdUserCreate(), + newUserListCommand(), + microcmdUserChangePassword(), + microcmdUserDelete(), + newUserGenerateAccessTokenCommand(), + microcmdUserMustChangePassword(), + }, + } } diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go index 61064fdef4b..7f8330ff7e4 100644 --- a/cmd/admin_user_generate_access_token.go +++ b/cmd/admin_user_generate_access_token.go @@ -14,32 +14,34 @@ import ( "github.com/urfave/cli/v3" ) -var microcmdUserGenerateAccessToken = &cli.Command{ - Name: "generate-access-token", - Usage: "Generate an access token for a specific user", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Usage: "Username", +func newUserGenerateAccessTokenCommand() *cli.Command { + return &cli.Command{ + Name: "generate-access-token", + Usage: "Generate an access token for a specific user", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "Username", + }, + &cli.StringFlag{ + Name: "token-name", + Aliases: []string{"t"}, + Usage: "Token name", + Value: "gitea-admin", + }, + &cli.BoolFlag{ + Name: "raw", + Usage: "Display only the token value", + }, + &cli.StringFlag{ + Name: "scopes", + Value: "all", + Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, + }, }, - &cli.StringFlag{ - Name: "token-name", - Aliases: []string{"t"}, - Usage: "Token name", - Value: "gitea-admin", - }, - &cli.BoolFlag{ - Name: "raw", - Usage: "Display only the token value", - }, - &cli.StringFlag{ - Name: "scopes", - Value: "all", - Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, - }, - }, - Action: runGenerateAccessToken, + Action: runGenerateAccessToken, + } } func runGenerateAccessToken(ctx context.Context, c *cli.Command) error { diff --git a/cmd/admin_user_list.go b/cmd/admin_user_list.go index e3d345e2f24..2958fe0cc8b 100644 --- a/cmd/admin_user_list.go +++ b/cmd/admin_user_list.go @@ -14,16 +14,18 @@ import ( "github.com/urfave/cli/v3" ) -var microcmdUserList = &cli.Command{ - Name: "list", - Usage: "List users", - Action: runListUsers, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "admin", - Usage: "List only admin users", +func newUserListCommand() *cli.Command { + return &cli.Command{ + Name: "list", + Usage: "List users", + Action: runListUsers, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "admin", + Usage: "List only admin users", + }, }, - }, + } } func runListUsers(ctx context.Context, c *cli.Command) error { diff --git a/cmd/docs.go b/cmd/docs.go index 098c0e9a8a1..8e0f9428df3 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -13,23 +13,24 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDocs represents the available docs sub-command. -var CmdDocs = &cli.Command{ - Name: "docs", - Usage: "Output CLI documentation", - Description: "A command to output Gitea's CLI documentation, optionally to a file.", - Action: runDocs, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "man", - Usage: "Output man pages instead", +func newDocsCommand() *cli.Command { + return &cli.Command{ + Name: "docs", + Usage: "Output CLI documentation", + Description: "A command to output Gitea's CLI documentation, optionally to a file.", + Action: runDocs, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "man", + Usage: "Output man pages instead", + }, + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "Path to output to instead of stdout (will overwrite if exists)", + }, }, - &cli.StringFlag{ - Name: "output", - Aliases: []string{"o"}, - Usage: "Path to output to instead of stdout (will overwrite if exists)", - }, - }, + } } func runDocs(_ context.Context, cmd *cli.Command) error { diff --git a/cmd/doctor.go b/cmd/doctor.go index 596dd611786..188740dbcea 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -24,73 +24,77 @@ import ( "xorm.io/xorm" ) -// CmdDoctor represents the available doctor sub-command. -var CmdDoctor = &cli.Command{ - Name: "doctor", - Usage: "Diagnose and optionally fix problems, convert or re-create database tables", - Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", - - Commands: []*cli.Command{ - cmdDoctorCheck, - cmdRecreateTable, - cmdDoctorConvert, - }, +func newDoctorCommand() *cli.Command { + return &cli.Command{ + Name: "doctor", + Usage: "Diagnose and optionally fix problems, convert or re-create database tables", + Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", + Commands: []*cli.Command{ + newDoctorCheckCommand(), + newRecreateTableCommand(), + newDoctorConvertCommand(), + }, + } } -var cmdDoctorCheck = &cli.Command{ - Name: "check", - Usage: "Diagnose and optionally fix problems", - Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", - Action: runDoctorCheck, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "list", - Usage: "List the available checks", +func newDoctorCheckCommand() *cli.Command { + return &cli.Command{ + Name: "check", + Usage: "Diagnose and optionally fix problems", + Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", + Action: runDoctorCheck, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "list", + Usage: "List the available checks", + }, + &cli.BoolFlag{ + Name: "default", + Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)", + }, + &cli.StringSliceFlag{ + Name: "run", + Usage: "Run the provided checks - (if --default is set, the default checks will also run)", + }, + &cli.BoolFlag{ + Name: "all", + Usage: "Run all the available checks", + }, + &cli.BoolFlag{ + Name: "fix", + Usage: "Automatically fix what we can", + }, + &cli.StringFlag{ + Name: "log-file", + Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`, + }, + &cli.BoolFlag{ + Name: "color", + Aliases: []string{"H"}, + Usage: "Use color for outputted information", + }, }, - &cli.BoolFlag{ - Name: "default", - Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)", - }, - &cli.StringSliceFlag{ - Name: "run", - Usage: "Run the provided checks - (if --default is set, the default checks will also run)", - }, - &cli.BoolFlag{ - Name: "all", - Usage: "Run all the available checks", - }, - &cli.BoolFlag{ - Name: "fix", - Usage: "Automatically fix what we can", - }, - &cli.StringFlag{ - Name: "log-file", - Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`, - }, - &cli.BoolFlag{ - Name: "color", - Aliases: []string{"H"}, - Usage: "Use color for outputted information", - }, - }, + } } -var cmdRecreateTable = &cli.Command{ - Name: "recreate-table", - Usage: "Recreate tables from XORM definitions and copy the data.", - ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "debug", - Usage: "Print SQL commands sent", +func newRecreateTableCommand() *cli.Command { + return &cli.Command{ + Name: "recreate-table", + Usage: "Recreate tables from XORM definitions and copy the data.", + ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "Print SQL commands sent", + }, }, - }, - Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns. + Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns. This command will cause Xorm to recreate tables, copying over the data and deleting the old table. You should back-up your database before doing this and ensure that your database is up-to-date first.`, - Action: runRecreateTable, + Action: runRecreateTable, + } } func runRecreateTable(ctx context.Context, cmd *cli.Command) error { diff --git a/cmd/doctor_convert.go b/cmd/doctor_convert.go index 8cb718d3839..f4867912ab3 100644 --- a/cmd/doctor_convert.go +++ b/cmd/doctor_convert.go @@ -14,12 +14,13 @@ import ( "github.com/urfave/cli/v3" ) -// cmdDoctorConvert represents the available convert sub-command. -var cmdDoctorConvert = &cli.Command{ - Name: "convert", - Usage: "Convert the database", - Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar", - Action: runDoctorConvert, +func newDoctorConvertCommand() *cli.Command { + return &cli.Command{ + Name: "convert", + Usage: "Convert the database", + Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar", + Action: runDoctorConvert, + } } func runDoctorConvert(ctx context.Context, cmd *cli.Command) error { diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index da942b38b60..7d2f3589475 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -23,7 +23,7 @@ func TestDoctorRun(t *testing.T) { SkipDatabaseInitialization: true, }) app := &cli.Command{ - Commands: []*cli.Command{cmdDoctorCheck}, + Commands: []*cli.Command{newDoctorCheckCommand()}, } err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"}) assert.NoError(t, err) diff --git a/cmd/dump.go b/cmd/dump.go index 7f0b23ed984..49f4d9e8946 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -23,78 +23,79 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDump represents the available dump sub-command. -var CmdDump = &cli.Command{ - Name: "dump", - Usage: "Dump Gitea files and database", - Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`, - Action: runDump, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "file", - Aliases: []string{"f"}, - Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`, +func newDumpCommand() *cli.Command { + return &cli.Command{ + Name: "dump", + Usage: "Dump Gitea files and database", + Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`, + Action: runDump, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`, + }, + &cli.BoolFlag{ + Name: "verbose", + Aliases: []string{"V"}, + Usage: "Show process details", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display warnings and errors", + }, + &cli.StringFlag{ + Name: "tempdir", + Aliases: []string{"t"}, + Value: os.TempDir(), + Usage: "Temporary dir path", + }, + &cli.StringFlag{ + Name: "database", + Aliases: []string{"d"}, + Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres", + }, + &cli.BoolFlag{ + Name: "skip-repository", + Aliases: []string{"R"}, + Usage: "Skip the repository dumping", + }, + &cli.BoolFlag{ + Name: "skip-log", + Aliases: []string{"L"}, + Usage: "Skip the log dumping", + }, + &cli.BoolFlag{ + Name: "skip-custom-dir", + Usage: "Skip custom directory", + }, + &cli.BoolFlag{ + Name: "skip-lfs-data", + Usage: "Skip LFS data", + }, + &cli.BoolFlag{ + Name: "skip-attachment-data", + Usage: "Skip attachment data", + }, + &cli.BoolFlag{ + Name: "skip-package-data", + Usage: "Skip package data", + }, + &cli.BoolFlag{ + Name: "skip-index", + Usage: "Skip bleve index data", + }, + &cli.BoolFlag{ + Name: "skip-db", + Usage: "Skip database", + }, + &cli.StringFlag{ + Name: "type", + Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "), + }, }, - &cli.BoolFlag{ - Name: "verbose", - Aliases: []string{"V"}, - Usage: "Show process details", - }, - &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Only display warnings and errors", - }, - &cli.StringFlag{ - Name: "tempdir", - Aliases: []string{"t"}, - Value: os.TempDir(), - Usage: "Temporary dir path", - }, - &cli.StringFlag{ - Name: "database", - Aliases: []string{"d"}, - Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres", - }, - &cli.BoolFlag{ - Name: "skip-repository", - Aliases: []string{"R"}, - Usage: "Skip the repository dumping", - }, - &cli.BoolFlag{ - Name: "skip-log", - Aliases: []string{"L"}, - Usage: "Skip the log dumping", - }, - &cli.BoolFlag{ - Name: "skip-custom-dir", - Usage: "Skip custom directory", - }, - &cli.BoolFlag{ - Name: "skip-lfs-data", - Usage: "Skip LFS data", - }, - &cli.BoolFlag{ - Name: "skip-attachment-data", - Usage: "Skip attachment data", - }, - &cli.BoolFlag{ - Name: "skip-package-data", - Usage: "Skip package data", - }, - &cli.BoolFlag{ - Name: "skip-index", - Usage: "Skip bleve index data", - }, - &cli.BoolFlag{ - Name: "skip-db", - Usage: "Skip database", - }, - &cli.StringFlag{ - Name: "type", - Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "), - }, - }, + } } func fatal(format string, args ...any) { diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go index beda305c85f..367454366ec 100644 --- a/cmd/dump_repo.go +++ b/cmd/dump_repo.go @@ -22,61 +22,62 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDumpRepository represents the available dump repository sub-command. -var CmdDumpRepository = &cli.Command{ - Name: "dump-repo", - Usage: "Dump the repository from git/github/gitea/gitlab", - Description: "This is a command for dumping the repository data.", - Action: runDumpRepository, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "git_service", - Value: "", - Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", - }, - &cli.StringFlag{ - Name: "repo_dir", - Aliases: []string{"r"}, - Value: "./data", - Usage: "Repository dir path to store the data", - }, - &cli.StringFlag{ - Name: "clone_addr", - Value: "", - Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", - }, - &cli.StringFlag{ - Name: "auth_username", - Value: "", - Usage: "The username to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "auth_password", - Value: "", - Usage: "The password to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "auth_token", - Value: "", - Usage: "The personal token to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "owner_name", - Value: "", - Usage: "The data will be stored on a directory with owner name if not empty", - }, - &cli.StringFlag{ - Name: "repo_name", - Value: "", - Usage: "The data will be stored on a directory with repository name if not empty", - }, - &cli.StringFlag{ - Name: "units", - Value: "", - Usage: `Which items will be migrated, one or more units should be separated as comma. +func newDumpRepositoryCommand() *cli.Command { + return &cli.Command{ + Name: "dump-repo", + Usage: "Dump the repository from git/github/gitea/gitlab", + Description: "This is a command for dumping the repository data.", + Action: runDumpRepository, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "git_service", + Value: "", + Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", + }, + &cli.StringFlag{ + Name: "repo_dir", + Aliases: []string{"r"}, + Value: "./data", + Usage: "Repository dir path to store the data", + }, + &cli.StringFlag{ + Name: "clone_addr", + Value: "", + Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", + }, + &cli.StringFlag{ + Name: "auth_username", + Value: "", + Usage: "The username to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "auth_password", + Value: "", + Usage: "The password to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "auth_token", + Value: "", + Usage: "The personal token to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "owner_name", + Value: "", + Usage: "The data will be stored on a directory with owner name if not empty", + }, + &cli.StringFlag{ + Name: "repo_name", + Value: "", + Usage: "The data will be stored on a directory with repository name if not empty", + }, + &cli.StringFlag{ + Name: "units", + Value: "", + Usage: `Which items will be migrated, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, + }, }, - }, + } } func runDumpRepository(ctx context.Context, cmd *cli.Command) error { diff --git a/cmd/embedded.go b/cmd/embedded.go index 9180407fd18..e2110756b8f 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -23,20 +23,23 @@ import ( "github.com/urfave/cli/v3" ) -// CmdEmbedded represents the available extract sub-command. -var ( - CmdEmbedded = &cli.Command{ +var matchedAssetFiles []assetFile + +func newEmbeddedCommand() *cli.Command { + return &cli.Command{ Name: "embedded", Usage: "Extract embedded resources", Description: "A command for extracting embedded resources, like templates and images", Commands: []*cli.Command{ - subcmdList, - subcmdView, - subcmdExtract, + newEmbeddedListCommand(), + newEmbeddedViewCommand(), + newEmbeddedExtractCommand(), }, } +} - subcmdList = &cli.Command{ +func newEmbeddedListCommand() *cli.Command { + return &cli.Command{ Name: "list", Usage: "List files matching the given pattern", Action: runList, @@ -48,8 +51,10 @@ var ( }, }, } +} - subcmdView = &cli.Command{ +func newEmbeddedViewCommand() *cli.Command { + return &cli.Command{ Name: "view", Usage: "View a file matching the given pattern", Action: runView, @@ -61,8 +66,10 @@ var ( }, }, } +} - subcmdExtract = &cli.Command{ +func newEmbeddedExtractCommand() *cli.Command { + return &cli.Command{ Name: "extract", Usage: "Extract resources", Action: runExtract, @@ -91,9 +98,7 @@ var ( }, }, } - - matchedAssetFiles []assetFile -) +} type assetFile struct { fs *assetfs.LayeredFS diff --git a/cmd/generate.go b/cmd/generate.go index 9cb4cf39171..b94ff79aaec 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -15,45 +15,52 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdGenerate represents the available generate sub-command. - CmdGenerate = &cli.Command{ +func newGenerateCommand() *cli.Command { + return &cli.Command{ Name: "generate", Usage: "Generate Gitea's secrets/keys/tokens", Commands: []*cli.Command{ - subcmdSecret, + newGenerateSecretCommand(), }, } +} - subcmdSecret = &cli.Command{ +func newGenerateSecretCommand() *cli.Command { + return &cli.Command{ Name: "secret", Usage: "Generate a secret token", Commands: []*cli.Command{ - microcmdGenerateInternalToken, - microcmdGenerateLfsJwtSecret, - microcmdGenerateSecretKey, + newGenerateInternalTokenCommand(), + newGenerateLfsJWTSecretCommand(), + newGenerateSecretKeyCommand(), }, } +} - microcmdGenerateInternalToken = &cli.Command{ +func newGenerateInternalTokenCommand() *cli.Command { + return &cli.Command{ Name: "INTERNAL_TOKEN", Usage: "Generate a new INTERNAL_TOKEN", Action: runGenerateInternalToken, } +} - microcmdGenerateLfsJwtSecret = &cli.Command{ +func newGenerateLfsJWTSecretCommand() *cli.Command { + return &cli.Command{ Name: "JWT_SECRET", Aliases: []string{"LFS_JWT_SECRET"}, Usage: "Generate a new JWT_SECRET", Action: runGenerateLfsJwtSecret, } +} - microcmdGenerateSecretKey = &cli.Command{ +func newGenerateSecretKeyCommand() *cli.Command { + return &cli.Command{ Name: "SECRET_KEY", Usage: "Generate a new SECRET_KEY", Action: runGenerateSecretKey, } -) +} func runGenerateInternalToken(_ context.Context, c *cli.Command) error { internalToken, err := generate.NewInternalToken() diff --git a/cmd/hook.go b/cmd/hook.go index 992e52c2790..4a6c7c2905a 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -28,23 +28,24 @@ const ( hookBatchSize = 500 ) -var ( - // CmdHook represents the available hooks sub-command. - CmdHook = &cli.Command{ +func newHookCommand() *cli.Command { + return &cli.Command{ Name: "hook", Usage: "(internal) Should only be called by Git", Hidden: true, // internal commands shouldn't be visible Description: "Delegate commands to corresponding Git hooks", Before: PrepareConsoleLoggerLevel(log.FATAL), Commands: []*cli.Command{ - subcmdHookPreReceive, - subcmdHookUpdate, - subcmdHookPostReceive, - subcmdHookProcReceive, + newHookPreReceiveCommand(), + newHookUpdateCommand(), + newHookPostReceiveCommand(), + newHookProcReceiveCommand(), }, } +} - subcmdHookPreReceive = &cli.Command{ +func newHookPreReceiveCommand() *cli.Command { + return &cli.Command{ Name: "pre-receive", Usage: "Delegate pre-receive Git hook", Description: "This command should only be called by Git", @@ -55,7 +56,10 @@ var ( }, }, } - subcmdHookUpdate = &cli.Command{ +} + +func newHookUpdateCommand() *cli.Command { + return &cli.Command{ Name: "update", Usage: "Delegate update Git hook", Description: "This command should only be called by Git", @@ -66,7 +70,10 @@ var ( }, }, } - subcmdHookPostReceive = &cli.Command{ +} + +func newHookPostReceiveCommand() *cli.Command { + return &cli.Command{ Name: "post-receive", Usage: "Delegate post-receive Git hook", Description: "This command should only be called by Git", @@ -77,8 +84,11 @@ var ( }, }, } - // Note: new hook since git 2.29 - subcmdHookProcReceive = &cli.Command{ +} + +// Note: new hook since git 2.29 +func newHookProcReceiveCommand() *cli.Command { + return &cli.Command{ Name: "proc-receive", Usage: "Delegate proc-receive Git hook", Description: "This command should only be called by Git", @@ -89,7 +99,7 @@ var ( }, }, } -) +} type delayWriter struct { internal io.Writer diff --git a/cmd/keys.go b/cmd/keys.go index 035d39bfb86..912cf250915 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -15,40 +15,42 @@ import ( "github.com/urfave/cli/v3" ) -// CmdKeys represents the available keys sub-command -var CmdKeys = &cli.Command{ - Name: "keys", - Usage: "(internal) Should only be called by SSH server", - 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, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "expected", - Aliases: []string{"e"}, - Value: "git", - Usage: "Expected user for whom provide key commands", +// NewKeysCommand returns the internal SSH key lookup sub-command. +func NewKeysCommand() *cli.Command { + return &cli.Command{ + Name: "keys", + Usage: "(internal) Should only be called by SSH server", + 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, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "expected", + Aliases: []string{"e"}, + Value: "git", + Usage: "Expected user for whom provide key commands", + }, + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Value: "", + Usage: "Username trying to log in by SSH", + }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "", + Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)", + }, + &cli.StringFlag{ + Name: "content", + Aliases: []string{"k"}, + Value: "", + Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)", + }, }, - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Value: "", - Usage: "Username trying to log in by SSH", - }, - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Value: "", - Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)", - }, - &cli.StringFlag{ - Name: "content", - Aliases: []string{"k"}, - Value: "", - Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)", - }, - }, + } } func runKeys(ctx context.Context, c *cli.Command) error { diff --git a/cmd/main.go b/cmd/main.go index 2ee00382d75..a6b89a6fada 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -112,35 +112,36 @@ func NewMainApp(appVer AppVersion) *cli.Command { Usage: "Set custom path (defaults to '{WorkPath}/custom')", }, } + webCmd := newWebCommand() // these sub-commands need to use a config file subCmdWithConfig := []*cli.Command{ - CmdWeb, - CmdServ, - CmdHook, - CmdKeys, - CmdDump, - CmdAdmin, - CmdMigrate, - CmdDoctor, - CmdManager, - CmdEmbedded, - CmdMigrateStorage, - CmdDumpRepository, - CmdRestoreRepository, - CmdActions, + webCmd, + newServCommand(), + newHookCommand(), + NewKeysCommand(), + newDumpCommand(), + newAdminCommand(), + newMigrateCommand(), + newDoctorCommand(), + newManagerCommand(), + newEmbeddedCommand(), + newMigrateStorageCommand(), + newDumpRepositoryCommand(), + newRestoreRepositoryCommand(), + newActionsCommand(), } // these sub-commands do not need the config file, and they do not depend on any path or environment variable. subCmdStandalone := []*cli.Command{ cmdConfig(), cmdCert(), - CmdGenerate, - CmdDocs, + newGenerateCommand(), + newDocsCommand(), } // TODO: we should eventually drop the default command, // but not sure whether it would break Windows users who used to double-click the EXE to run. - app.DefaultCommand = CmdWeb.Name + app.DefaultCommand = webCmd.Name app.Before = PrepareConsoleLoggerLevel(log.INFO) for i := range subCmdWithConfig { diff --git a/cmd/manager.go b/cmd/manager.go index f0935ea0657..586c65990bc 100644 --- a/cmd/manager.go +++ b/cmd/manager.go @@ -13,22 +13,24 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdManager represents the manager command - CmdManager = &cli.Command{ +func newManagerCommand() *cli.Command { + return &cli.Command{ Name: "manager", Usage: "Manage the running gitea process", Description: "This is a command for managing the running gitea process", Commands: []*cli.Command{ - subcmdShutdown, - subcmdRestart, - subcmdReloadTemplates, - subcmdFlushQueues, - subcmdLogging, - subCmdProcesses, + newShutdownCommand(), + newRestartCommand(), + newReloadTemplatesCommand(), + newFlushQueuesCommand(), + newLoggingCommand(), + newProcessesCommand(), }, } - subcmdShutdown = &cli.Command{ +} + +func newShutdownCommand() *cli.Command { + return &cli.Command{ Name: "shutdown", Usage: "Gracefully shutdown the running process", Flags: []cli.Flag{ @@ -38,7 +40,10 @@ var ( }, Action: runShutdown, } - subcmdRestart = &cli.Command{ +} + +func newRestartCommand() *cli.Command { + return &cli.Command{ Name: "restart", Usage: "Gracefully restart the running process - (not implemented for windows servers)", Flags: []cli.Flag{ @@ -48,7 +53,10 @@ var ( }, Action: runRestart, } - subcmdReloadTemplates = &cli.Command{ +} + +func newReloadTemplatesCommand() *cli.Command { + return &cli.Command{ Name: "reload-templates", Usage: "Reload template files in the running process", Flags: []cli.Flag{ @@ -58,7 +66,10 @@ var ( }, Action: runReloadTemplates, } - subcmdFlushQueues = &cli.Command{ +} + +func newFlushQueuesCommand() *cli.Command { + return &cli.Command{ Name: "flush-queues", Usage: "Flush queues in the running process", Action: runFlushQueues, @@ -77,7 +88,10 @@ var ( }, }, } - subCmdProcesses = &cli.Command{ +} + +func newProcessesCommand() *cli.Command { + return &cli.Command{ Name: "processes", Usage: "Display running processes within the current process", Action: runProcesses, @@ -107,7 +121,7 @@ var ( }, }, } -) +} func runShutdown(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go index ac29e7d3e50..5812e707e20 100644 --- a/cmd/manager_logging.go +++ b/cmd/manager_logging.go @@ -15,8 +15,8 @@ import ( "github.com/urfave/cli/v3" ) -var ( - defaultLoggingFlags = []cli.Flag{ +func defaultLoggingFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "logger", Usage: `Logger name - will default to "default"`, @@ -57,8 +57,10 @@ var ( Name: "debug", }, } +} - subcmdLogging = &cli.Command{ +func newLoggingCommand() *cli.Command { + return &cli.Command{ Name: "logging", Usage: "Adjust logging commands", Commands: []*cli.Command{ @@ -109,7 +111,7 @@ var ( { Name: "file", Usage: "Add a file logger", - Flags: append(defaultLoggingFlags, []cli.Flag{ + Flags: append(defaultLoggingFlags(), []cli.Flag{ &cli.StringFlag{ Name: "filename", Aliases: []string{"f"}, @@ -150,7 +152,7 @@ var ( }, { Name: "conn", Usage: "Add a net conn logger", - Flags: append(defaultLoggingFlags, []cli.Flag{ + Flags: append(defaultLoggingFlags(), []cli.Flag{ &cli.BoolFlag{ Name: "reconnect-on-message", Aliases: []string{"R"}, @@ -191,7 +193,7 @@ var ( }, }, } -) +} func runRemoveLogger(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) diff --git a/cmd/migrate.go b/cmd/migrate.go index e24dc9e5720..016f8a0db56 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -14,12 +14,13 @@ import ( "github.com/urfave/cli/v3" ) -// CmdMigrate represents the available migrate sub-command. -var CmdMigrate = &cli.Command{ - Name: "migrate", - Usage: "Migrate the database", - Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, - Action: runMigrate, +func newMigrateCommand() *cli.Command { + return &cli.Command{ + Name: "migrate", + Usage: "Migrate the database", + Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, + Action: runMigrate, + } } func runMigrate(ctx context.Context, c *cli.Command) error { diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index a6bf9fa4b54..c9b82055c13 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -25,107 +25,108 @@ import ( "github.com/urfave/cli/v3" ) -// CmdMigrateStorage represents the available migrate storage sub-command. -var CmdMigrateStorage = &cli.Command{ - Name: "migrate-storage", - Usage: "Migrate the storage", - Description: "Copies stored files from storage configured in app.ini to parameter-configured storage", - Action: runMigrateStorage, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Value: "", - Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'", +func newMigrateStorageCommand() *cli.Command { + return &cli.Command{ + Name: "migrate-storage", + Usage: "Migrate the storage", + Description: "Copies stored files from storage configured in app.ini to parameter-configured storage", + Action: runMigrateStorage, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "", + Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'", + }, + &cli.StringFlag{ + Name: "storage", + Aliases: []string{"s"}, + Value: "", + Usage: "New storage type: local (default), minio or azureblob", + }, + &cli.StringFlag{ + Name: "path", + Aliases: []string{"p"}, + Value: "", + Usage: "New storage placement if store is local (leave blank for default)", + }, + // Minio Storage special configurations + &cli.StringFlag{ + Name: "minio-endpoint", + Value: "", + Usage: "Minio storage endpoint", + }, + &cli.StringFlag{ + Name: "minio-access-key-id", + Value: "", + Usage: "Minio storage accessKeyID", + }, + &cli.StringFlag{ + Name: "minio-secret-access-key", + Value: "", + Usage: "Minio storage secretAccessKey", + }, + &cli.StringFlag{ + Name: "minio-bucket", + Value: "", + Usage: "Minio storage bucket", + }, + &cli.StringFlag{ + Name: "minio-location", + Value: "", + Usage: "Minio storage location to create bucket", + }, + &cli.StringFlag{ + Name: "minio-base-path", + Value: "", + Usage: "Minio storage base path on the bucket", + }, + &cli.BoolFlag{ + Name: "minio-use-ssl", + Usage: "Enable SSL for minio", + }, + &cli.BoolFlag{ + Name: "minio-insecure-skip-verify", + Usage: "Skip SSL verification", + }, + &cli.StringFlag{ + Name: "minio-checksum-algorithm", + Value: "", + Usage: "Minio checksum algorithm (default/md5)", + }, + &cli.StringFlag{ + Name: "minio-bucket-lookup-type", + Value: "", + Usage: "Minio bucket lookup type", + }, + // Azure Blob Storage special configurations + &cli.StringFlag{ + Name: "azureblob-endpoint", + Value: "", + Usage: "Azure Blob storage endpoint", + }, + &cli.StringFlag{ + Name: "azureblob-account-name", + Value: "", + Usage: "Azure Blob storage account name", + }, + &cli.StringFlag{ + Name: "azureblob-account-key", + Value: "", + Usage: "Azure Blob storage account key", + }, + &cli.StringFlag{ + Name: "azureblob-container", + Value: "", + Usage: "Azure Blob storage container", + }, + &cli.StringFlag{ + Name: "azureblob-base-path", + Value: "", + Usage: "Azure Blob storage base path", + }, }, - &cli.StringFlag{ - Name: "storage", - Aliases: []string{"s"}, - Value: "", - Usage: "New storage type: local (default), minio or azureblob", - }, - &cli.StringFlag{ - Name: "path", - Aliases: []string{"p"}, - Value: "", - Usage: "New storage placement if store is local (leave blank for default)", - }, - // Minio Storage special configurations - &cli.StringFlag{ - Name: "minio-endpoint", - Value: "", - Usage: "Minio storage endpoint", - }, - &cli.StringFlag{ - Name: "minio-access-key-id", - Value: "", - Usage: "Minio storage accessKeyID", - }, - &cli.StringFlag{ - Name: "minio-secret-access-key", - Value: "", - Usage: "Minio storage secretAccessKey", - }, - &cli.StringFlag{ - Name: "minio-bucket", - Value: "", - Usage: "Minio storage bucket", - }, - &cli.StringFlag{ - Name: "minio-location", - Value: "", - Usage: "Minio storage location to create bucket", - }, - &cli.StringFlag{ - Name: "minio-base-path", - Value: "", - Usage: "Minio storage base path on the bucket", - }, - &cli.BoolFlag{ - Name: "minio-use-ssl", - Usage: "Enable SSL for minio", - }, - &cli.BoolFlag{ - Name: "minio-insecure-skip-verify", - Usage: "Skip SSL verification", - }, - &cli.StringFlag{ - Name: "minio-checksum-algorithm", - Value: "", - Usage: "Minio checksum algorithm (default/md5)", - }, - &cli.StringFlag{ - Name: "minio-bucket-lookup-type", - Value: "", - Usage: "Minio bucket lookup type", - }, - // Azure Blob Storage special configurations - &cli.StringFlag{ - Name: "azureblob-endpoint", - Value: "", - Usage: "Azure Blob storage endpoint", - }, - &cli.StringFlag{ - Name: "azureblob-account-name", - Value: "", - Usage: "Azure Blob storage account name", - }, - &cli.StringFlag{ - Name: "azureblob-account-key", - Value: "", - Usage: "Azure Blob storage account key", - }, - &cli.StringFlag{ - Name: "azureblob-container", - Value: "", - Usage: "Azure Blob storage container", - }, - &cli.StringFlag{ - Name: "azureblob-base-path", - Value: "", - Usage: "Azure Blob storage base path", - }, - }, + } } func migrateAttachments(ctx context.Context, dstStorage storage.ObjectStorage) error { diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index c61f5a582ef..26b4682f136 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -13,40 +13,41 @@ import ( "github.com/urfave/cli/v3" ) -// CmdRestoreRepository represents the available restore a repository sub-command. -var CmdRestoreRepository = &cli.Command{ - Name: "restore-repo", - Usage: "Restore the repository from disk", - Description: "This is a command for restoring the repository data.", - Action: runRestoreRepository, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "repo_dir", - Aliases: []string{"r"}, - Value: "./data", - Usage: "Repository dir path to restore from", - }, - &cli.StringFlag{ - Name: "owner_name", - Value: "", - Usage: "Restore destination owner name", - }, - &cli.StringFlag{ - Name: "repo_name", - Value: "", - Usage: "Restore destination repository name", - }, - &cli.StringFlag{ - Name: "units", - Value: "", - Usage: `Which items will be restored, one or more units should be separated as comma. +func newRestoreRepositoryCommand() *cli.Command { + return &cli.Command{ + Name: "restore-repo", + Usage: "Restore the repository from disk", + Description: "This is a command for restoring the repository data.", + Action: runRestoreRepository, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "repo_dir", + Aliases: []string{"r"}, + Value: "./data", + Usage: "Repository dir path to restore from", + }, + &cli.StringFlag{ + Name: "owner_name", + Value: "", + Usage: "Restore destination owner name", + }, + &cli.StringFlag{ + Name: "repo_name", + Value: "", + Usage: "Restore destination repository name", + }, + &cli.StringFlag{ + Name: "units", + Value: "", + Usage: `Which items will be restored, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, + }, + &cli.BoolFlag{ + Name: "validation", + Usage: "Sanity check the content of the files before trying to load them", + }, }, - &cli.BoolFlag{ - Name: "validation", - Usage: "Sanity check the content of the files before trying to load them", - }, - }, + } } func runRestoreRepository(ctx context.Context, c *cli.Command) error { diff --git a/cmd/serv.go b/cmd/serv.go index 4110fda0d50..a35d476c860 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -35,22 +35,23 @@ import ( "github.com/urfave/cli/v3" ) -// CmdServ represents the available serv sub-command. -var CmdServ = &cli.Command{ - Name: "serv", - Usage: "(internal) Should only be called by SSH shell", - Description: "Serv provides access auth for repositories", - Hidden: true, // Internal commands shouldn't be visible in help - Before: PrepareConsoleLoggerLevel(log.FATAL), - Action: runServ, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "enable-pprof", +func newServCommand() *cli.Command { + return &cli.Command{ + Name: "serv", + Usage: "(internal) Should only be called by SSH shell", + Description: "Serv provides access auth for repositories", + Hidden: true, // Internal commands shouldn't be visible in help + Before: PrepareConsoleLoggerLevel(log.FATAL), + Action: runServ, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "enable-pprof", + }, + &cli.BoolFlag{ + Name: "debug", + }, }, - &cli.BoolFlag{ - Name: "debug", - }, - }, + } } func setup(ctx context.Context, debug bool) { diff --git a/cmd/web.go b/cmd/web.go index 5000e780c56..994c481fc08 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -34,42 +34,43 @@ import ( // PIDFile could be set from build tag var PIDFile = "/run/gitea.pid" -// CmdWeb represents the available web sub-command. -var CmdWeb = &cli.Command{ - Name: "web", - Usage: "Start Gitea web server", - Description: `Gitea web server is the only thing you need to run, +func newWebCommand() *cli.Command { + return &cli.Command{ + Name: "web", + Usage: "Start Gitea web server", + Description: `Gitea web server is the only thing you need to run, and it takes care of all the other things for you`, - Before: PrepareConsoleLoggerLevel(log.INFO), - Action: runWeb, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "port", - Aliases: []string{"p"}, - Value: "3000", - Usage: "Temporary port number to prevent conflict", + Before: PrepareConsoleLoggerLevel(log.INFO), + Action: runWeb, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "port", + Aliases: []string{"p"}, + Value: "3000", + Usage: "Temporary port number to prevent conflict", + }, + &cli.StringFlag{ + Name: "install-port", + Value: "3000", + Usage: "Temporary port number to run the install page on to prevent conflict", + }, + &cli.StringFlag{ + Name: "pid", + Aliases: []string{"P"}, + Value: PIDFile, + Usage: "Custom pid file path", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display Fatal logging errors until logging is set-up", + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Set initial logging to TRACE level until logging is properly set-up", + }, }, - &cli.StringFlag{ - Name: "install-port", - Value: "3000", - Usage: "Temporary port number to run the install page on to prevent conflict", - }, - &cli.StringFlag{ - Name: "pid", - Aliases: []string{"P"}, - Value: PIDFile, - Usage: "Custom pid file path", - }, - &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Only display Fatal logging errors until logging is set-up", - }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Set initial logging to TRACE level until logging is properly set-up", - }, - }, + } } func runHTTPRedirector() { diff --git a/tests/integration/cmd_keys_test.go b/tests/integration/cmd_keys_test.go index d911bdf17d2..b71d023f72f 100644 --- a/tests/integration/cmd_keys_test.go +++ b/tests/integration/cmd_keys_test.go @@ -10,7 +10,6 @@ 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" @@ -38,13 +37,14 @@ 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 + keysCmd := cmd.NewKeysCommand() + keysCmd.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}, + Commands: []*cli.Command{keysCmd}, } err := app.Run(t.Context(), append([]string{"prog"}, tt.args...)) if tt.wantErr { From bc5c55407234c1c4cbc213fe4ae08083d1000d1e Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Wed, 25 Mar 2026 17:37:48 +0100 Subject: [PATCH 08/16] Feature non-zipped actions artifacts (action v7) (#36786) - content_encoding contains a slash => v4 artifact - updated proto files to support mime_type and no longer return errors for upload-artifact v7 - json and txt files are now previewed in browser - normalized content-disposition header creation - azure blob storage uploads directly in servedirect mode (no proxying data) - normalize content-disposition headers based on go mime package - getting both filename and filename* encoding is done via custom code Closes #36829 ----- Signed-off-by: ChristopherHX Co-authored-by: wxiaoguang --- models/actions/artifact.go | 38 +- models/fixtures/action_artifact.yml | 36 ++ modules/actions/artifacts.go | 51 +- modules/httplib/content_disposition.go | 65 ++ modules/httplib/content_disposition_test.go | 64 ++ modules/httplib/serve.go | 134 ++-- modules/httplib/serve_test.go | 8 +- modules/lfs/content_store.go | 2 +- modules/storage/minio.go | 6 +- modules/storage/storage.go | 24 +- modules/storage/storage_test.go | 40 +- modules/typesniffer/typesniffer.go | 4 + routers/api/actions/artifact.pb.go | 607 ++++++------------ routers/api/actions/artifact.proto | 3 + routers/api/actions/artifacts.go | 4 +- routers/api/actions/artifacts_chunks.go | 20 +- routers/api/actions/artifactsv4.go | 183 ++++-- routers/api/v1/repo/action.go | 18 +- routers/api/v1/repo/file.go | 39 +- routers/common/actions.go | 6 +- routers/common/serve.go | 29 +- routers/web/admin/diagnosis.go | 8 +- routers/web/repo/actions/view.go | 45 +- routers/web/repo/attachment.go | 4 +- routers/web/repo/download.go | 33 +- services/context/base.go | 4 +- services/lfs/server.go | 2 +- services/repository/archiver/archiver.go | 2 +- .../api_actions_artifact_v4_test.go | 350 +++++++--- 29 files changed, 1003 insertions(+), 826 deletions(-) create mode 100644 modules/httplib/content_disposition.go create mode 100644 modules/httplib/content_disposition_test.go diff --git a/models/actions/artifact.go b/models/actions/artifact.go index ec5cc0e32f1..d61afb2aed4 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -53,6 +53,11 @@ func init() { db.RegisterModel(new(ActionArtifact)) } +const ( + ContentEncodingV3Gzip = "gzip" + ContentTypeZip = "application/zip" +) + // ActionArtifact is a file that is stored in the artifact storage. type ActionArtifact struct { ID int64 `xorm:"pk autoincr"` @@ -61,16 +66,26 @@ type ActionArtifact struct { RepoID int64 `xorm:"index"` OwnerID int64 CommitSHA string - StoragePath string // The path to the artifact in the storage - FileSize int64 // The size of the artifact in bytes - FileCompressedSize int64 // The size of the artifact in bytes after gzip compression - ContentEncoding string // The content encoding of the artifact - ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it - ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it - Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete - CreatedUnix timeutil.TimeStamp `xorm:"created"` - UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` - ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired + StoragePath string // The path to the artifact in the storage + FileSize int64 // The size of the artifact in bytes + FileCompressedSize int64 // The size of the artifact in bytes after gzip compression + + // The content encoding or content type of the artifact + // * empty or null: legacy (v3) uncompressed content + // * magic string "gzip" (ContentEncodingV3Gzip): v3 gzip compressed content + // * requires gzip decoding before storing in a zip for download + // * requires gzip content-encoding header when downloaded single files within a workflow + // * mime type for "Content-Type": + // * "application/zip" (ContentTypeZip), seems to be an abuse, fortunately there is no conflict, and it won't cause problems? + // * "application/pdf", "text/html", etc.: real content type of the artifact + ContentEncodingOrType string `xorm:"content_encoding"` + + ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it + ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it + Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` + ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired } func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiredDays int64) (*ActionArtifact, error) { @@ -156,7 +171,8 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond { } if opts.FinalizedArtifactsV4 { cond = cond.And(builder.Eq{"status": ArtifactStatusUploadConfirmed}.Or(builder.Eq{"status": ArtifactStatusExpired})) - cond = cond.And(builder.Eq{"content_encoding": "application/zip"}) + // see the comment of ActionArtifact.ContentEncodingOrType: "*/*" means the field is a content type + cond = cond.And(builder.Like{"content_encoding", "%/%"}) } return cond diff --git a/models/fixtures/action_artifact.yml b/models/fixtures/action_artifact.yml index ee8ef0d5cec..a25dfc205c4 100644 --- a/models/fixtures/action_artifact.yml +++ b/models/fixtures/action_artifact.yml @@ -141,3 +141,39 @@ created_unix: 1730330775 updated_unix: 1730330775 expired_unix: 1738106775 + +- + id: 26 + run_id: 792 + runner_id: 1 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/pdf" + artifact_path: "report.pdf" + artifact_name: "report.pdf" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 + +- + id: 27 + run_id: 792 + runner_id: 1 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/html" + artifact_path: "report.html" + artifact_name: "report.html" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 diff --git a/modules/actions/artifacts.go b/modules/actions/artifacts.go index e8bf70ec310..4884eb42e80 100644 --- a/modules/actions/artifacts.go +++ b/modules/actions/artifacts.go @@ -5,44 +5,61 @@ package actions import ( "net/http" + "strings" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/services/context" ) -// Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend -// The v4 backend ensures ContentEncoding is set to "application/zip", which is not the case for the old backend +// IsArtifactV4 detects whether the artifact is likely from v4. +// V4 backend stores the files as a single combined zip file per artifact, and ensures ContentEncoding contains a slash +// (otherwise this uses application/zip instead of the custom mime type), which is not the case for the old backend. func IsArtifactV4(art *actions_model.ActionArtifact) bool { - return art.ArtifactName+".zip" == art.ArtifactPath && art.ContentEncoding == "application/zip" + return strings.Contains(art.ContentEncodingOrType, "/") } -func DownloadArtifactV4ServeDirectOnly(ctx *context.Base, art *actions_model.ActionArtifact) (bool, error) { - if setting.Actions.ArtifactStorage.ServeDirect() { - u, err := storage.ActionsArtifacts.ServeDirectURL(art.StoragePath, art.ArtifactPath, ctx.Req.Method, nil) - if u != nil && err == nil { - ctx.Redirect(u.String(), http.StatusFound) - return true, nil - } +func GetArtifactV4ServeDirectURL(art *actions_model.ActionArtifact, method string) (string, error) { + contentType := art.ContentEncodingOrType + u, err := storage.ActionsArtifacts.ServeDirectURL(art.StoragePath, art.ArtifactPath, method, &storage.ServeDirectOptions{ContentType: contentType}) + if err != nil { + return "", err } - return false, nil + return u.String(), nil } -func DownloadArtifactV4Fallback(ctx *context.Base, art *actions_model.ActionArtifact) error { +func DownloadArtifactV4ServeDirect(ctx *context.Base, art *actions_model.ActionArtifact) bool { + if !setting.Actions.ArtifactStorage.ServeDirect() { + return false + } + u, err := GetArtifactV4ServeDirectURL(art, ctx.Req.Method) + if err != nil { + log.Error("GetArtifactV4ServeDirectURL: %v", err) + return false + } + ctx.Redirect(u, http.StatusFound) + return true +} + +func DownloadArtifactV4ReadStorage(ctx *context.Base, art *actions_model.ActionArtifact) error { f, err := storage.ActionsArtifacts.Open(art.StoragePath) if err != nil { return err } defer f.Close() - http.ServeContent(ctx.Resp, ctx.Req, art.ArtifactName+".zip", art.CreatedUnix.AsLocalTime(), f) + httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, f, httplib.ServeHeaderOptions{ + Filename: art.ArtifactPath, + ContentType: art.ContentEncodingOrType, // v4 guarantees that the field is Content-Type + }) return nil } func DownloadArtifactV4(ctx *context.Base, art *actions_model.ActionArtifact) error { - ok, err := DownloadArtifactV4ServeDirectOnly(ctx, art) - if ok || err != nil { - return err + if DownloadArtifactV4ServeDirect(ctx, art) { + return nil } - return DownloadArtifactV4Fallback(ctx, art) + return DownloadArtifactV4ReadStorage(ctx, art) } diff --git a/modules/httplib/content_disposition.go b/modules/httplib/content_disposition.go new file mode 100644 index 00000000000..da23dae2210 --- /dev/null +++ b/modules/httplib/content_disposition.go @@ -0,0 +1,65 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package httplib + +import ( + "mime" + "strings" + + "code.gitea.io/gitea/modules/setting" +) + +type ContentDispositionType string + +const ( + ContentDispositionInline ContentDispositionType = "inline" + ContentDispositionAttachment ContentDispositionType = "attachment" +) + +func needsEncodingRune(b rune) bool { + return (b < ' ' || b > '~') && b != '\t' +} + +// getSafeName replaces all invalid chars in the filename field by underscore +func getSafeName(s string) (_ string, needsEncoding bool) { + var out strings.Builder + for _, b := range s { + if needsEncodingRune(b) { + needsEncoding = true + out.WriteRune('_') + } else { + out.WriteRune(b) + } + } + return out.String(), needsEncoding +} + +func EncodeContentDispositionAttachment(filename string) string { + return encodeContentDisposition(ContentDispositionAttachment, filename) +} + +func EncodeContentDispositionInline(filename string) string { + return encodeContentDisposition(ContentDispositionInline, filename) +} + +// encodeContentDisposition encodes a correct Content-Disposition Header +func encodeContentDisposition(t ContentDispositionType, filename string) string { + safeFilename, needsEncoding := getSafeName(filename) + result := mime.FormatMediaType(string(t), map[string]string{"filename": safeFilename}) + // No need for the utf8 encoding + if !needsEncoding { + return result + } + utf8Result := mime.FormatMediaType(string(t), map[string]string{"filename": filename}) + + // The mime package might have unexpected results in other go versions + // Make tests instance fail, otherwise use the default behavior of the go mime package + if !strings.HasPrefix(result, string(t)+"; filename=") || !strings.HasPrefix(utf8Result, string(t)+"; filename*=") { + setting.PanicInDevOrTesting("Unexpected mime package result %s", result) + return utf8Result + } + + encodedFileName := strings.TrimPrefix(utf8Result, string(t)) + return result + encodedFileName +} diff --git a/modules/httplib/content_disposition_test.go b/modules/httplib/content_disposition_test.go new file mode 100644 index 00000000000..bf5040e1075 --- /dev/null +++ b/modules/httplib/content_disposition_test.go @@ -0,0 +1,64 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package httplib + +import ( + "mime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestContentDisposition(t *testing.T) { + type testEntry struct { + disposition ContentDispositionType + filename string + header string + } + table := []testEntry{ + {disposition: ContentDispositionInline, filename: "test.txt", header: "inline; filename=test.txt"}, + {disposition: ContentDispositionInline, filename: "test❌.txt", header: "inline; filename=test_.txt; filename*=utf-8''test%E2%9D%8C.txt"}, + {disposition: ContentDispositionInline, filename: "test ❌.txt", header: "inline; filename=\"test _.txt\"; filename*=utf-8''test%20%E2%9D%8C.txt"}, + {disposition: ContentDispositionInline, filename: "\"test.txt", header: "inline; filename=\"\\\"test.txt\""}, + {disposition: ContentDispositionInline, filename: "hello\tworld.txt", header: "inline; filename=\"hello\tworld.txt\""}, + {disposition: ContentDispositionAttachment, filename: "hello\tworld.txt", header: "attachment; filename=\"hello\tworld.txt\""}, + {disposition: ContentDispositionAttachment, filename: "hello\nworld.txt", header: "attachment; filename=hello_world.txt; filename*=utf-8''hello%0Aworld.txt"}, + {disposition: ContentDispositionAttachment, filename: "hello\rworld.txt", header: "attachment; filename=hello_world.txt; filename*=utf-8''hello%0Dworld.txt"}, + } + + // Check the needsEncodingRune replacer ranges except tab that is checked above + // Any change in behavior should fail here + for c := ' '; !needsEncodingRune(c); c++ { + var header string + switch { + case strings.ContainsAny(string(c), ` (),/:;<=>?@[]`): + header = "inline; filename=\"hello" + string(c) + "world.txt\"" + case strings.ContainsAny(string(c), `"\`): + // This document advises against for backslash in quoted form: + // https://datatracker.ietf.org/doc/html/rfc6266#appendix-D + // However the mime package is not generating the filename* in this scenario + header = "inline; filename=\"hello\\" + string(c) + "world.txt\"" + default: + header = "inline; filename=hello" + string(c) + "world.txt" + } + table = append(table, testEntry{ + disposition: ContentDispositionInline, + filename: "hello" + string(c) + "world.txt", + header: header, + }) + } + + for _, entry := range table { + t.Run(string(entry.disposition)+"_"+entry.filename, func(t *testing.T) { + encoded := encodeContentDisposition(entry.disposition, entry.filename) + assert.Equal(t, entry.header, encoded) + disposition, params, err := mime.ParseMediaType(encoded) + require.NoError(t, err) + assert.Equal(t, string(entry.disposition), disposition) + assert.Equal(t, entry.filename, params["filename"]) + }) + } +} diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index fc7edc36c43..e8299d1c805 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -8,10 +8,9 @@ import ( "errors" "fmt" "io" + "io/fs" "net/http" - "net/url" "path" - "path/filepath" "strconv" "strings" "time" @@ -27,18 +26,19 @@ import ( ) type ServeHeaderOptions struct { - ContentType string // defaults to "application/octet-stream" - ContentTypeCharset string - ContentLength *int64 - Disposition string // defaults to "attachment" + ContentType string // defaults to "application/octet-stream" + ContentLength *int64 + Filename string - CacheIsPublic bool - CacheDuration time.Duration // defaults to 5 minutes - LastModified time.Time + ContentDisposition ContentDispositionType + + CacheIsPublic bool + CacheDuration time.Duration // defaults to 5 minutes + LastModified time.Time } // ServeSetHeaders sets necessary content serve headers -func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { +func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) { header := w.Header() skipCompressionExts := container.SetOf(".gz", ".bz2", ".zip", ".xz", ".zst", ".deb", ".apk", ".jar", ".png", ".jpg", ".webp") @@ -46,14 +46,7 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { w.Header().Add(gzhttp.HeaderNoCompression, "1") } - contentType := typesniffer.MimeTypeApplicationOctetStream - if opts.ContentType != "" { - if opts.ContentTypeCharset != "" { - contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset) - } else { - contentType = opts.ContentType - } - } + contentType := util.IfZero(opts.ContentType, typesniffer.MimeTypeApplicationOctetStream) header.Set("Content-Type", contentType) header.Set("X-Content-Type-Options", "nosniff") @@ -61,14 +54,18 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10)) } - if opts.Filename != "" { - disposition := opts.Disposition - if disposition == "" { - disposition = "attachment" - } + // Disable script execution of HTML/SVG files, since we serve the file from the same origin as Gitea server + header.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox") + if strings.Contains(contentType, "application/pdf") { + // no sandbox attribute for PDF as it breaks rendering in at least safari. this + // should generally be safe as scripts inside PDF can not escape the PDF document + // see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion + // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context + header.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") + } - backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \" - header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename))) + if opts.Filename != "" && opts.ContentDisposition != "" { + header.Set("Content-Disposition", encodeContentDisposition(opts.ContentDisposition, path.Base(opts.Filename))) header.Set("Access-Control-Expose-Headers", "Content-Disposition") } @@ -84,49 +81,40 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { } } -// ServeData download file from io.Reader -func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byte, opts *ServeHeaderOptions) { - // do not set "Content-Length", because the length could only be set by callers, and it needs to support range requests - sniffedType := typesniffer.DetectContentType(mineBuf) - - // the "render" parameter came from year 2016: 638dd24c, it doesn't have clear meaning, so I think it could be removed later - isPlain := sniffedType.IsText() || r.FormValue("render") != "" +func serveSetHeadersByUserContent(w http.ResponseWriter, contentPrefetchBuf []byte, opts ServeHeaderOptions) { + var detectCharset bool if setting.MimeTypeMap.Enabled { - fileExtension := strings.ToLower(filepath.Ext(opts.Filename)) + fileExtension := strings.ToLower(path.Ext(opts.Filename)) opts.ContentType = setting.MimeTypeMap.Map[fileExtension] + detectCharset = !strings.Contains(opts.ContentType, "charset=") } if opts.ContentType == "" { + sniffedType := typesniffer.DetectContentType(contentPrefetchBuf) if sniffedType.IsBrowsableBinaryType() { opts.ContentType = sniffedType.GetMimeType() - } else if isPlain { + } else if sniffedType.IsText() { + // intentionally do not render user's HTML content as a page, for safety, and avoid content spamming & abusing opts.ContentType = "text/plain" + detectCharset = true } else { opts.ContentType = typesniffer.MimeTypeApplicationOctetStream } } - if isPlain { - charset, _ := charsetModule.DetectEncoding(mineBuf) - opts.ContentTypeCharset = strings.ToLower(charset) + if detectCharset { + if charset, _ := charsetModule.DetectEncoding(contentPrefetchBuf); charset != "" { + opts.ContentType += "; charset=" + strings.ToLower(charset) + } } - // serve types that can present a security risk with CSP - w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox") - - if sniffedType.IsPDF() { - // no sandbox attribute for PDF as it breaks rendering in at least safari. this - // should generally be safe as scripts inside PDF can not escape the PDF document - // see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion - // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context - w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") - } - - // TODO: UNIFY-CONTENT-DISPOSITION-FROM-STORAGE - opts.Disposition = "inline" - if sniffedType.IsSvgImage() && !setting.UI.SVG.Enabled { - opts.Disposition = "attachment" + if opts.ContentDisposition == "" { + sniffedType := typesniffer.FromContentType(opts.ContentType) + opts.ContentDisposition = ContentDispositionInline + if sniffedType.IsSvgImage() && !setting.UI.SVG.Enabled { + opts.ContentDisposition = ContentDispositionAttachment + } } ServeSetHeaders(w, opts) @@ -134,7 +122,10 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byt const mimeDetectionBufferLen = 1024 -func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, reader io.Reader, opts *ServeHeaderOptions) { +func ServeUserContentByReader(r *http.Request, w http.ResponseWriter, size int64, reader io.Reader, opts ServeHeaderOptions) { + if opts.ContentLength != nil { + panic("do not set ContentLength, use size argument instead") + } buf := make([]byte, mimeDetectionBufferLen) n, err := util.ReadAtMost(reader, buf) if err != nil { @@ -144,7 +135,7 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, re if n >= 0 { buf = buf[:n] } - setServeHeadersByFile(r, w, buf, opts) + serveSetHeadersByUserContent(w, buf, opts) // reset the reader to the beginning reader = io.MultiReader(bytes.NewReader(buf), reader) @@ -198,32 +189,29 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, re partialLength := end - start + 1 w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size)) w.Header().Set("Content-Length", strconv.FormatInt(partialLength, 10)) - if _, err = io.CopyN(io.Discard, reader, start); err != nil { - http.Error(w, "serve content: unable to skip", http.StatusInternalServerError) - return + + if seeker, ok := reader.(io.Seeker); ok { + if _, err = seeker.Seek(start, io.SeekStart); err != nil { + http.Error(w, "serve content: unable to seek", http.StatusInternalServerError) + return + } + } else { + if _, err = io.CopyN(io.Discard, reader, start); err != nil { + http.Error(w, "serve content: unable to skip", http.StatusInternalServerError) + return + } } w.WriteHeader(http.StatusPartialContent) _, _ = io.CopyN(w, reader, partialLength) // just like http.ServeContent, not necessary to handle the error } -func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, modTime *time.Time, reader io.ReadSeeker, opts *ServeHeaderOptions) { - buf := make([]byte, mimeDetectionBufferLen) - n, err := util.ReadAtMost(reader, buf) +func ServeUserContentByFile(r *http.Request, w http.ResponseWriter, file fs.File, opts ServeHeaderOptions) { + info, err := file.Stat() if err != nil { - http.Error(w, "serve content: unable to read", http.StatusInternalServerError) + http.Error(w, "unable to serve file, stat error", http.StatusInternalServerError) return } - if _, err = reader.Seek(0, io.SeekStart); err != nil { - http.Error(w, "serve content: unable to seek", http.StatusInternalServerError) - return - } - if n >= 0 { - buf = buf[:n] - } - setServeHeadersByFile(r, w, buf, opts) - if modTime == nil { - modTime = &time.Time{} - } - http.ServeContent(w, r, opts.Filename, *modTime, reader) + opts.LastModified = info.ModTime() + ServeUserContentByReader(r, w, info.Size(), file, opts) } diff --git a/modules/httplib/serve_test.go b/modules/httplib/serve_test.go index 78b88c9b5f1..38cf4c197f7 100644 --- a/modules/httplib/serve_test.go +++ b/modules/httplib/serve_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestServeContentByReader(t *testing.T) { +func TestServeUserContentByReader(t *testing.T) { data := "0123456789abcdef" test := func(t *testing.T, expectedStatusCode int, expectedContent string) { @@ -27,7 +27,7 @@ func TestServeContentByReader(t *testing.T) { } reader := strings.NewReader(data) w := httptest.NewRecorder() - ServeContentByReader(r, w, int64(len(data)), reader, &ServeHeaderOptions{}) + ServeUserContentByReader(r, w, int64(len(data)), reader, ServeHeaderOptions{}) assert.Equal(t, expectedStatusCode, w.Code) if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK { assert.Equal(t, strconv.Itoa(len(expectedContent)), w.Header().Get("Content-Length")) @@ -58,7 +58,7 @@ func TestServeContentByReader(t *testing.T) { }) } -func TestServeContentByReadSeeker(t *testing.T) { +func TestServeUserContentByFile(t *testing.T) { data := "0123456789abcdef" tmpFile := t.TempDir() + "/test" err := os.WriteFile(tmpFile, []byte(data), 0o644) @@ -76,7 +76,7 @@ func TestServeContentByReadSeeker(t *testing.T) { defer seekReader.Close() w := httptest.NewRecorder() - ServeContentByReadSeeker(r, w, nil, seekReader, &ServeHeaderOptions{}) + ServeUserContentByFile(r, w, seekReader, ServeHeaderOptions{}) assert.Equal(t, expectedStatusCode, w.Code) if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK { assert.Equal(t, strconv.Itoa(len(expectedContent)), w.Header().Get("Content-Length")) diff --git a/modules/lfs/content_store.go b/modules/lfs/content_store.go index 0d9c0c98acc..be1e6c8e90c 100644 --- a/modules/lfs/content_store.go +++ b/modules/lfs/content_store.go @@ -104,7 +104,7 @@ func (s *ContentStore) Verify(pointer Pointer) (bool, error) { } // ReadMetaObject will read a git_model.LFSMetaObject and return a reader -func ReadMetaObject(pointer Pointer) (io.ReadSeekCloser, error) { +func ReadMetaObject(pointer Pointer) (storage.Object, error) { contentStore := NewContentStore() return contentStore.Get(pointer) } diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 1355280f367..ace78bb6105 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -23,11 +23,7 @@ import ( "github.com/minio/minio-go/v7/pkg/credentials" ) -var ( - _ ObjectStorage = &MinioStorage{} - - quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") -) +var _ ObjectStorage = &MinioStorage{} type minioObject struct { *minio.Object diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 2491c77a3e0..e19c421ba82 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -12,6 +12,7 @@ import ( "os" "path" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" @@ -62,31 +63,30 @@ type Object interface { type ServeDirectOptions struct { // Overrides the automatically detected MIME type. ContentType string - // Overrides the default Content-Disposition header, which is `inline; filename="name"`. - ContentDisposition string } // Safe defaults are applied only when not explicitly overridden by the caller. -func prepareServeDirectOptions(optsOptional *ServeDirectOptions, name string) (ret ServeDirectOptions) { +func prepareServeDirectOptions(optsOptional *ServeDirectOptions, name string) (ret struct { + ContentType string + ContentDisposition string +}, +) { // Here we might not know the real filename, and it's quite inefficient to detect the MIME type by pre-fetching the object head. // So we just do a quick detection by extension name, at least it works for the "View Raw File" for an LFS file on the Web UI. // TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future if optsOptional != nil { - ret = *optsOptional + ret.ContentType = optsOptional.ContentType } - - // TODO: UNIFY-CONTENT-DISPOSITION-FROM-STORAGE + name = path.Base(name) if ret.ContentType == "" { ext := path.Ext(name) ret.ContentType = public.DetectWellKnownMimeType(ext) } - if ret.ContentDisposition == "" { - // When using ServeDirect, the URL is from the object storage's web server, - // it is not the same origin as Gitea server, so it should be safe enough to use "inline" to render the content directly. - // If a browser doesn't support the content type to be displayed inline, browser will download with the filename. - ret.ContentDisposition = fmt.Sprintf(`inline; filename="%s"`, quoteEscaper.Replace(name)) - } + // When using ServeDirect, the URL is from the object storage's web server, + // it is not the same origin as Gitea server, so it should be safe enough to use "inline" to render the content directly. + // If a browser doesn't support the content type to be displayed inline, browser will download with the filename. + ret.ContentDisposition = httplib.EncodeContentDispositionInline(name) return ret } diff --git a/modules/storage/storage_test.go b/modules/storage/storage_test.go index 4156723c364..83ee2ef7934 100644 --- a/modules/storage/storage_test.go +++ b/modules/storage/storage_test.go @@ -53,7 +53,12 @@ func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) { } } -func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path, name string, expected ServeDirectOptions, reqParams *ServeDirectOptions) { +type expectedServeDirectHeaders struct { + ContentType string + ContentDisposition string +} + +func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path, name string, expected expectedServeDirectHeaders, reqParams *ServeDirectOptions) { u, err := s.ServeDirectURL(path, name, http.MethodGet, reqParams) require.NoError(t, err) resp, err := http.Get(u.String()) @@ -71,36 +76,29 @@ func testBlobStorageURLContentTypeAndDisposition(t *testing.T, typStr Type, cfg s, err := NewStorage(typStr, cfg) assert.NoError(t, err) - data := "Q2xTckt6Y1hDOWh0" // arbitrary test content; specific value is irrelevant to this test - testfilename := "test.txt" // arbitrary file name; specific value is irrelevant to this test - _, err = s.Save(testfilename, strings.NewReader(data), int64(len(data))) + testFilename := "test.txt" + _, err = s.Save(testFilename, strings.NewReader("dummy-content"), -1) assert.NoError(t, err) - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.txt", ServeDirectOptions{ + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.txt", expectedServeDirectHeaders{ ContentType: "text/plain; charset=utf-8", - ContentDisposition: `inline; filename="test.txt"`, + ContentDisposition: `inline; filename=test.txt`, }, nil) - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.pdf", ServeDirectOptions{ + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.pdf", expectedServeDirectHeaders{ ContentType: "application/pdf", - ContentDisposition: `inline; filename="test.pdf"`, + ContentDisposition: `inline; filename=test.pdf`, }, nil) - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.wasm", ServeDirectOptions{ - ContentDisposition: `inline; filename="test.wasm"`, + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{ + ContentDisposition: `inline; filename=test.wasm`, }, nil) - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.wasm", ServeDirectOptions{ - ContentDisposition: `inline; filename="test.wasm"`, - }, &ServeDirectOptions{}) - - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.txt", ServeDirectOptions{ - ContentType: "application/octet-stream", - ContentDisposition: `inline; filename="test.xml"`, + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{ + ContentType: "application/wasm", + ContentDisposition: `inline; filename=test.wasm`, }, &ServeDirectOptions{ - ContentType: "application/octet-stream", - ContentDisposition: `inline; filename="test.xml"`, + ContentType: "application/wasm", }) - - assert.NoError(t, s.Delete(testfilename)) + assert.NoError(t, s.Delete(testFilename)) } diff --git a/modules/typesniffer/typesniffer.go b/modules/typesniffer/typesniffer.go index 0c4867d8f01..90423d48ce3 100644 --- a/modules/typesniffer/typesniffer.go +++ b/modules/typesniffer/typesniffer.go @@ -183,3 +183,7 @@ func DetectContentType(data []byte) SniffedType { } return SniffedType{ct} } + +func FromContentType(contentType string) SniffedType { + return SniffedType{contentType} +} diff --git a/routers/api/actions/artifact.pb.go b/routers/api/actions/artifact.pb.go index 590eda9fb9a..130e20301fa 100644 --- a/routers/api/actions/artifact.pb.go +++ b/routers/api/actions/artifact.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 -// protoc v4.25.2 +// protoc-gen-go v1.36.11 +// protoc v7.34.0 // source: artifact.proto package actions @@ -12,6 +12,7 @@ package actions import ( reflect "reflect" sync "sync" + unsafe "unsafe" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -27,24 +28,22 @@ const ( ) type CreateArtifactRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` - WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` - Version int32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + Version int32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` + MimeType *wrapperspb.StringValue `protobuf:"bytes,6,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateArtifactRequest) Reset() { *x = CreateArtifactRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateArtifactRequest) String() string { @@ -55,7 +54,7 @@ func (*CreateArtifactRequest) ProtoMessage() {} func (x *CreateArtifactRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -105,22 +104,26 @@ func (x *CreateArtifactRequest) GetVersion() int32 { return 0 } -type CreateArtifactResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *CreateArtifactRequest) GetMimeType() *wrapperspb.StringValue { + if x != nil { + return x.MimeType + } + return nil +} - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"` +type CreateArtifactResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateArtifactResponse) Reset() { *x = CreateArtifactResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateArtifactResponse) String() string { @@ -131,7 +134,7 @@ func (*CreateArtifactResponse) ProtoMessage() {} func (x *CreateArtifactResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -161,24 +164,21 @@ func (x *CreateArtifactResponse) GetSignedUploadUrl() string { } type FinalizeArtifactRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` Hash *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=hash,proto3" json:"hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FinalizeArtifactRequest) Reset() { *x = FinalizeArtifactRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FinalizeArtifactRequest) String() string { @@ -189,7 +189,7 @@ func (*FinalizeArtifactRequest) ProtoMessage() {} func (x *FinalizeArtifactRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -240,21 +240,18 @@ func (x *FinalizeArtifactRequest) GetHash() *wrapperspb.StringValue { } type FinalizeArtifactResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` unknownFields protoimpl.UnknownFields - - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` + sizeCache protoimpl.SizeCache } func (x *FinalizeArtifactResponse) Reset() { *x = FinalizeArtifactResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FinalizeArtifactResponse) String() string { @@ -265,7 +262,7 @@ func (*FinalizeArtifactResponse) ProtoMessage() {} func (x *FinalizeArtifactResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -295,23 +292,20 @@ func (x *FinalizeArtifactResponse) GetArtifactId() int64 { } type ListArtifactsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` NameFilter *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=name_filter,json=nameFilter,proto3" json:"name_filter,omitempty"` IdFilter *wrapperspb.Int64Value `protobuf:"bytes,4,opt,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListArtifactsRequest) Reset() { *x = ListArtifactsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListArtifactsRequest) String() string { @@ -322,7 +316,7 @@ func (*ListArtifactsRequest) ProtoMessage() {} func (x *ListArtifactsRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -366,20 +360,17 @@ func (x *ListArtifactsRequest) GetIdFilter() *wrapperspb.Int64Value { } type ListArtifactsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"` unknownFields protoimpl.UnknownFields - - Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListArtifactsResponse) Reset() { *x = ListArtifactsResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListArtifactsResponse) String() string { @@ -390,7 +381,7 @@ func (*ListArtifactsResponse) ProtoMessage() {} func (x *ListArtifactsResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -413,25 +404,22 @@ func (x *ListArtifactsResponse) GetArtifacts() []*ListArtifactsResponse_Monolith } type ListArtifactsResponse_MonolithArtifact struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` DatabaseId int64 `protobuf:"varint,3,opt,name=database_id,json=databaseId,proto3" json:"database_id,omitempty"` Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"` CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListArtifactsResponse_MonolithArtifact) Reset() { *x = ListArtifactsResponse_MonolithArtifact{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListArtifactsResponse_MonolithArtifact) String() string { @@ -442,7 +430,7 @@ func (*ListArtifactsResponse_MonolithArtifact) ProtoMessage() {} func (x *ListArtifactsResponse_MonolithArtifact) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -500,22 +488,19 @@ func (x *ListArtifactsResponse_MonolithArtifact) GetCreatedAt() *timestamppb.Tim } type GetSignedArtifactURLRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` - WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetSignedArtifactURLRequest) Reset() { *x = GetSignedArtifactURLRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetSignedArtifactURLRequest) String() string { @@ -526,7 +511,7 @@ func (*GetSignedArtifactURLRequest) ProtoMessage() {} func (x *GetSignedArtifactURLRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -563,20 +548,17 @@ func (x *GetSignedArtifactURLRequest) GetName() string { } type GetSignedArtifactURLResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"` unknownFields protoimpl.UnknownFields - - SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetSignedArtifactURLResponse) Reset() { *x = GetSignedArtifactURLResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetSignedArtifactURLResponse) String() string { @@ -587,7 +569,7 @@ func (*GetSignedArtifactURLResponse) ProtoMessage() {} func (x *GetSignedArtifactURLResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -610,22 +592,19 @@ func (x *GetSignedArtifactURLResponse) GetSignedUrl() string { } type DeleteArtifactRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` - WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DeleteArtifactRequest) Reset() { *x = DeleteArtifactRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteArtifactRequest) String() string { @@ -636,7 +615,7 @@ func (*DeleteArtifactRequest) ProtoMessage() {} func (x *DeleteArtifactRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -673,21 +652,18 @@ func (x *DeleteArtifactRequest) GetName() string { } type DeleteArtifactResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` unknownFields protoimpl.UnknownFields - - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteArtifactResponse) Reset() { *x = DeleteArtifactResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteArtifactResponse) String() string { @@ -698,7 +674,7 @@ func (*DeleteArtifactResponse) ProtoMessage() {} func (x *DeleteArtifactResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -729,173 +705,105 @@ func (x *DeleteArtifactResponse) GetArtifactId() int64 { var File_artifact_proto protoreflect.FileDescriptor -var file_artifact_proto_rawDesc = []byte{ - 0x0a, 0x0e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x12, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a, - 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x22, 0xf5, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, - 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, - 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, - 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, - 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, - 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, - 0x6f, 0x6b, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x75, 0x70, 0x6c, - 0x6f, 0x61, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, - 0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xe8, - 0x01, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, - 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, - 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, - 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, - 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x4b, 0x0a, 0x18, 0x46, 0x69, 0x6e, - 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, - 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, - 0x66, 0x61, 0x63, 0x74, 0x49, 0x64, 0x22, 0x84, 0x02, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41, - 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, - 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, - 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x69, 0x6c, - 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x69, 0x6c, - 0x74, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x7c, 0x0a, - 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, - 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, - 0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x26, - 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, - 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, - 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, - 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, - 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, - 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x64, - 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, - 0x73, 0x69, 0x7a, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, - 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, - 0xa6, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, - 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, - 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, - 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x53, - 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e, - 0x65, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69, - 0x67, 0x6e, 0x65, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xa0, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, - 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, - 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, - 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, - 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x49, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_artifact_proto_rawDesc = "" + + "\n" + + "\x0eartifact.proto\x12\x1dgithub.actions.results.api.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\xb0\x02\n" + + "\x15CreateArtifactRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x129\n" + + "\n" + + "expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12\x18\n" + + "\aversion\x18\x05 \x01(\x05R\aversion\x129\n" + + "\tmime_type\x18\x06 \x01(\v2\x1c.google.protobuf.StringValueR\bmimeType\"T\n" + + "\x16CreateArtifactResponse\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12*\n" + + "\x11signed_upload_url\x18\x02 \x01(\tR\x0fsignedUploadUrl\"\xe8\x01\n" + + "\x17FinalizeArtifactRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\x120\n" + + "\x04hash\x18\x05 \x01(\v2\x1c.google.protobuf.StringValueR\x04hash\"K\n" + + "\x18FinalizeArtifactResponse\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12\x1f\n" + + "\vartifact_id\x18\x02 \x01(\x03R\n" + + "artifactId\"\x84\x02\n" + + "\x14ListArtifactsRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12=\n" + + "\vname_filter\x18\x03 \x01(\v2\x1c.google.protobuf.StringValueR\n" + + "nameFilter\x128\n" + + "\tid_filter\x18\x04 \x01(\v2\x1b.google.protobuf.Int64ValueR\bidFilter\"|\n" + + "\x15ListArtifactsResponse\x12c\n" + + "\tartifacts\x18\x01 \x03(\v2E.github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifactR\tartifacts\"\xa1\x02\n" + + "&ListArtifactsResponse_MonolithArtifact\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x1f\n" + + "\vdatabase_id\x18\x03 \x01(\x03R\n" + + "databaseId\x12\x12\n" + + "\x04name\x18\x04 \x01(\tR\x04name\x12\x12\n" + + "\x04size\x18\x05 \x01(\x03R\x04size\x129\n" + + "\n" + + "created_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\xa6\x01\n" + + "\x1bGetSignedArtifactURLRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\"=\n" + + "\x1cGetSignedArtifactURLResponse\x12\x1d\n" + + "\n" + + "signed_url\x18\x01 \x01(\tR\tsignedUrl\"\xa0\x01\n" + + "\x15DeleteArtifactRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\"I\n" + + "\x16DeleteArtifactResponse\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12\x1f\n" + + "\vartifact_id\x18\x02 \x01(\x03R\n" + + "artifactIdB)Z'code.gitea.io/gitea/routers/api/actionsb\x06proto3" var ( file_artifact_proto_rawDescOnce sync.Once - file_artifact_proto_rawDescData = file_artifact_proto_rawDesc + file_artifact_proto_rawDescData []byte ) func file_artifact_proto_rawDescGZIP() []byte { file_artifact_proto_rawDescOnce.Do(func() { - file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(file_artifact_proto_rawDescData) + file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_artifact_proto_rawDesc), len(file_artifact_proto_rawDesc))) }) return file_artifact_proto_rawDescData } -var ( - file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11) - file_artifact_proto_goTypes = []interface{}{ - (*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest - (*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse - (*FinalizeArtifactRequest)(nil), // 2: github.actions.results.api.v1.FinalizeArtifactRequest - (*FinalizeArtifactResponse)(nil), // 3: github.actions.results.api.v1.FinalizeArtifactResponse - (*ListArtifactsRequest)(nil), // 4: github.actions.results.api.v1.ListArtifactsRequest - (*ListArtifactsResponse)(nil), // 5: github.actions.results.api.v1.ListArtifactsResponse - (*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact - (*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest - (*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse - (*DeleteArtifactRequest)(nil), // 9: github.actions.results.api.v1.DeleteArtifactRequest - (*DeleteArtifactResponse)(nil), // 10: github.actions.results.api.v1.DeleteArtifactResponse - (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp - (*wrapperspb.StringValue)(nil), // 12: google.protobuf.StringValue - (*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value - } -) - +var file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_artifact_proto_goTypes = []any{ + (*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest + (*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse + (*FinalizeArtifactRequest)(nil), // 2: github.actions.results.api.v1.FinalizeArtifactRequest + (*FinalizeArtifactResponse)(nil), // 3: github.actions.results.api.v1.FinalizeArtifactResponse + (*ListArtifactsRequest)(nil), // 4: github.actions.results.api.v1.ListArtifactsRequest + (*ListArtifactsResponse)(nil), // 5: github.actions.results.api.v1.ListArtifactsResponse + (*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact + (*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest + (*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse + (*DeleteArtifactRequest)(nil), // 9: github.actions.results.api.v1.DeleteArtifactRequest + (*DeleteArtifactResponse)(nil), // 10: github.actions.results.api.v1.DeleteArtifactResponse + (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp + (*wrapperspb.StringValue)(nil), // 12: google.protobuf.StringValue + (*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value +} var file_artifact_proto_depIdxs = []int32{ 11, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp - 12, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue - 12, // 2: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue - 13, // 3: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value - 6, // 4: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact - 11, // 5: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 12, // 1: github.actions.results.api.v1.CreateArtifactRequest.mime_type:type_name -> google.protobuf.StringValue + 12, // 2: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue + 12, // 3: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue + 13, // 4: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value + 6, // 5: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact + 11, // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_artifact_proto_init() } @@ -903,145 +811,11 @@ func file_artifact_proto_init() { if File_artifact_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_artifact_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateArtifactRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateArtifactResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FinalizeArtifactRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FinalizeArtifactResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListArtifactsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListArtifactsResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListArtifactsResponse_MonolithArtifact); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetSignedArtifactURLRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetSignedArtifactURLResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteArtifactRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteArtifactResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_artifact_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_artifact_proto_rawDesc), len(file_artifact_proto_rawDesc)), NumEnums: 0, NumMessages: 11, NumExtensions: 0, @@ -1052,7 +826,6 @@ func file_artifact_proto_init() { MessageInfos: file_artifact_proto_msgTypes, }.Build() File_artifact_proto = out.File - file_artifact_proto_rawDesc = nil file_artifact_proto_goTypes = nil file_artifact_proto_depIdxs = nil } diff --git a/routers/api/actions/artifact.proto b/routers/api/actions/artifact.proto index c68e5d030d0..7da8bad564b 100644 --- a/routers/api/actions/artifact.proto +++ b/routers/api/actions/artifact.proto @@ -5,12 +5,15 @@ import "google/protobuf/wrappers.proto"; package github.actions.results.api.v1; +option go_package = "code.gitea.io/gitea/routers/api/actions"; + message CreateArtifactRequest { string workflow_run_backend_id = 1; string workflow_job_run_backend_id = 2; string name = 3; google.protobuf.Timestamp expires_at = 4; int32 version = 5; + google.protobuf.StringValue mime_type = 6; } message CreateArtifactResponse { diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 76facd769f2..a6722616cfe 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -282,7 +282,7 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) { artifact.FileCompressedSize != chunksTotalSize { artifact.FileSize = fileRealTotalSize artifact.FileCompressedSize = chunksTotalSize - artifact.ContentEncoding = ctx.Req.Header.Get("Content-Encoding") + artifact.ContentEncodingOrType = ctx.Req.Header.Get("Content-Encoding") if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error update artifact: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error update artifact") @@ -492,7 +492,7 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) { defer fd.Close() // if artifact is compressed, set content-encoding header to gzip - if artifact.ContentEncoding == "gzip" { + if artifact.ContentEncodingOrType == actions.ContentEncodingV3Gzip { ctx.Resp.Header().Set("Content-Encoding", "gzip") } log.Debug("[artifact] downloadArtifact, name: %s, path: %s, storage: %s, size: %d", artifact.ArtifactName, artifact.ArtifactPath, artifact.StoragePath, artifact.FileSize) diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index 86a51d6ca64..8d04c689221 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -285,6 +285,17 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int return nil } +func generateArtifactStoragePath(artifact *actions.ActionArtifact) string { + // if chunk is gzip, use gz as extension + // download-artifact action will use content-encoding header to decide if it should decompress the file + extension := "chunk" + if artifact.ContentEncodingOrType == actions.ContentEncodingV3Gzip { + extension = "chunk.gz" + } + + return fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension) +} + func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact, checksum string) error { sort.Slice(chunks, func(i, j int) bool { return chunks[i].Start < chunks[j].Start @@ -335,15 +346,8 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st mergedReader = io.TeeReader(mergedReader, hashSha256) } - // if chunk is gzip, use gz as extension - // download-artifact action will use content-encoding header to decide if it should decompress the file - extension := "chunk" - if artifact.ContentEncoding == "gzip" { - extension = "chunk.gz" - } - // save merged file - storagePath := fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension) + storagePath := generateArtifactStoragePath(artifact) written, err := st.Save(storagePath, mergedReader, artifact.FileCompressedSize) if err != nil { return fmt.Errorf("save merged file error: %v", err) diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 62605f27022..e86645cb0cf 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -89,10 +89,12 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/base64" + "encoding/hex" "encoding/xml" "errors" "fmt" "io" + "mime" "net/http" "net/url" "path" @@ -100,8 +102,9 @@ import ( "strings" "time" - "code.gitea.io/gitea/models/actions" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -113,12 +116,10 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/known/timestamppb" + "xorm.io/builder" ) -const ( - ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService" - ArtifactV4ContentEncoding = "application/zip" -) +const ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService" type artifactV4Routes struct { prefix string @@ -219,7 +220,7 @@ func parseChunkFileItemV4(st storage.ObjectStorage, artifactID int64, fpath stri return &item, nil } -func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) { +func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions_model.ActionTask, string, bool) { rawTaskID := ctx.Req.URL.Query().Get("taskID") rawArtifactID := ctx.Req.URL.Query().Get("artifactID") sig := ctx.Req.URL.Query().Get("sig") @@ -246,13 +247,13 @@ func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (* ctx.HTTPError(http.StatusUnauthorized, "Error link expired") return nil, "", false } - task, err := actions.GetTaskByID(ctx, taskID) + task, err := actions_model.GetTaskByID(ctx, taskID) if err != nil { log.Error("Error runner api getting task by ID: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task by ID") return nil, "", false } - if task.Status != actions.StatusRunning { + if task.Status != actions_model.StatusRunning { log.Error("Error runner api getting task: task is not running") ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task: task is not running") return nil, "", false @@ -265,9 +266,9 @@ func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (* return task, artifactName, true } -func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) { - var art actions.ActionArtifact - has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ? AND content_encoding = ?", runID, name, name+".zip", ArtifactV4ContentEncoding).Get(&art) +func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions_model.ActionArtifact, error) { + var art actions_model.ActionArtifact + has, err := db.GetEngine(ctx).Where(builder.Eq{"run_id": runID, "artifact_name": name}, builder.Like{"content_encoding", "%/%"}).Get(&art) if err != nil { return nil, err } else if !has { @@ -321,26 +322,59 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { if req.ExpiresAt != nil { retentionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24) } + encoding := req.GetMimeType().GetValue() + // Validate media type + if encoding != "" { + encoding, _, _ = mime.ParseMediaType(encoding) + } + fileName := artifactName + if !strings.Contains(encoding, "/") || strings.EqualFold(encoding, actions_model.ContentTypeZip) && !strings.HasSuffix(fileName, ".zip") { + encoding = actions_model.ContentTypeZip + fileName = artifactName + ".zip" + } // create or get artifact with name and path - artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", retentionDays) + artifact, err := actions_model.CreateArtifact(ctx, ctx.ActionTask, artifactName, fileName, retentionDays) if err != nil { log.Error("Error create or get artifact: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error create or get artifact") return } - artifact.ContentEncoding = ArtifactV4ContentEncoding + artifact.ContentEncodingOrType = encoding artifact.FileSize = 0 artifact.FileCompressedSize = 0 - if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + + var respData CreateArtifactResponse + + if setting.Actions.ArtifactStorage.ServeDirect() && setting.Actions.ArtifactStorage.Type == setting.AzureBlobStorageType { + storagePath := generateArtifactStoragePath(artifact) + if artifact.StoragePath != "" { + _ = storage.ActionsArtifacts.Delete(artifact.StoragePath) + } + artifact.StoragePath = storagePath + artifact.Status = actions_model.ArtifactStatusUploadPending + u, err := storage.ActionsArtifacts.ServeDirectURL(artifact.StoragePath, artifact.ArtifactPath, http.MethodPut, nil) + if err != nil { + log.Error("Error ServeDirectURL: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error ServeDirectURL") + return + } + respData = CreateArtifactResponse{ + Ok: true, + SignedUploadUrl: u.String(), + } + } else { + respData = CreateArtifactResponse{ + Ok: true, + SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID), + } + } + + if err := actions_model.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID") return } - respData := CreateArtifactResponse{ - Ok: true, - SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID), - } r.sendProtobufBody(ctx, &respData) } @@ -370,7 +404,7 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { } artifact.FileCompressedSize += uploadedLength artifact.FileSize += uploadedLength - if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + if err := actions_model.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID") return @@ -448,9 +482,27 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { return } - var chunks []*chunkFileItem + if setting.Actions.ArtifactStorage.ServeDirect() && setting.Actions.ArtifactStorage.Type == setting.AzureBlobStorageType { + r.finalizeAzureServeDirect(ctx, &req, artifact) + } else { + r.finalizeDefaultArtifact(ctx, &req, artifact, runID) + } + + // Return on finalize error + if ctx.Written() { + return + } + + respData := FinalizeArtifactResponse{ + Ok: true, + ArtifactId: artifact.ID, + } + r.sendProtobufBody(ctx, &respData) +} + +func (r *artifactV4Routes) finalizeDefaultArtifact(ctx *ArtifactContext, req *FinalizeArtifactRequest, artifact *actions_model.ActionArtifact, runID int64) { blockList, blockListErr := r.readBlockList(runID, artifact.ID) - chunks, err = listOrderedChunksForArtifact(r.fs, runID, artifact.ID, blockList) + chunks, err := listOrderedChunksForArtifact(r.fs, runID, artifact.ID, blockList) if err != nil { log.Error("Error list chunks: %v", errors.Join(blockListErr, err)) ctx.HTTPError(http.StatusInternalServerError, "Error list chunks") @@ -465,21 +517,63 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { return } - checksum := "" - if req.Hash != nil { - checksum = req.Hash.Value - } - if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil { + if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, req.GetHash().GetValue()); err != nil { log.Error("Error merge chunks: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks") return } +} - respData := FinalizeArtifactResponse{ - Ok: true, - ArtifactId: artifact.ID, +func (r *artifactV4Routes) finalizeAzureServeDirect(ctx *ArtifactContext, req *FinalizeArtifactRequest, artifact *actions_model.ActionArtifact) { + checksumValue, hasSha256Checksum := strings.CutPrefix(req.GetHash().GetValue(), "sha256:") + var actualLength int64 + if hasSha256Checksum { + hashSha256 := sha256.New() + obj, err := storage.ActionsArtifacts.Open(artifact.StoragePath) + if err != nil { + log.Error("Error read block: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error read block") + return + } + defer obj.Close() + actualLength, err = io.Copy(hashSha256, obj) + if err != nil { + log.Error("Error read block: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error read block") + return + } + rawChecksum := hashSha256.Sum(nil) + actualChecksum := hex.EncodeToString(rawChecksum) + if checksumValue != actualChecksum { + log.Error("Error merge chunks: checksum mismatch") + ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks: checksum mismatch") + return + } + } else { + fi, err := storage.ActionsArtifacts.Stat(artifact.StoragePath) + if err != nil { + log.Error("Error stat block: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error stat block") + return + } + actualLength = fi.Size() + } + + if req.Size != actualLength { + log.Error("Error merge chunks: length mismatch") + ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks: length mismatch") + return + } + + // Update artifact metadata and status now that the upload is confirmed. + artifact.FileSize = actualLength + artifact.FileCompressedSize = actualLength + artifact.Status = actions_model.ArtifactStatusUploadConfirmed + if err := actions_model.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + log.Error("Error UpdateArtifactByID: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID") + return } - r.sendProtobufBody(ctx, &respData) } func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { @@ -493,9 +587,10 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { return } - artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ - RunID: runID, - Status: int(actions.ArtifactStatusUploadConfirmed), + artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{ + RunID: runID, + Status: int(actions_model.ArtifactStatusUploadConfirmed), + FinalizedArtifactsV4: true, }) if err != nil { log.Error("Error getting artifacts: %v", err) @@ -507,7 +602,7 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { table := map[string]*ListArtifactsResponse_MonolithArtifact{} for _, artifact := range artifacts { - if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != ArtifactV4ContentEncoding { + if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value { table[artifact.ArtifactName] = nil continue } @@ -553,7 +648,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } - if artifact.Status != actions.ArtifactStatusUploadConfirmed { + if artifact.Status != actions_model.ArtifactStatusUploadConfirmed { log.Error("Error artifact not found: %s", artifact.Status.ToString()) ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return @@ -563,9 +658,9 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { if setting.Actions.ArtifactStorage.ServeDirect() { // DO NOT USE the http POST method coming from the getSignedArtifactURL endpoint - u, err := storage.ActionsArtifacts.ServeDirectURL(artifact.StoragePath, artifact.ArtifactPath, http.MethodGet, nil) - if u != nil && err == nil { - respData.SignedUrl = u.String() + u, err := actions.GetArtifactV4ServeDirectURL(artifact, http.MethodGet) + if err == nil { + respData.SignedUrl = u } } if respData.SignedUrl == "" { @@ -587,15 +682,17 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } - if artifact.Status != actions.ArtifactStatusUploadConfirmed { + if artifact.Status != actions_model.ArtifactStatusUploadConfirmed { log.Error("Error artifact not found: %s", artifact.Status.ToString()) ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } - file, _ := r.fs.Open(artifact.StoragePath) - - _, _ = io.Copy(ctx.Resp, file) + err = actions.DownloadArtifactV4ReadStorage(ctx.Base, artifact) + if err != nil { + log.Error("Error serve artifact: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "failed to download artifact") + } } func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { @@ -617,7 +714,7 @@ func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { return } - err = actions.SetArtifactNeedDelete(ctx, runID, req.Name) + err = actions_model.SetArtifactNeedDelete(ctx, runID, req.Name) if err != nil { log.Error("Error deleting artifacts: %v", err) ctx.HTTPError(http.StatusInternalServerError, err.Error()) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index d704092051b..0c48f732abf 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -1784,7 +1784,7 @@ func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) str func buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string { // endPoint is a path like "api/v1/repos/owner/repo/actions/artifacts/1/zip/raw" expires := time.Now().Add(60 * time.Minute).Unix() - uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.URLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10) + uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.RawURLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10) return uploadURL } @@ -1829,18 +1829,16 @@ func DownloadArtifact(ctx *context.APIContext) { ctx.APIError(http.StatusNotFound, "Artifact has expired") return } - ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName)) if actions.IsArtifactV4(art) { - ok, err := actions.DownloadArtifactV4ServeDirectOnly(ctx.Base, art) - if ok { - return - } - if err != nil { - ctx.APIErrorInternal(err) + // @actions/toolkit asserts that downloaded artifacts of a different runid return 302 + // https://github.com/actions/toolkit/blob/44d43b5490b02998bd09b0c4ff369a4cc67876c2/packages/artifact/src/internal/download/download-artifact.ts#L203-L210 + if actions.DownloadArtifactV4ServeDirect(ctx.Base, art) { return } + // @actions/toolkit asserts a 302 for the artifact download, so we have to build a signed URL and redirect to it + // TODO: a perma link to the code for reference redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository, art.ID), art.ID) ctx.Redirect(redirectURL, http.StatusFound) return @@ -1868,7 +1866,7 @@ func DownloadArtifactRaw(ctx *context.APIContext) { sigStr := ctx.Req.URL.Query().Get("sig") expiresStr := ctx.Req.URL.Query().Get("expires") - sigBytes, _ := base64.URLEncoding.DecodeString(sigStr) + sigBytes, _ := base64.RawURLEncoding.DecodeString(sigStr) expires, _ := strconv.ParseInt(expiresStr, 10, 64) expectedSig := buildSignature(buildDownloadRawEndpoint(repo, art.ID), expires, art.ID) @@ -1887,8 +1885,6 @@ func DownloadArtifactRaw(ctx *context.APIContext) { ctx.APIError(http.StatusNotFound, "Artifact has expired") return } - ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName)) - if actions.IsArtifactV4(art) { err := actions.DownloadArtifactV4(ctx.Base, art) if err != nil { diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index d0596d778b7..9949928622c 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -17,9 +17,9 @@ import ( git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/lfs" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" api "code.gitea.io/gitea/modules/structs" @@ -151,35 +151,18 @@ func GetRawFileOrLFS(ctx *context.APIContext) { // OK, now the blob is known to have at most 1024 (lfs pointer max size) bytes, // we can simply read this in one go (This saves reading it twice) - dataRc, err := blob.DataAsync() + lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize) if err != nil { ctx.APIErrorInternal(err) return } - buf, err := io.ReadAll(dataRc) - if err != nil { - _ = dataRc.Close() - ctx.APIErrorInternal(err) - return - } - - if err := dataRc.Close(); err != nil { - log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Repo.Repository, err) - } - // Check if the blob represents a pointer - pointer, _ := lfs.ReadPointer(bytes.NewReader(buf)) + pointer, _ := lfs.ReadPointerFromBuffer(lfsPointerBuf) // if it's not a pointer, just serve the data directly if !pointer.IsValid() { - // First handle caching for the blob - if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { - return - } - - // If not cached - serve! - common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)) + _, _ = ctx.Resp.Write(lfsPointerBuf) return } @@ -188,12 +171,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { // If there isn't one, just serve the data directly if errors.Is(err, git_model.ErrLFSObjectNotExist) { - // Handle caching for the blob SHA (not the LFS object OID) - if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { - return - } - - common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)) + _, _ = ctx.Resp.Write(lfsPointerBuf) return } else if err != nil { ctx.APIErrorInternal(err) @@ -214,14 +192,13 @@ func GetRawFileOrLFS(ctx *context.APIContext) { } } - lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer) + lfsDataFile, err := lfs.ReadMetaObject(meta.Pointer) if err != nil { ctx.APIErrorInternal(err) return } - defer lfsDataRc.Close() - - common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc) + defer lfsDataFile.Close() + httplib.ServeUserContentByFile(ctx.Base.Req, ctx.Base.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath}) } func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) { diff --git a/routers/common/actions.go b/routers/common/actions.go index 39d2111f5a1..f698ba94363 100644 --- a/routers/common/actions.go +++ b/routers/common/actions.go @@ -10,6 +10,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -60,9 +61,8 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository ctx.ServeContent(reader, &context.ServeHeaderOptions{ Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, curJob.Name, task.ID), ContentLength: &task.LogSize, - ContentType: "text/plain", - ContentTypeCharset: "utf-8", - Disposition: "attachment", + ContentType: "text/plain; charset=utf-8", + ContentDisposition: httplib.ContentDispositionAttachment, }) return nil } diff --git a/routers/common/serve.go b/routers/common/serve.go index 4bb1a48b0da..9232d90c94f 100644 --- a/routers/common/serve.go +++ b/routers/common/serve.go @@ -4,7 +4,6 @@ package common import ( - "io" "path" "time" @@ -12,7 +11,6 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/context" @@ -24,29 +22,24 @@ func ServeBlob(ctx *context.Base, repo *repo_model.Repository, filePath string, return nil } + if err := repo.LoadOwner(ctx); err != nil { + return err + } + dataRc, err := blob.DataAsync() if err != nil { return err } - defer func() { - if err = dataRc.Close(); err != nil { - log.Error("ServeBlob: Close: %v", err) - } - }() + defer dataRc.Close() - _ = repo.LoadOwner(ctx) - httplib.ServeContentByReader(ctx.Req, ctx.Resp, blob.Size(), dataRc, &httplib.ServeHeaderOptions{ + if lastModified == nil { + lastModified = new(time.Time) + } + httplib.ServeUserContentByReader(ctx.Req, ctx.Resp, blob.Size(), dataRc, httplib.ServeHeaderOptions{ Filename: path.Base(filePath), - CacheIsPublic: !repo.IsPrivate && repo.Owner != nil && repo.Owner.Visibility == structs.VisibleTypePublic, + CacheIsPublic: !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic, CacheDuration: setting.StaticCacheTime, + LastModified: *lastModified, }) return nil } - -func ServeContentByReader(ctx *context.Base, filePath string, size int64, reader io.Reader) { - httplib.ServeContentByReader(ctx.Req, ctx.Resp, size, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)}) -} - -func ServeContentByReadSeeker(ctx *context.Base, filePath string, modTime *time.Time, reader io.ReadSeeker) { - httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, modTime, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)}) -} diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go index 5395529d66a..205ab2f8ea1 100644 --- a/routers/web/admin/diagnosis.go +++ b/routers/web/admin/diagnosis.go @@ -18,10 +18,10 @@ import ( func MonitorDiagnosis(ctx *context.Context) { seconds := min(max(ctx.FormInt64("seconds"), 1), 300) - httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{ - ContentType: "application/zip", - Disposition: "attachment", - Filename: fmt.Sprintf("gitea-diagnosis-%s.zip", time.Now().Format("20060102-150405")), + httplib.ServeSetHeaders(ctx.Resp, httplib.ServeHeaderOptions{ + ContentType: "application/zip", + Filename: fmt.Sprintf("gitea-diagnosis-%s.zip", time.Now().Format("20060102-150405")), + ContentDisposition: httplib.ContentDispositionAttachment, }) zipWriter := zip.NewWriter(ctx.Resp) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 98d86f0bb34..90810a6d251 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/templates" @@ -716,8 +717,9 @@ func ArtifactsDownloadView(ctx *context_module.Context) { } } - ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName)) - + // A v4 Artifact may only contain a single file + // Multiple files are uploaded as a single file archive + // All other cases fall back to the legacy v1–v3 zip handling below if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) { err := actions.DownloadArtifactV4(ctx.Base, artifacts[0]) if err != nil { @@ -729,34 +731,41 @@ func ArtifactsDownloadView(ctx *context_module.Context) { // Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend // Those need to be zipped for download - writer := zip.NewWriter(ctx.Resp) - defer writer.Close() - for _, art := range artifacts { + ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(artifactName+".zip")) + zipWriter := zip.NewWriter(ctx.Resp) + defer zipWriter.Close() + + writeArtifactToZip := func(art *actions_model.ActionArtifact) error { f, err := storage.ActionsArtifacts.Open(art.StoragePath) if err != nil { - ctx.ServerError("ActionsArtifacts.Open", err) - return + return fmt.Errorf("ActionsArtifacts.Open: %w", err) } + defer f.Close() - var r io.ReadCloser - if art.ContentEncoding == "gzip" { + var r io.ReadCloser = f + if art.ContentEncodingOrType == actions_model.ContentEncodingV3Gzip { r, err = gzip.NewReader(f) if err != nil { - ctx.ServerError("gzip.NewReader", err) - return + return fmt.Errorf("gzip.NewReader: %w", err) } - } else { - r = f } defer r.Close() - w, err := writer.Create(art.ArtifactPath) + w, err := zipWriter.Create(art.ArtifactPath) if err != nil { - ctx.ServerError("writer.Create", err) - return + return fmt.Errorf("zipWriter.Create: %w", err) } - if _, err := io.Copy(w, r); err != nil { - ctx.ServerError("io.Copy", err) + _, err = io.Copy(w, r) + if err != nil { + return fmt.Errorf("io.Copy: %w", err) + } + return nil + } + + for _, art := range artifacts { + err := writeArtifactToZip(art) + if err != nil { + ctx.ServerError("writeArtifactToZip", err) return } } diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index 19d533f3624..9b2c64049bc 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -11,10 +11,10 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" - "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/attachment" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" @@ -199,7 +199,7 @@ func ServeAttachment(ctx *context.Context, uuid string) { } defer fr.Close() - common.ServeContentByReadSeeker(ctx.Base, attach.Name, new(attach.CreatedUnix.AsTime()), fr) + httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, fr, httplib.ServeHeaderOptions{Filename: attach.Name}) } // GetAttachment serve attachments diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go index 073d3d74208..25166ea1d3d 100644 --- a/routers/web/repo/download.go +++ b/routers/web/repo/download.go @@ -10,8 +10,8 @@ import ( git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/lfs" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/routers/common" @@ -24,28 +24,15 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim return nil } - dataRc, err := blob.DataAsync() + lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize) if err != nil { return err } - closed := false - defer func() { - if closed { - return - } - if err = dataRc.Close(); err != nil { - log.Error("ServeBlobOrLFS: Close: %v", err) - } - }() - pointer, _ := lfs.ReadPointer(dataRc) + pointer, _ := lfs.ReadPointerFromBuffer(lfsPointerBuf) if pointer.IsValid() { meta, _ := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid) if meta == nil { - if err = dataRc.Close(); err != nil { - log.Error("ServeBlobOrLFS: Close: %v", err) - } - closed = true return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified) } if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`, meta.UpdatedUnix.AsTimePtr()) { @@ -61,22 +48,14 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim } } - lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer) + lfsDataFile, err := lfs.ReadMetaObject(meta.Pointer) if err != nil { return err } - defer func() { - if err = lfsDataRc.Close(); err != nil { - log.Error("ServeBlobOrLFS: Close: %v", err) - } - }() - common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc) + defer lfsDataFile.Close() + httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath}) return nil } - if err = dataRc.Close(); err != nil { - log.Error("ServeBlobOrLFS: Close: %v", err) - } - closed = true return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified) } diff --git a/services/context/base.go b/services/context/base.go index 4baea95ccf9..06ccefa3aa5 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -173,12 +173,12 @@ func (b *Base) Redirect(location string, status ...int) { type ServeHeaderOptions httplib.ServeHeaderOptions func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) { - httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opt)) + httplib.ServeSetHeaders(b.Resp, *(*httplib.ServeHeaderOptions)(opt)) } // ServeContent serves content to http request func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { - httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opts)) + httplib.ServeSetHeaders(b.Resp, *(*httplib.ServeHeaderOptions)(opts)) http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r) } diff --git a/services/lfs/server.go b/services/lfs/server.go index fc09eb58ca4..d0fd841041f 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -172,7 +172,7 @@ func DownloadHandler(ctx *context.Context) { if len(filename) > 0 { decodedFilename, err := base64.RawURLEncoding.DecodeString(filename) if err == nil { - ctx.Resp.Header().Set("Content-Disposition", "attachment; filename=\""+string(decodedFilename)+"\"") + ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(string(decodedFilename))) ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") } } diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go index 1d28e00655c..2431ae4b93a 100644 --- a/services/repository/archiver/archiver.go +++ b/services/repository/archiver/archiver.go @@ -328,7 +328,7 @@ func ServeRepoArchive(ctx *gitea_context.Base, archiveReq *ArchiveRequest) error if setting.Repository.StreamArchives || len(archiveReq.Paths) > 0 { // the header must be set before starting streaming even an error would occur, // because errors may happen in git command and such cases aren't in our control. - httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{Filename: downloadName}) + httplib.ServeSetHeaders(ctx.Resp, httplib.ServeHeaderOptions{Filename: downloadName}) if err := archiveReq.Stream(ctx, ctx.Resp); err != nil && !ctx.Written() { if gitcmd.StderrHasPrefix(err, "fatal: pathspec") { return util.NewInvalidArgumentErrorf("path doesn't exist or is invalid") diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go index 4127ae91f58..c0cd4cdebd2 100644 --- a/tests/integration/api_actions_artifact_v4_test.go +++ b/tests/integration/api_actions_artifact_v4_test.go @@ -11,23 +11,28 @@ import ( "encoding/xml" "fmt" "io" + "mime" "net/http" "strings" "testing" "time" + actions_model "code.gitea.io/gitea/models/actions" 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" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/actions" actions_service "code.gitea.io/gitea/services/actions" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/known/timestamppb" @@ -48,15 +53,18 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { assert.NoError(t, err) table := []struct { - name string - version int32 - blockID bool - noLength bool - append int + name string + version int32 + contentType string + blockID bool + noLength bool + append int + path string }{ { name: "artifact", version: 4, + path: "artifact.zip", }, { name: "artifact2", @@ -98,6 +106,23 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { append: 4, blockID: true, }, + { + name: "artifact9.json", + version: 7, + contentType: "application/json", + }, + { + name: "artifact10", + version: 7, + contentType: "application/zip", + path: "artifact10.zip", + }, + { + name: "artifact11.zip", + version: 7, + contentType: "application/zip", + path: "artifact11.zip", + }, } for _, entry := range table { @@ -108,6 +133,7 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { Name: entry.name, WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", + MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil), })).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var uploadResp actions.CreateArtifactResponse @@ -120,9 +146,8 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { blocks := make([]string, 0, util.Iif(entry.blockID, entry.append+1, 0)) // get upload url - idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") for i := range entry.append + 1 { - url := uploadResp.SignedUploadUrl[idx:] + url := uploadResp.SignedUploadUrl // See https://learn.microsoft.com/en-us/rest/api/storageservices/append-block // See https://learn.microsoft.com/en-us/rest/api/storageservices/put-block if entry.blockID { @@ -146,7 +171,7 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { if entry.blockID && entry.append > 0 { // https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list - blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist" + blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist" // upload artifact blockList blockList := &actions.BlockList{ Latest: blocks, @@ -174,6 +199,19 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { var finalizeResp actions.FinalizeArtifactResponse protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) assert.True(t, finalizeResp.Ok) + + artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId}) + if entry.contentType != "" { + assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType) + } else { + assert.Equal(t, "application/zip", artifact.ContentEncodingOrType) + } + if entry.path != "" { + assert.Equal(t, entry.path, artifact.ArtifactPath) + } + assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status) + assert.Equal(t, int64(entry.append+1)*1024, artifact.FileSize) + assert.Equal(t, int64(entry.append+1)*1024, artifact.FileCompressedSize) }) } } @@ -198,8 +236,7 @@ func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) { assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") // get upload url - idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") - url := uploadResp.SignedUploadUrl[idx:] + "&comp=block" + url := uploadResp.SignedUploadUrl + "&comp=block" // upload artifact chunk body := strings.Repeat("B", 1024) @@ -243,8 +280,7 @@ func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) { assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") // get upload url - idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") - url := uploadResp.SignedUploadUrl[idx:] + "&comp=block" + url := uploadResp.SignedUploadUrl + "&comp=block" // upload artifact chunk body := strings.Repeat("A", 1024) @@ -290,9 +326,8 @@ func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") // get upload urls - idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") - url := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=%2f..%2fmyfile" - blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist" + url := uploadResp.SignedUploadUrl + "&comp=block&blockid=%2f..%2fmyfile" + blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist" // upload artifact chunk body := strings.Repeat("A", 1024) @@ -339,63 +374,126 @@ func TestActionsArtifactV4UploadSingleFileWithChunksOutOfOrder(t *testing.T) { token, err := actions_service.CreateAuthorizationToken(48, 792, 193) assert.NoError(t, err) - // acquire artifact upload url - req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ - Version: 4, - Name: "artifactWithChunksOutOfOrder", - WorkflowRunBackendId: "792", - WorkflowJobRunBackendId: "193", - })).AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var uploadResp actions.CreateArtifactResponse - protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) - assert.True(t, uploadResp.Ok) - assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") - - // get upload urls - idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") - block1URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block1" - block2URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block2" - blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist" - - // upload artifact chunks - bodyb := strings.Repeat("B", 1024) - req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb)) - MakeRequest(t, req, http.StatusCreated) - - bodya := strings.Repeat("A", 1024) - req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya)) - MakeRequest(t, req, http.StatusCreated) - - // upload artifact blockList - blockList := &actions.BlockList{ - Latest: []string{ - "block1", - "block2", - }, + table := []struct { + name string + artifactName string + serveDirect bool + contentType string + }{ + {name: "Upload-Zip", artifactName: "artifact-v4-upload", contentType: ""}, + {name: "Upload-Pdf", artifactName: "report-upload.pdf", contentType: "application/pdf"}, + {name: "Upload-Html", artifactName: "report-upload.html", contentType: "application/html"}, + {name: "ServeDirect-Zip", artifactName: "artifact-v4-upload-serve-direct", contentType: "", serveDirect: true}, + {name: "ServeDirect-Pdf", artifactName: "report-upload-serve-direct.pdf", contentType: "application/pdf", serveDirect: true}, + {name: "ServeDirect-Html", artifactName: "report-upload-serve-direct.html", contentType: "application/html", serveDirect: true}, } - rawBlockList, err := xml.Marshal(blockList) - assert.NoError(t, err) - req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) - MakeRequest(t, req, http.StatusCreated) - t.Logf("Create artifact confirm") + for _, entry := range table { + t.Run(entry.name, func(t *testing.T) { + // Only AzureBlobStorageType supports ServeDirect Uploads + switch setting.Actions.ArtifactStorage.Type { + case setting.AzureBlobStorageType: + defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.serveDirect)() + default: + if entry.serveDirect { + t.Skip() + } + } + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ + Version: util.Iif[int32](entry.contentType != "", 7, 4), + Name: entry.artifactName, + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil), + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp actions.CreateArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) + assert.True(t, uploadResp.Ok) + if !entry.serveDirect { + assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") + } - sha := sha256.Sum256([]byte(bodya + bodyb)) + // get upload urls + block1URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block1")) + block2URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block2")) + blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist" - // confirm artifact upload - req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ - Name: "artifactWithChunksOutOfOrder", - Size: 2048, - Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), - WorkflowRunBackendId: "792", - WorkflowJobRunBackendId: "193", - })). - AddTokenAuth(token) - resp = MakeRequest(t, req, http.StatusOK) - var finalizeResp actions.FinalizeArtifactResponse - protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) - assert.True(t, finalizeResp.Ok) + // upload artifact chunks + bodyb := strings.Repeat("B", 1024) + req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb)) + if entry.serveDirect { + req.Request.RequestURI = "" + nresp, err := http.DefaultClient.Do(req.Request) + require.NoError(t, err) + nresp.Body.Close() + require.Equal(t, http.StatusCreated, nresp.StatusCode) + } else { + MakeRequest(t, req, http.StatusCreated) + } + + bodya := strings.Repeat("A", 1024) + req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya)) + if entry.serveDirect { + req.Request.RequestURI = "" + nresp, err := http.DefaultClient.Do(req.Request) + require.NoError(t, err) + nresp.Body.Close() + require.Equal(t, http.StatusCreated, nresp.StatusCode) + } else { + MakeRequest(t, req, http.StatusCreated) + } + + // upload artifact blockList + blockList := &actions.BlockList{ + Latest: []string{ + base64.RawURLEncoding.EncodeToString([]byte("block1")), + base64.RawURLEncoding.EncodeToString([]byte("block2")), + }, + } + rawBlockList, err := xml.Marshal(blockList) + assert.NoError(t, err) + req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) + if entry.serveDirect { + req.Request.RequestURI = "" + nresp, err := http.DefaultClient.Do(req.Request) + require.NoError(t, err) + nresp.Body.Close() + require.Equal(t, http.StatusCreated, nresp.StatusCode) + } else { + MakeRequest(t, req, http.StatusCreated) + } + + t.Logf("Create artifact confirm") + + sha := sha256.Sum256([]byte(bodya + bodyb)) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ + Name: entry.artifactName, + Size: 2048, + Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.FinalizeArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) + assert.True(t, finalizeResp.Ok) + + artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId}) + if entry.contentType != "" { + assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType) + } else { + assert.Equal(t, "application/zip", artifact.ContentEncodingOrType) + } + assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status) + assert.Equal(t, int64(2048), artifact.FileSize) + assert.Equal(t, int64(2048), artifact.FileCompressedSize) + }) + } } func TestActionsArtifactV4DownloadSingle(t *testing.T) { @@ -404,33 +502,97 @@ func TestActionsArtifactV4DownloadSingle(t *testing.T) { token, err := actions_service.CreateAuthorizationToken(48, 792, 193) assert.NoError(t, err) - // list artifacts by name - req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{ - NameFilter: wrapperspb.String("artifact-v4-download"), - WorkflowRunBackendId: "792", - WorkflowJobRunBackendId: "193", - })).AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var listResp actions.ListArtifactsResponse - protojson.Unmarshal(resp.Body.Bytes(), &listResp) - assert.Len(t, listResp.Artifacts, 1) + table := []struct { + Name string + ArtifactName string + FileName string + ServeDirect bool + ContentType string + ContentDisposition string + }{ + {Name: "Download-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip"}, + {Name: "Download-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf"}, + {Name: "Download-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html"}, + {Name: "ServeDirect-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip", ServeDirect: true}, + {Name: "ServeDirect-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf", ServeDirect: true}, + {Name: "ServeDirect-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html", ServeDirect: true}, + } - // acquire artifact download url - req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{ - Name: "artifact-v4-download", - WorkflowRunBackendId: "792", - WorkflowJobRunBackendId: "193", - })). - AddTokenAuth(token) - resp = MakeRequest(t, req, http.StatusOK) - var finalizeResp actions.GetSignedArtifactURLResponse - protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) - assert.NotEmpty(t, finalizeResp.SignedUrl) + for _, entry := range table { + t.Run(entry.Name, func(t *testing.T) { + switch setting.Actions.ArtifactStorage.Type { + case setting.AzureBlobStorageType: + defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.ServeDirect)() + case setting.MinioStorageType: + defer test.MockVariableValue(&setting.Actions.ArtifactStorage.MinioConfig.ServeDirect, entry.ServeDirect)() + default: + if entry.ServeDirect { + t.Skip() + } + } - req = NewRequest(t, "GET", finalizeResp.SignedUrl) - resp = MakeRequest(t, req, http.StatusOK) - body := strings.Repeat("D", 1024) - assert.Equal(t, body, resp.Body.String()) + // list artifacts by name + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{ + NameFilter: wrapperspb.String(entry.ArtifactName), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var listResp actions.ListArtifactsResponse + require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp)) + require.Len(t, listResp.Artifacts, 1) + + // list artifacts by id + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{ + IdFilter: wrapperspb.Int64(listResp.Artifacts[0].DatabaseId), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp)) + assert.Len(t, listResp.Artifacts, 1) + + // acquire artifact download url + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{ + Name: entry.ArtifactName, + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.GetSignedArtifactURLResponse + require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)) + assert.NotEmpty(t, finalizeResp.SignedUrl) + + body := strings.Repeat("D", 1024) + var contentDisposition string + if entry.ServeDirect { + externalReq, err := http.NewRequestWithContext(t.Context(), http.MethodGet, finalizeResp.SignedUrl, nil) + require.NoError(t, err) + externalResp, err := http.DefaultClient.Do(externalReq) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, externalResp.StatusCode) + assert.Equal(t, entry.ContentType, externalResp.Header.Get("Content-Type")) + contentDisposition = externalResp.Header.Get("Content-Disposition") + buf := make([]byte, 1024) + n, err := io.ReadAtLeast(externalResp.Body, buf, len(buf)) + externalResp.Body.Close() + require.NoError(t, err) + assert.Equal(t, len(buf), n) + assert.Equal(t, body, string(buf)) + } else { + req = NewRequest(t, "GET", finalizeResp.SignedUrl) + resp = MakeRequest(t, req, http.StatusOK) + assert.Equal(t, entry.ContentType, resp.Header().Get("Content-Type")) + contentDisposition = resp.Header().Get("Content-Disposition") + assert.Equal(t, body, resp.Body.String()) + } + disposition, param, err := mime.ParseMediaType(contentDisposition) + require.NoError(t, err) + assert.Equal(t, "inline", disposition) + assert.Equal(t, entry.FileName, param["filename"]) + }) + } } func TestActionsArtifactV4RunDownloadSinglePublicApi(t *testing.T) { @@ -561,7 +723,7 @@ func TestActionsArtifactV4ListAndGetPublicApi(t *testing.T) { for _, artifact := range listResp.Entries { assert.Contains(t, artifact.URL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), artifact.ID)) assert.Contains(t, artifact.ArchiveDownloadURL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip", repo.FullName(), artifact.ID)) - req = NewRequestWithBody(t, "GET", listResp.Entries[0].URL, nil). + req = NewRequestWithBody(t, "GET", artifact.URL, nil). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) From a3cc34472b4fe730aa8766d874ebf00c75cef2c8 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:07:59 -0700 Subject: [PATCH 09/16] Pass ServeHeaderOptions by value instead of pointer, fine tune httplib tests (#36982) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass `ServeHeaderOptions` by value instead of pointer across all call sites — no nil-check semantics are needed and the struct is small enough that copying is fine. ## Changes - **`services/context/base.go`**: `SetServeHeaders` and `ServeContent` accept `ServeHeaderOptions` (value, not pointer); internal unsafe pointer cast replaced with a clean type conversion - **`routers/api/packages/helper/helper.go`**: `ServePackageFile` variadic changed from `...*context.ServeHeaderOptions` to `...context.ServeHeaderOptions`; internal variable is now a value type - **All call sites** (13 files): `&context.ServeHeaderOptions{...}` → `context.ServeHeaderOptions{...}` Before/after at the definition level: ```go // Before func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) { ... } func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { ... } func ServePackageFile(..., forceOpts ...*context.ServeHeaderOptions) { ... } // After func (b *Base) SetServeHeaders(opts ServeHeaderOptions) { ... } func (b *Base) ServeContent(r io.ReadSeeker, opts ServeHeaderOptions) { ... } func ServePackageFile(..., forceOpts ...context.ServeHeaderOptions) { ... } ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: wxiaoguang <2114189+wxiaoguang@users.noreply.github.com> Co-authored-by: wxiaoguang --- modules/httplib/serve.go | 2 +- routers/api/actions/artifacts.go | 2 +- routers/api/packages/alpine/alpine.go | 2 +- routers/api/packages/arch/arch.go | 4 +- routers/api/packages/debian/debian.go | 4 +- routers/api/packages/helper/helper.go | 6 +- routers/api/packages/maven/maven.go | 2 +- routers/api/packages/rpm/rpm.go | 4 +- routers/api/packages/rubygems/rubygems.go | 4 +- routers/api/packages/swift/swift.go | 4 +- routers/common/actions.go | 2 +- routers/web/user/setting/packages.go | 2 +- services/context/base.go | 10 +- services/repository/archiver/archiver.go | 2 +- tests/integration/download_test.go | 124 +++++++++------------- 15 files changed, 77 insertions(+), 97 deletions(-) diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index e8299d1c805..8abf6f18877 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -87,7 +87,7 @@ func serveSetHeadersByUserContent(w http.ResponseWriter, contentPrefetchBuf []by if setting.MimeTypeMap.Enabled { fileExtension := strings.ToLower(path.Ext(opts.Filename)) opts.ContentType = setting.MimeTypeMap.Map[fileExtension] - detectCharset = !strings.Contains(opts.ContentType, "charset=") + detectCharset = strings.HasPrefix(opts.ContentType, "text/") && !strings.Contains(opts.ContentType, "charset=") } if opts.ContentType == "" { diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index a6722616cfe..13cbecb5cd0 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -496,7 +496,7 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) { ctx.Resp.Header().Set("Content-Encoding", "gzip") } log.Debug("[artifact] downloadArtifact, name: %s, path: %s, storage: %s, size: %d", artifact.ArtifactName, artifact.ArtifactPath, artifact.StoragePath, artifact.FileSize) - ctx.ServeContent(fd, &context.ServeHeaderOptions{ + ctx.ServeContent(fd, context.ServeHeaderOptions{ Filename: artifact.ArtifactName, LastModified: artifact.CreatedUnix.AsLocalTime(), }) diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index f250a1a5494..52fc287a0e4 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -54,7 +54,7 @@ func GetRepositoryKey(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{ ContentType: "application/x-pem-file", Filename: fmt.Sprintf("%s@%s.rsa.pub", ctx.Package.Owner.LowerName, hex.EncodeToString(fingerprint)), }) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index 5a124f6918d..f3b70f39b68 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -35,7 +35,7 @@ func GetRepositoryKey(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{ ContentType: "application/pgp-keys", }) } @@ -232,7 +232,7 @@ func GetPackageOrRepositoryFile(ctx *context.Context) { return } - ctx.ServeContent(bytes.NewReader(data), &context.ServeHeaderOptions{ + ctx.ServeContent(bytes.NewReader(data), context.ServeHeaderOptions{ Filename: filenameOrig, }) return diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go index 82c7952bdbb..785efb6dda3 100644 --- a/routers/api/packages/debian/debian.go +++ b/routers/api/packages/debian/debian.go @@ -35,7 +35,7 @@ func GetRepositoryKey(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{ ContentType: "application/pgp-keys", Filename: "repository.key", }) @@ -233,7 +233,7 @@ func DownloadPackageFile(ctx *context.Context) { return } - helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{ + helper.ServePackageFile(ctx, s, u, pf, context.ServeHeaderOptions{ ContentType: "application/vnd.debian.binary-package", Filename: pf.Name, LastModified: pf.CreatedUnix.AsLocalTime(), diff --git a/routers/api/packages/helper/helper.go b/routers/api/packages/helper/helper.go index 27d4e6ffdc6..01ae5d2b7ee 100644 --- a/routers/api/packages/helper/helper.go +++ b/routers/api/packages/helper/helper.go @@ -39,7 +39,7 @@ func ProcessErrorForUser(ctx *context.Context, status int, errObj any) string { // ServePackageFile the content of the package file // If the url is set it will redirect the request, otherwise the content is copied to the response. -func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...*context.ServeHeaderOptions) { +func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...context.ServeHeaderOptions) { if u != nil { ctx.Redirect(u.String()) return @@ -47,11 +47,11 @@ func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf defer s.Close() - var opts *context.ServeHeaderOptions + var opts context.ServeHeaderOptions if len(forceOpts) > 0 { opts = forceOpts[0] } else { - opts = &context.ServeHeaderOptions{ + opts = context.ServeHeaderOptions{ Filename: pf.Name, LastModified: pf.CreatedUnix.AsLocalTime(), } diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index 6c2916908b6..446398caf70 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -200,7 +200,7 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool return } - opts := &context.ServeHeaderOptions{ + opts := context.ServeHeaderOptions{ ContentLength: &pb.Size, LastModified: pf.CreatedUnix.AsLocalTime(), } diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go index 5abbb0c8ae6..4447a0c3cf0 100644 --- a/routers/api/packages/rpm/rpm.go +++ b/routers/api/packages/rpm/rpm.go @@ -57,7 +57,7 @@ func GetRepositoryKey(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{ ContentType: "application/pgp-keys", Filename: "repository.key", }) @@ -80,7 +80,7 @@ func CheckRepositoryFileExistence(ctx *context.Context) { return } - ctx.SetServeHeaders(&context.ServeHeaderOptions{ + ctx.SetServeHeaders(context.ServeHeaderOptions{ Filename: pf.Name, LastModified: pf.CreatedUnix.AsLocalTime(), }) diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index 69764c1df3f..fe2e7af6d9a 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -79,7 +79,7 @@ func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_mo }) } - ctx.SetServeHeaders(&context.ServeHeaderOptions{ + ctx.SetServeHeaders(context.ServeHeaderOptions{ Filename: filename + ".gz", }) @@ -119,7 +119,7 @@ func ServePackageSpecification(ctx *context.Context) { return } - ctx.SetServeHeaders(&context.ServeHeaderOptions{ + ctx.SetServeHeaders(context.ServeHeaderOptions{ Filename: filename, }) diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go index 66c28c97725..948ece7a27a 100644 --- a/routers/api/packages/swift/swift.go +++ b/routers/api/packages/swift/swift.go @@ -281,7 +281,7 @@ func DownloadManifest(ctx *context.Context) { filename = fmt.Sprintf("Package@swift-%s.swift", swiftVersion) } - ctx.ServeContent(strings.NewReader(m.Content), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(m.Content), context.ServeHeaderOptions{ ContentType: "text/x-swift", Filename: filename, LastModified: pv.CreatedUnix.AsLocalTime(), @@ -437,7 +437,7 @@ func DownloadPackageFile(ctx *context.Context) { Digest: pd.Files[0].Blob.HashSHA256, }) - helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{ + helper.ServePackageFile(ctx, s, u, pf, context.ServeHeaderOptions{ Filename: pf.Name, ContentType: "application/zip", LastModified: pf.CreatedUnix.AsLocalTime(), diff --git a/routers/common/actions.go b/routers/common/actions.go index f698ba94363..4eb7078db67 100644 --- a/routers/common/actions.go +++ b/routers/common/actions.go @@ -58,7 +58,7 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository if p := strings.Index(workflowName, "."); p > 0 { workflowName = workflowName[0:p] } - ctx.ServeContent(reader, &context.ServeHeaderOptions{ + ctx.ServeContent(reader, context.ServeHeaderOptions{ Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, curJob.Name, task.ID), ContentLength: &task.LogSize, ContentType: "text/plain; charset=utf-8", diff --git a/routers/web/user/setting/packages.go b/routers/web/user/setting/packages.go index 62b0240642d..66aa2413775 100644 --- a/routers/web/user/setting/packages.go +++ b/routers/web/user/setting/packages.go @@ -112,7 +112,7 @@ func RegenerateChefKeyPair(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(priv), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(priv), context.ServeHeaderOptions{ ContentType: "application/x-pem-file", Filename: ctx.Doer.Name + ".priv", }) diff --git a/services/context/base.go b/services/context/base.go index 06ccefa3aa5..8d44de5bc72 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -170,15 +170,15 @@ func (b *Base) Redirect(location string, status ...int) { http.Redirect(b.Resp, b.Req, location, code) } -type ServeHeaderOptions httplib.ServeHeaderOptions +type ServeHeaderOptions = httplib.ServeHeaderOptions -func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) { - httplib.ServeSetHeaders(b.Resp, *(*httplib.ServeHeaderOptions)(opt)) +func (b *Base) SetServeHeaders(opts ServeHeaderOptions) { + httplib.ServeSetHeaders(b.Resp, opts) } // ServeContent serves content to http request -func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { - httplib.ServeSetHeaders(b.Resp, *(*httplib.ServeHeaderOptions)(opts)) +func (b *Base) ServeContent(r io.ReadSeeker, opts ServeHeaderOptions) { + httplib.ServeSetHeaders(b.Resp, opts) http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r) } diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go index 2431ae4b93a..f7069f226be 100644 --- a/services/repository/archiver/archiver.go +++ b/services/repository/archiver/archiver.go @@ -359,7 +359,7 @@ func ServeRepoArchive(ctx *gitea_context.Base, archiveReq *ArchiveRequest) error } defer fr.Close() - ctx.ServeContent(fr, &gitea_context.ServeHeaderOptions{ + ctx.ServeContent(fr, gitea_context.ServeHeaderOptions{ Filename: downloadName, LastModified: archiver.CreatedUnix.AsLocalTime(), }) diff --git a/tests/integration/download_test.go b/tests/integration/download_test.go index efe5ac791cf..3e7be98b093 100644 --- a/tests/integration/download_test.go +++ b/tests/integration/download_test.go @@ -8,86 +8,66 @@ import ( "testing" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" ) -func TestDownloadByID(t *testing.T) { +func TestDownloadRepoContent(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") - // Request raw blob - req := NewRequest(t, "GET", "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f") - resp := session.MakeRequest(t, req, http.StatusOK) + t.Run("RawBlob", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String()) + }) - assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String()) -} - -func TestDownloadByIDForSVGUsesSecureHeaders(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - session := loginUser(t, "user2") - - // Request raw blob - req := NewRequest(t, "GET", "/user2/repo2/raw/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b") - resp := session.MakeRequest(t, req, http.StatusOK) - - assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy")) - assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type")) - assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options")) -} - -func TestDownloadByIDMedia(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - session := loginUser(t, "user2") - - // Request raw blob - req := NewRequest(t, "GET", "/user2/repo1/media/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f") - resp := session.MakeRequest(t, req, http.StatusOK) - - assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String()) -} - -func TestDownloadByIDMediaForSVGUsesSecureHeaders(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - session := loginUser(t, "user2") - - // Request raw blob - req := NewRequest(t, "GET", "/user2/repo2/media/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b") - resp := session.MakeRequest(t, req, http.StatusOK) - - assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy")) - assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type")) - assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options")) -} - -func TestDownloadRawTextFileWithoutMimeTypeMapping(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - session := loginUser(t, "user2") - - req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml") - resp := session.MakeRequest(t, req, http.StatusOK) - - assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) -} - -func TestDownloadRawTextFileWithMimeTypeMapping(t *testing.T) { - defer tests.PrepareTestEnv(t)() - setting.MimeTypeMap.Map[".xml"] = "text/xml" - setting.MimeTypeMap.Enabled = true - - session := loginUser(t, "user2") - - req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml") - resp := session.MakeRequest(t, req, http.StatusOK) - - assert.Equal(t, "text/xml; charset=utf-8", resp.Header().Get("Content-Type")) - - delete(setting.MimeTypeMap.Map, ".xml") - setting.MimeTypeMap.Enabled = false + t.Run("SVGUsesSecureHeaders", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo2/raw/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy")) + assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type")) + assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options")) + }) + + t.Run("MediaBlob", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo1/media/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String()) + }) + + t.Run("MediaSVGUsesSecureHeaders", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo2/media/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy")) + assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type")) + assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options")) + }) + + t.Run("MimeTypeMap", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml") + resp := session.MakeRequest(t, req, http.StatusOK) + // although the file is a valid XML file, it is served as "text/plain" to avoid site content spamming (the same to "text/html" files) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.MimeTypeMap)() + setting.MimeTypeMap.Enabled = true + + setting.MimeTypeMap.Map[".xml"] = "text/xml" + req = NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml") + resp = session.MakeRequest(t, req, http.StatusOK) + // respect the mime mapping, and "text/plain" protection isn't used anymore + assert.Equal(t, "text/xml; charset=utf-8", resp.Header().Get("Content-Type")) + assert.Equal(t, "inline; filename=test.xml", resp.Header().Get("Content-Disposition")) + + setting.MimeTypeMap.Map[".xml"] = "application/xml" + req = NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml") + resp = session.MakeRequest(t, req, http.StatusOK) + // non-text file don't have "charset" + assert.Equal(t, "application/xml", resp.Header().Get("Content-Type")) + }) } From ffa626b585225d62718f39e1b5fcc00416b0b7e4 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Thu, 26 Mar 2026 00:53:31 +0000 Subject: [PATCH 10/16] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 663de757726..894502ce10f 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -969,7 +969,6 @@ "repo.visibility_description": "Ní bheidh ach an t-úinéir nó baill na heagraíochta má tá cearta acu in ann é a fheiceáil.", "repo.visibility_helper": "Déan stóras príobháideach", "repo.visibility_helper_forced": "Cuireann riarthóir do shuíomh iallach ar stórais nua a bheith príobháideach.", - "repo.visibility_fork_helper": "(Beidh tionchar ag athrú seo ar gach forc.)", "repo.clone_helper": "Teastaíonn cabhair ó chlónáil? Tabhair cuairt ar Cabhair.", "repo.fork_repo": "Stóras Forc", "repo.fork_from": "Forc ó", @@ -2174,7 +2173,8 @@ "repo.settings.transfer_abort_invalid": "Ní féidir leat aistriú stóras nach bhfuil ann a chealú.", "repo.settings.transfer_abort_success": "Cuireadh an t-aistriú stóras chuig %s ar ceal go rathúil.", "repo.settings.transfer_desc": "Aistrigh an stóras seo chuig úsáideoir nó chuig eagraíocht a bhfuil cearta riarthóra agat ina leith.", - "repo.settings.transfer_form_title": "Cuir isteach ainm an stóras mar dhearbhú:", + "repo.settings.enter_repo_name_to_confirm": "Cuir isteach ainm an stórais mar dheimhniú:", + "repo.settings.enter_repo_full_name_to_confirm": "Cuir isteach ainm iomlán an stórais (úinéir/ainm) mar dheimhniú:", "repo.settings.transfer_in_progress": "Tá aistriú ar siúl faoi láthair. Cealaigh é más mian leat an stóras seo a aistriú chuig úsáideoir eile.", "repo.settings.transfer_notices_1": "- Caillfidh tú rochtain ar an stóras má aistríonn tú é chuig úsáideoir aonair.", "repo.settings.transfer_notices_2": "- Coimeádfaidh tú rochtain ar an stóras má aistríonn tú é chuig eagraíocht a bhfuil (comh)úinéir agat.", @@ -2474,10 +2474,13 @@ "repo.settings.matrix.room_id": "ID seomra", "repo.settings.matrix.message_type": "Cineál teachtaireachta", "repo.settings.visibility.private.button": "Déan Príobháideach", - "repo.settings.visibility.private.text": "Má athraítear an infheictheacht go príobháideach, ní bheidh an stór le feiceáil ach ag baill cheadaithe agus d’fhéadfadh sé go mbainfí an gaol idir é agus forcanna, faireoirí agus réaltaí atá ann cheana féin.", + "repo.settings.visibility.private.text": "Má athraítear an infheictheacht go príobháideach, ní bheidh an stór le feiceáil ach ag baill cheadaithe agus d’fhéadfadh sé go mbainfí an gaol idir é agus forcanna, breathnóirí agus réaltaí atá ann cheana féin.", "repo.settings.visibility.private.bullet_title": "An infheictheacht a athrú go toil phríobháide", "repo.settings.visibility.private.bullet_one": "Déan an stóras le feiceáil ag baill cheadaithe amháin.", - "repo.settings.visibility.private.bullet_two": "D’fhéadfadh sé an gaol idir é agus forcanna, faireoirí, agus réaltaí a bhaint.", + "repo.settings.visibility.private.bullet_two": "Cuir an infheictheacht i bhfeidhm ar a fhorcanna, agus bain na breathnóirí agus na réaltaí.", + "repo.settings.visibility.private.stats_stars": "Tá %d réalta(í) sa stórlann seo a d'fhéadfadh a bheith caillte.", + "repo.settings.visibility.private.stats_watchers": "Tá %d breathnóir(í) sa stórlann seo a d'fhéadfadh a bheith caillte.", + "repo.settings.visibility.private.stats_forks": "Tá %d forc(anna) bainteach leis an stórlann seo.", "repo.settings.visibility.public.button": "Déan Poiblí", "repo.settings.visibility.public.text": "Má athraíonn an infheictheacht don phobal, beidh an stóras le feiceáil do dhuine ar bith.", "repo.settings.visibility.public.bullet_title": "Athróidh an infheictheacht go poiblí:", From 9583e1a65c5f11c4aa66e2e8656cde9e70d9b5a8 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 26 Mar 2026 10:48:09 +0100 Subject: [PATCH 11/16] Linkify URLs in Actions workflow logs (#36986) Detect URLs in Actions log output and render them as clickable links, similar to how GitHub Actions handles this. Pre-existing links from ansi_up's OSC 8 parsing are also kept intact. --------- Signed-off-by: silverwind Co-authored-by: Claude (claude-opus-4-6) Co-authored-by: wxiaoguang --- web_src/css/themes/theme-gitea-dark.css | 1 + web_src/css/themes/theme-gitea-light.css | 1 + web_src/js/components/ActionRunJobView.vue | 5 ++++ web_src/js/render/ansi.test.ts | 5 ++++ web_src/js/render/ansi.ts | 29 ++++++++++++---------- web_src/js/utils/url.test.ts | 29 +++++++++++++++++++++- web_src/js/utils/url.ts | 28 +++++++++++++++++++++ 7 files changed, 84 insertions(+), 14 deletions(-) diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index f3475895090..c62c20f93a8 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -74,6 +74,7 @@ gitea-theme-meta-info { --color-console-active-bg: #2e353b; --color-console-menu-bg: #262b31; --color-console-menu-border: #414b55; + --color-console-link: #8f9ba8; /* named colors */ --color-red: #cc4848; --color-orange: #cc580c; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index dc916f002d3..5f437c5a6c5 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -74,6 +74,7 @@ gitea-theme-meta-info { --color-console-active-bg: #d0d7de; --color-console-menu-bg: #f8f9fb; --color-console-menu-border: #d0d7de; + --color-console-link: #5c656d; /* named colors */ --color-red: #db2828; --color-orange: #f2711c; diff --git a/web_src/js/components/ActionRunJobView.vue b/web_src/js/components/ActionRunJobView.vue index 9d8ee0dbde5..747889d04cc 100644 --- a/web_src/js/components/ActionRunJobView.vue +++ b/web_src/js/components/ActionRunJobView.vue @@ -641,6 +641,11 @@ async function hashChangeListener() { overflow-wrap: anywhere; } +.job-step-logs .log-msg a { + color: var(--color-console-link) !important; + text-decoration: underline; +} + .job-step-logs .job-log-line .log-cmd-command { color: var(--color-ansi-blue); } diff --git a/web_src/js/render/ansi.test.ts b/web_src/js/render/ansi.test.ts index 21b7523994a..2d9b8ede008 100644 --- a/web_src/js/render/ansi.test.ts +++ b/web_src/js/render/ansi.test.ts @@ -17,4 +17,9 @@ test('renderAnsi', () => { // treat "\033[0K" and "\033[0J" (Erase display/line) as "\r", then it will be covered to "\n" finally. expect(renderAnsi('a\x1b[Kb\x1b[2Jc')).toEqual('a\nb\nc'); expect(renderAnsi('\x1b[48;5;88ma\x1b[38;208;48;5;159mb\x1b[m')).toEqual(`ab`); + + // URLs in ANSI output become clickable links + const link = (url: string) => `${url}`; + expect(renderAnsi('Downloading https://github.com/actions/upload-artifact/releases')).toEqual(`Downloading ${link('https://github.com/actions/upload-artifact/releases')}`); + expect(renderAnsi('\x1b[32mhttps://proxy.golang.org/cached-only\x1b[0m')).toEqual(`${link('https://proxy.golang.org/cached-only')}`); }); diff --git a/web_src/js/render/ansi.ts b/web_src/js/render/ansi.ts index f5429ef6add..4625e542333 100644 --- a/web_src/js/render/ansi.ts +++ b/web_src/js/render/ansi.ts @@ -1,4 +1,5 @@ import {AnsiUp} from 'ansi_up'; +import {linkifyURLs} from '../utils/url.ts'; const replacements: Array<[RegExp, string]> = [ [/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op @@ -25,21 +26,23 @@ export function renderAnsi(line: string): string { } } + let result: string; if (!line.includes('\r')) { - return ansi_up.ansi_to_html(line); - } - - // handle "\rReading...1%\rReading...5%\rReading...100%", - // convert it into a multiple-line string: "Reading...1%\nReading...5%\nReading...100%" - const lines: Array = []; - for (const part of line.split('\r')) { - if (part === '') continue; - const partHtml = ansi_up.ansi_to_html(part); - if (partHtml !== '') { - lines.push(partHtml); + result = ansi_up.ansi_to_html(line); + } else { + // handle "\rReading...1%\rReading...5%\rReading...100%", + // convert it into a multiple-line string: "Reading...1%\nReading...5%\nReading...100%" + const lines: Array = []; + for (const part of line.split('\r')) { + if (part === '') continue; + const partHtml = ansi_up.ansi_to_html(part); + if (partHtml !== '') { + lines.push(partHtml); + } } + // the log message element is with "white-space: break-spaces;", so use "\n" to break lines + result = lines.join('\n'); } - // the log message element is with "white-space: break-spaces;", so use "\n" to break lines - return lines.join('\n'); + return linkifyURLs(result); } diff --git a/web_src/js/utils/url.test.ts b/web_src/js/utils/url.test.ts index c39dd157322..3a4323e88f7 100644 --- a/web_src/js/utils/url.test.ts +++ b/web_src/js/utils/url.test.ts @@ -1,10 +1,37 @@ -import {pathEscapeSegments, toOriginUrl} from './url.ts'; +import {linkifyURLs, pathEscapeSegments, toOriginUrl} from './url.ts'; test('pathEscapeSegments', () => { expect(pathEscapeSegments('a/b/c')).toEqual('a/b/c'); expect(pathEscapeSegments('a/b/ c')).toEqual('a/b/%20c'); }); +test('linkifyURLs', () => { + const link = (url: string) => `${url}`; + expect(linkifyURLs('https://example.com')).toEqual(link('https://example.com')); + expect(linkifyURLs('https://dl.google.com/go/go1.23.6.linux-amd64.tar.gz')).toEqual(link('https://dl.google.com/go/go1.23.6.linux-amd64.tar.gz')); + expect(linkifyURLs('https://example.com/path?query=1&b=2#frag')).toEqual(link('https://example.com/path?query=1&b=2#frag')); + expect(linkifyURLs('visit https://example.com/repo for info')).toEqual(`visit ${link('https://example.com/repo')} for info`); + expect(linkifyURLs('See https://example.com.')).toEqual(`See ${link('https://example.com')}.`); + expect(linkifyURLs('https://example.com, and more')).toEqual(`${link('https://example.com')}, and more`); + expect(linkifyURLs('https://proxy.golang.org/cached-only')).toEqual(`${link('https://proxy.golang.org/cached-only')}`); + expect(linkifyURLs('https://registry.npmjs.org/@types/node')).toEqual(`${link('https://registry.npmjs.org/@types/node')}`); + expect(linkifyURLs('https://a.com and https://b.org')).toEqual(`${link('https://a.com')} and ${link('https://b.org')}`); + expect(linkifyURLs('no urls here')).toEqual('no urls here'); + expect(linkifyURLs('http://example.com/path')).toEqual(link('http://example.com/path')); + expect(linkifyURLs('http://localhost:3000/repo')).toEqual(link('http://localhost:3000/repo')); + expect(linkifyURLs('https://')).toEqual('https://'); + expect(linkifyURLs('Click here')).toEqual('Click here'); + expect(linkifyURLs('Click here')).toEqual('Click here'); + expect(linkifyURLs('https://example.com')).toEqual('https://example.com'); + expect(linkifyURLs('https://evil.com/')).toEqual(`${link('https://evil.com/')}`); + expect(linkifyURLs('https://evil.com/"onmouseover="alert(1)')).toEqual(`${link('https://evil.com/')}"onmouseover="alert(1)`); + expect(linkifyURLs('javascript:alert(1)')).toEqual('javascript:alert(1)'); // eslint-disable-line no-script-url + expect(linkifyURLs("https://evil.com/'onclick='alert(1)")).toEqual(`${link('https://evil.com/')}'onclick='alert(1)`); + expect(linkifyURLs('data:text/html,')).toEqual('data:text/html,'); + expect(linkifyURLs('https://evil.com/\nonclick=alert(1)')).toEqual(`${link('https://evil.com/')}\nonclick=alert(1)`); + expect(linkifyURLs('https://evil.com/"onmouseover=alert(1)')).toEqual(`${link('https://evil.com/"onmouseover=alert')}(1)`); +}); + test('toOriginUrl', () => { const oldLocation = String(window.location); for (const origin of ['https://example.com', 'https://example.com:3000']) { diff --git a/web_src/js/utils/url.ts b/web_src/js/utils/url.ts index 6bcb4c16093..469693373aa 100644 --- a/web_src/js/utils/url.ts +++ b/web_src/js/utils/url.ts @@ -2,6 +2,34 @@ export function pathEscapeSegments(s: string): string { return s.split('/').map(encodeURIComponent).join('/'); } +// Match HTML tags (to skip) or URLs (to linkify) in HTML content +const urlLinkifyPattern = /(<([-\w]+)[^>]*>)|(<\/([-\w]+)[^>]*>)|(https?:\/\/[^\s<>"'`|(){}[\]]+)/gi; +const trailingPunctPattern = /[.,;:!?]+$/; + +// Convert URLs to clickable links in HTML, preserving existing HTML tags +export function linkifyURLs(html: string): string { + let inAnchor = false; + return html.replace(urlLinkifyPattern, (match, _openTagFull, openTag, _closeTagFull, closeTag, url) => { + // skip URLs inside existing tags + if (openTag === 'a') { + inAnchor = true; + return match; + } else if (closeTag === 'a') { + inAnchor = false; + return match; + } + if (inAnchor || !url) { + return match; + } + + const trailingPunct = url.match(trailingPunctPattern); + const cleanUrl = trailingPunct ? url.slice(0, -trailingPunct[0].length) : url; + const trailing = trailingPunct ? trailingPunct[0] : ''; + // safe because regexp only matches valid URLs (no quotes or angle brackets) + return `${cleanUrl}${trailing}`; // eslint-disable-line github/unescaped-html-literal + }); +} + /** Convert an absolute or relative URL to an absolute URL with the current origin. It only * processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'. */ export function toOriginUrl(urlStr: string) { From d5a89805d90d31465ac10fdf3d1a9119b669e8be Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 26 Mar 2026 11:18:50 +0100 Subject: [PATCH 12/16] Improve severity labels in Actions logs and tweak colors (#36993) Add support for error, warning, notice, and debug log commands with bold label prefixes and colored backgrounds matching GitHub's style. Parse both `##[cmd]` and `::cmd args::` formats. Also improved the severity colors globally and added a devtest page for these. --------- Co-authored-by: Claude (claude-opus-4-6) --- templates/devtest/severity-colors.tmpl | 80 +++++++++++++++++++++ web_src/css/base.css | 6 -- web_src/css/modules/message.css | 23 +----- web_src/css/themes/theme-gitea-dark.css | 20 +++--- web_src/css/themes/theme-gitea-light.css | 28 ++++---- web_src/js/components/ActionRunJobView.vue | 27 ++++++- web_src/js/components/ActionRunView.test.ts | 10 ++- web_src/js/components/ActionRunView.ts | 32 ++++++++- 8 files changed, 168 insertions(+), 58 deletions(-) create mode 100644 templates/devtest/severity-colors.tmpl diff --git a/templates/devtest/severity-colors.tmpl b/templates/devtest/severity-colors.tmpl new file mode 100644 index 00000000000..9f86b864ea9 --- /dev/null +++ b/templates/devtest/severity-colors.tmpl @@ -0,0 +1,80 @@ +{{template "devtest/devtest-header"}} +
+

Severity Colors

+ +

Messages

+
+
Error Message
+

This is an error message using --color-error-* variables.

+
+
+
Warning Message
+

This is a warning message using --color-warning-* variables.

+
+
+
Success Message
+

This is a success message using --color-success-* variables.

+
+
+
Info Message
+

This is an info message using --color-info-* variables.

+
+ +

Form Fields

+
+
+ + +
+
+ +

Labels

+
+
Red
+
Orange
+
Yellow
+
Green
+
Blue
+
Violet
+
Purple
+
+ +

Color Swatches

+

Error

+
+
+
Text
+ error-bg +
+
+
Hover
+ error-bg-hover +
+
+
Active
+ error-bg-active +
+
+

Warning

+
+
+
Text
+ warning-bg +
+
+

Success

+
+
+
Text
+ success-bg +
+
+

Info

+
+
+
Text
+ info-bg +
+
+
+{{template "devtest/devtest-footer"}} diff --git a/web_src/css/base.css b/web_src/css/base.css index 2c7bd7395a4..b4139c0e728 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -393,12 +393,6 @@ img.ui.avatar, aspect-ratio: 1; } -.ui.error.message .header, -.ui.warning.message .header { - color: inherit; - filter: saturate(2); -} - .full.height { flex-grow: 1; padding-bottom: var(--page-space-bottom); diff --git a/web_src/css/modules/message.css b/web_src/css/modules/message.css index 7e8a2cf7446..ce997c4350b 100644 --- a/web_src/css/modules/message.css +++ b/web_src/css/modules/message.css @@ -41,9 +41,9 @@ margin-bottom: 1em; } -.ui.info.message .header, -.ui.blue.message .header { - color: var(--color-blue); +.ui.message .header { + color: inherit; + filter: saturate(2); } .ui.info.message, @@ -55,12 +55,6 @@ border-color: var(--color-info-border); } -.ui.success.message .header, -.ui.positive.message .header, -.ui.green.message .header { - color: var(--color-green); -} - .ui.success.message, .ui.attached.success.message, .ui.positive.message, @@ -70,12 +64,6 @@ border-color: var(--color-success-border); } -.ui.error.message .header, -.ui.negative.message .header, -.ui.red.message .header { - color: var(--color-red); -} - .ui.error.message, .ui.attached.error.message, .ui.red.message, @@ -87,11 +75,6 @@ border-color: var(--color-error-border); } -.ui.warning.message .header, -.ui.yellow.message .header { - color: var(--color-yellow); -} - .ui.warning.message, .ui.attached.warning.message, .ui.yellow.message, diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index c62c20f93a8..fbdef1e2fb8 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -162,20 +162,20 @@ gitea-theme-meta-info { --color-diff-removed-row-border: #634343; --color-diff-removed-word-bg: #6f3333; --color-diff-inactive: #22282d; - --color-error-border: #a04141; - --color-error-bg: #522; - --color-error-bg-active: #744; - --color-error-bg-hover: #633; - --color-error-text: #f9cbcb; + --color-error-border: #da3633; + --color-error-bg: #3c2425; + --color-error-bg-active: #5a3637; + --color-error-bg-hover: #4c2d2e; + --color-error-text: #f5817c; --color-success-border: #458a57; --color-success-bg: #284034; - --color-success-text: #6cc664; - --color-warning-border: #bb9d00; - --color-warning-bg: #3a3a30; - --color-warning-text: #fbbd08; + --color-success-text: #69be61; + --color-warning-border: #9e6a03; + --color-warning-bg: #2f2a1b; + --color-warning-text: #d29922; --color-info-border: #306090; --color-info-bg: #26354c; - --color-info-text: #38a8e8; + --color-info-text: #48b7f8; --color-red-badge: #db2828; --color-red-badge-bg: #db28281a; --color-red-badge-hover-bg: #db28284d; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 5f437c5a6c5..761cb18da05 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -162,20 +162,20 @@ gitea-theme-meta-info { --color-diff-removed-row-border: #f1c0c0; --color-diff-removed-word-bg: #fdb8c0; --color-diff-inactive: #f0f2f4; - --color-error-border: #e0b4b4; - --color-error-bg: #fff6f6; - --color-error-bg-active: #fbb; - --color-error-bg-hover: #fdd; - --color-error-text: #9f3a38; - --color-success-border: #a3c293; - --color-success-bg: #fcfff5; - --color-success-text: #2c662d; - --color-warning-border: #c9ba9b; - --color-warning-bg: #fffaf3; - --color-warning-text: #573a08; - --color-info-border: #a9d5de; - --color-info-bg: #f8ffff; - --color-info-text: #276f86; + --color-error-border: #d63333; + --color-error-bg: #ffebeb; + --color-error-bg-active: #fdd; + --color-error-bg-hover: #fee; + --color-error-text: #8a3231; + --color-success-border: #49842b; + --color-success-bg: #eef6e4; + --color-success-text: #2f6e30; + --color-warning-border: #bf8700; + --color-warning-bg: #fff8e1; + --color-warning-text: #744500; + --color-info-border: #2d8fa8; + --color-info-bg: #e8f4fd; + --color-info-text: #216078; --color-red-badge: #db2828; --color-red-badge-bg: #db28281a; --color-red-badge-hover-bg: #db28284d; diff --git a/web_src/js/components/ActionRunJobView.vue b/web_src/js/components/ActionRunJobView.vue index 747889d04cc..fba78917c9c 100644 --- a/web_src/js/components/ActionRunJobView.vue +++ b/web_src/js/components/ActionRunJobView.vue @@ -229,7 +229,8 @@ function createLogLine(stepIndex: number, startTime: number, line: LogLine, cmd: toggleElem(logTimeStamp, timeVisible.value['log-time-stamp']); toggleElem(logTimeSeconds, timeVisible.value['log-time-seconds']); - return createElementFromAttrs('div', {id: `jobstep-${stepIndex}-${line.index}`, class: 'job-log-line'}, + const lineClass = cmd?.name ? `job-log-line log-line-${cmd.name}` : 'job-log-line'; + return createElementFromAttrs('div', {id: `jobstep-${stepIndex}-${line.index}`, class: lineClass}, lineNum, logTimeStamp, logMsg, logTimeSeconds, ); } @@ -650,8 +651,28 @@ async function hashChangeListener() { color: var(--color-ansi-blue); } -.job-step-logs .job-log-line .log-cmd-error { - color: var(--color-ansi-red); +.job-step-logs .log-msg-label { + font-weight: var(--font-weight-semibold); +} + +.job-step-logs .log-line-error { + background: var(--color-error-bg); +} + +.job-step-logs .log-line-warning { + background: var(--color-warning-bg); +} + +.job-step-logs .log-cmd-error > .log-msg-label { + color: var(--color-error-text); +} + +.job-step-logs .log-cmd-warning > .log-msg-label { + color: var(--color-warning-text); +} + +.job-step-logs .log-cmd-debug { + color: var(--color-violet); } /* selectors here are intentionally exact to only match fullscreen */ diff --git a/web_src/js/components/ActionRunView.test.ts b/web_src/js/components/ActionRunView.test.ts index f0e3fa090ae..1f972b73c0d 100644 --- a/web_src/js/components/ActionRunView.test.ts +++ b/web_src/js/components/ActionRunView.test.ts @@ -8,8 +8,14 @@ test('LogLineMessage', () => { '##[endgroup]': '', '::endgroup::': '', - // parser shouldn't do any trim, keep origin output as-is - '##[error] foo': ' foo', + '##[error] foo': 'Error: foo', + '##[warning] foo': 'Warning: foo', + '##[notice] foo': 'Notice: foo', + '##[debug] foo': 'Debug: foo', + '::error::foo': 'Error: foo', + '::warning file=test.js,line=1::foo': 'Warning: foo', + '::notice::foo': 'Notice: foo', + '::debug::foo': 'Debug: foo', '[command] foo': ' foo', // hidden is special, it is actually skipped before creating diff --git a/web_src/js/components/ActionRunView.ts b/web_src/js/components/ActionRunView.ts index 6ae09a46fec..250f39e811d 100644 --- a/web_src/js/components/ActionRunView.ts +++ b/web_src/js/components/ActionRunView.ts @@ -17,6 +17,9 @@ const LogLinePrefixCommandMap: Record = { '##[endgroup]': 'endgroup', '##[error]': 'error', + '##[warning]': 'warning', + '##[notice]': 'notice', + '##[debug]': 'debug', '[command]': 'command', // https://github.com/actions/toolkit/blob/master/docs/commands.md @@ -26,13 +29,16 @@ const LogLinePrefixCommandMap: Record = { '::remove-matcher': 'hidden', // it has arguments }; +// Pattern for ::cmd:: and ::cmd args:: format (args are stripped for display) +const LogLineCmdPattern = /^::(error|warning|notice|debug)(?:\s[^:]*)?::/; + export type LogLine = { index: number; timestamp: number; message: string; }; -export type LogLineCommandName = 'group' | 'endgroup' | 'command' | 'error' | 'hidden'; +export type LogLineCommandName = 'group' | 'endgroup' | 'command' | 'error' | 'warning' | 'notice' | 'debug' | 'hidden'; export type LogLineCommand = { name: LogLineCommandName, prefix: string, @@ -45,19 +51,39 @@ export function parseLogLineCommand(line: LogLine): LogLineCommand | null { return {name: LogLinePrefixCommandMap[prefix], prefix}; } } + // Handle ::cmd:: and ::cmd args:: format (runner may pass these through raw) + const match = LogLineCmdPattern.exec(line.message); + if (match) { + return {name: match[1] as LogLineCommandName, prefix: match[0]}; + } return null; } +const LogLineLabelMap: Partial> = { + 'error': 'Error', + 'warning': 'Warning', + 'notice': 'Notice', + 'debug': 'Debug', +}; + export function createLogLineMessage(line: LogLine, cmd: LogLineCommand | null) { const logMsgAttrs = {class: 'log-msg'}; - if (cmd?.name) logMsgAttrs.class += ` log-cmd-${cmd?.name}`; // make it easier to add styles to some commands like "error" + if (cmd?.name) logMsgAttrs.class += ` log-cmd-${cmd.name}`; // make it easier to add styles to some commands like "error" // TODO: for some commands (::group::), the "prefix removal" works well, for some commands with "arguments" (::remove-matcher ...::), // it needs to do further processing in the future (fortunately, at the moment we don't need to handle these commands) const msgContent = cmd ? line.message.substring(cmd.prefix.length) : line.message; const logMsg = createElementFromAttrs('span', logMsgAttrs); - logMsg.innerHTML = renderAnsi(msgContent); + const label = cmd ? LogLineLabelMap[cmd.name] : null; + if (label) { + logMsg.append(createElementFromAttrs('span', {class: 'log-msg-label'}, `${label}:`)); + const msgSpan = document.createElement('span'); + msgSpan.innerHTML = ` ${renderAnsi(msgContent.trimStart())}`; + logMsg.append(msgSpan); + } else { + logMsg.innerHTML = renderAnsi(msgContent); + } return logMsg; } From 8fdd6d1235393f6e5cd3027872121a3a9868d3d1 Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Thu, 26 Mar 2026 12:48:04 -0600 Subject: [PATCH 13/16] Fix missing `workflow_run` notifications when updating jobs from multiple runs (#36997) This PR fixes `notifyWorkflowJobStatusUpdate` to send `WorkflowRunStatusUpdate` for each affected workflow run instead of only the first run in the input job list. --- services/actions/clear_tasks.go | 9 ++- tests/integration/repo_webhook_test.go | 83 ++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index e49bda1b164..c71f63e7d17 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -40,6 +40,8 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac if len(jobs) == 0 { return } + // The input jobs may belong to different runs, so track each affected run. + runs := make(map[int64]*actions_model.ActionRun, len(jobs)) for _, job := range jobs { if err := job.LoadAttributes(ctx); err != nil { log.Error("Failed to load job attributes: %v", err) @@ -47,10 +49,13 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac } CreateCommitStatusForRunJobs(ctx, job.Run, job) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + if _, ok := runs[job.RunID]; !ok { + runs[job.RunID] = job.Run + } } - if job := jobs[0]; job.Run != nil && job.Run.Repo != nil { - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + for _, run := range runs { + notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) } } diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index a90f50078e8..9ac9cced703 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/repo" @@ -1146,6 +1147,10 @@ func Test_WebhookWorkflowRun(t *testing.T) { testWorkflowRunEventsOnCancellingAbandonedRun(t, webhookData, false) }, }, + { + name: "WorkflowRunOnStoppingEndlessTasksForMultipleRuns", + testFunc: testWorkflowRunOnStoppingEndlessTasksForMultipleRuns, + }, } for _, obj := range testCases { t.Run(obj.name, func(t *testing.T) { @@ -1576,6 +1581,84 @@ jobs: assert.Equal(t, "user2/"+repoName, webhookData.payloads[1].Repo.FullName) } +func testWorkflowRunOnStoppingEndlessTasksForMultipleRuns(t *testing.T, webhookData *workflowRunWebhook) { + defer test.MockVariableValue(&setting.Actions.EndlessTaskTimeout, time.Second)() + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, "user2") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + repoName := "test-workflow-run-stop-endless-tasks" + testRepo := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: createActionsTestRepo(t, token, repoName, false).ID}) + + testAPICreateWebhookForRepo(t, session, "user2", repoName, webhookData.URL, "workflow_run") + + runners := make([]*mockRunner, 2) + for i := range runners { + runners[i] = newMockRunner() + runners[i].registerAsRepoRunner(t, "user2", repoName, fmt.Sprintf("mock-runner-%d", i), []string{"ubuntu-latest"}, false) + } + + workflowPath1 := ".gitea/workflows/endless-1.yml" + workflowPath2 := ".gitea/workflows/endless-2.yml" + workflowContent1 := `name: endless-1 +on: + push: + paths: + - '.gitea/workflows/endless-1.yml' +jobs: + job-1: + runs-on: ubuntu-latest + steps: + - run: echo 'job-1' +` + workflowContent2 := `name: endless-2 +on: + push: + paths: + - '.gitea/workflows/endless-2.yml' +jobs: + job-2: + runs-on: ubuntu-latest + steps: + - run: echo 'job-2' +` + + opts1 := getWorkflowCreateFileOptions(user2, testRepo.DefaultBranch, "create "+workflowPath1, workflowContent1) + createWorkflowFile(t, token, "user2", repoName, workflowPath1, opts1) + opts2 := getWorkflowCreateFileOptions(user2, testRepo.DefaultBranch, "create "+workflowPath2, workflowContent2) + createWorkflowFile(t, token, "user2", repoName, workflowPath2, opts2) + + task1 := runners[0].fetchTask(t) + task2 := runners[1].fetchTask(t) + _, job1, _ := getTaskAndJobAndRunByTaskID(t, task1.Id) + _, job2, _ := getTaskAndJobAndRunByTaskID(t, task2.Id) + require.NotEqual(t, job1.RunID, job2.RunID) + + initialRunEventsLen := len(webhookData.payloads) + + time.Sleep(2 * time.Second) + + require.NoError(t, actions.StopEndlessTasks(t.Context())) + + require.Len(t, webhookData.payloads, initialRunEventsLen+2) + + var completedRunIDs []int64 + for _, payload := range webhookData.payloads[initialRunEventsLen:] { + assert.Equal(t, "completed", payload.Action) + assert.Equal(t, "completed", payload.WorkflowRun.Status) + completedRunIDs = append(completedRunIDs, payload.WorkflowRun.ID) + } + assert.Len(t, completedRunIDs, 2) + assert.Contains(t, completedRunIDs, job1.RunID) + assert.Contains(t, completedRunIDs, job2.RunID) + + run1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job1.RunID}) + run2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job2.RunID}) + assert.Equal(t, actions_model.StatusFailure, run1.Status) + assert.Equal(t, actions_model.StatusFailure, run2.Status) +} + func testWebhookWorkflowRun(t *testing.T, webhookData *workflowRunWebhook) { // 1. create a new webhook with special webhook for repo1 user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) From 12737883ba08f48ccd85d4f7114117be64f33baf Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Fri, 27 Mar 2026 00:53:48 +0000 Subject: [PATCH 14/16] [skip ci] Updated translations via Crowdin --- options/locale/locale_fr-FR.json | 73 +++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/options/locale/locale_fr-FR.json b/options/locale/locale_fr-FR.json index e0d6cb541e4..6d0a7ccb6cd 100644 --- a/options/locale/locale_fr-FR.json +++ b/options/locale/locale_fr-FR.json @@ -81,6 +81,7 @@ "retry": "Réessayez", "rerun": "Relancer", "rerun_all": "Relancer toutes les tâches", + "rerun_failed": "Relancer les tâches échouées", "save": "Enregistrer", "add": "Ajouter", "add_all": "Tout Ajouter", @@ -168,6 +169,7 @@ "search.exact_tooltip": "Inclure uniquement les résultats qui correspondent exactement au terme de recherche", "search.repo_kind": "Chercher des dépôts…", "search.user_kind": "Chercher des utilisateurs…", + "search.badge_kind": "Chercher des badges…", "search.org_kind": "Chercher des organisations…", "search.team_kind": "Chercher des équipes…", "search.code_kind": "Chercher du code…", @@ -542,6 +544,7 @@ "form.glob_pattern_error": " a un motif glob invalide : %s.", "form.regex_pattern_error": " a un motif regex invalide : %s.", "form.username_error": " ne peut contenir que des caractères alphanumériques « a-z, A-Z, 0-9 », des traits d'union « - », des tirets bas « _ » et des points « . » et ne peux ni commencer, ni finir par des symboles, ni contenir des symboles consécutifs.", + "form.invalid_slug_error": " n’est pas valide.", "form.invalid_group_team_map_error": " a une cartographie invalide : %s", "form.unknown_error": "Erreur inconnue :", "form.captcha_incorrect": "Le code CAPTCHA est incorrect.", @@ -645,6 +648,7 @@ "user.block.note.edit": "Modifier la note", "user.block.list": "Utilisateurs bloqués", "user.block.list.none": "Vous n’avez bloqué aucun utilisateur.", + "settings.general": "Général", "settings.profile": "Profil", "settings.account": "Compte", "settings.appearance": "Apparence", @@ -965,7 +969,6 @@ "repo.visibility_description": "Seuls le propriétaire ou les membres de l'organisation, s'ils ont des droits, seront en mesure de le voir.", "repo.visibility_helper": "Rendre le dépôt privé", "repo.visibility_helper_forced": "L’administrateur requière que les nouveaux dépôts soient privés.", - "repo.visibility_fork_helper": "(Changer ceci affectera toutes les bifurcations.)", "repo.clone_helper": "Besoin d'aide pour dupliquer ? Visitez l'aide.", "repo.fork_repo": "Bifurquer le dépôt", "repo.fork_from": "Bifurquer depuis", @@ -2170,7 +2173,8 @@ "repo.settings.transfer_abort_invalid": "Vous ne pouvez pas annuler un transfert de dépôt inexistant.", "repo.settings.transfer_abort_success": "Le transfert du dépôt vers %s a bien été stoppé.", "repo.settings.transfer_desc": "Transférer ce dépôt à un autre utilisateur ou une organisation dont vous possédez des droits d'administrateur.", - "repo.settings.transfer_form_title": "Entrez le nom du dépôt pour confirmer :", + "repo.settings.enter_repo_name_to_confirm": "Entrez le nom du dépôt pour confirmer :", + "repo.settings.enter_repo_full_name_to_confirm": "Entrez le nom complet du dépôt (propriétaire/nom) pour confirmer :", "repo.settings.transfer_in_progress": "Il y a actuellement un transfert en cours. Veuillez l’annuler si vous souhaitez transférer ce dépôt à un autre utilisateur.", "repo.settings.transfer_notices_1": "- Vous perdrez l'accès à ce dépôt si vous le transférez à un autre utilisateur.", "repo.settings.transfer_notices_2": "- Vous conserverez l'accès à ce dépôt si vous le transférez à une organisation dont vous êtes (co-)propriétaire.", @@ -2316,7 +2320,7 @@ "repo.settings.event_workflow_run": "Exécution du flux de travail", "repo.settings.event_workflow_run_desc": "Tâche du flux de travail Gitea Actions ajoutée, en attente, en cours ou terminée.", "repo.settings.event_workflow_job": "Tâches du flux de travail", - "repo.settings.event_workflow_job_desc": "Travaux du flux de travail Gitea Actions en file d’attente, en attente, en cours ou terminée.", + "repo.settings.event_workflow_job_desc": "Tâches du flux de travail Gitea Actions en file d’attente, en attente, en cours ou terminée.", "repo.settings.event_package": "Paquet", "repo.settings.event_package_desc": "Paquet créé ou supprimé.", "repo.settings.branch_filter": "Filtre de branche", @@ -2473,7 +2477,10 @@ "repo.settings.visibility.private.text": "Rendre le dépôt privé rendra non seulement le dépôt visible uniquement aux membres autorisés, mais peut également rompre la relation entre lui et ses bifurcations, observateurs, et favoris.", "repo.settings.visibility.private.bullet_title": "Changer la visibilité en privé :", "repo.settings.visibility.private.bullet_one": "Rendra le dépôt visible uniquement aux membres autorisés.", - "repo.settings.visibility.private.bullet_two": "Peut supprimer la relation avec ses bifurcations, ses observateurs et ses favoris.", + "repo.settings.visibility.private.bullet_two": "Applique la visibilité aux bifurcation et retire les observateurs et les favoris.", + "repo.settings.visibility.private.stats_stars": "Il y a %d favori(s) sur ce dépôt qui pourrai(en)t être perdu(s).", + "repo.settings.visibility.private.stats_watchers": "Il y a %d observateur(s) sur ce dépôt qui pourrai(en)t être perdu(s).", + "repo.settings.visibility.private.stats_forks": "Il y a %d bifurcation(s) associée(s) à ce dépôt.", "repo.settings.visibility.public.button": "Rendre public", "repo.settings.visibility.public.text": "Rendre le dépôt public rendra le dépôt visible à tout le monde.", "repo.settings.visibility.public.bullet_title": "Changer la visibilité en public va :", @@ -2856,6 +2863,30 @@ "admin.hooks": "Déclencheurs web", "admin.integrations": "Intégrations", "admin.authentication": "Sources d'authentification", + "admin.badges": "Badges", + "admin.badges.badges_manage_panel": "Gestion du badge", + "admin.badges.details": "Détails du badge", + "admin.badges.new_badge": "Créer un nouveau badge", + "admin.badges.slug": "Limace", + "admin.badges.slug_been_taken": "Cette limace existe déjà.", + "admin.badges.description": "Description", + "admin.badges.image_url": "URL de l’image", + "admin.badges.new_success": "Le badge « %s » a été créé.", + "admin.badges.update_success": "Le badge a été actualisé.", + "admin.badges.deletion_success": "Le badge a été supprimé.", + "admin.badges.edit_badge": "Modifier le badge", + "admin.badges.update_badge": "Mettre à jour le badge", + "admin.badges.delete_badge": "Supprimer le badge", + "admin.badges.delete_badge_desc": "Êtes-vous sûr de vouloir supprimer définitivement ce badge ?", + "admin.badges.users_with_badge": "Utilisateurs avec badge : %s", + "admin.badges.not_found": "Badge introuvable.", + "admin.badges.user_already_has": "Cet utilisateur a déjà ce badge.", + "admin.badges.user_add_success": "Le badge a bien été assigné à l‘utilisateur.", + "admin.badges.user_remove_success": "Le badge a bien été retiré de l‘utilisateur.", + "admin.badges.manage_users": "Gérer les utilisateurs", + "admin.badges.add_user": "Ajouter un utilisateur", + "admin.badges.remove_user": "Supprimer l’utilisateur", + "admin.badges.delete_user_desc": "Êtes-vous sûr de vouloir supprimer cet utilisateur du badge ?", "admin.emails": "Courriels de l’utilisateur", "admin.config": "Configuration", "admin.config_summary": "Résumé", @@ -2946,7 +2977,7 @@ "admin.dashboard.gc_lfs": "Purger les métaobjets LFS", "admin.dashboard.stop_zombie_tasks": "Arrêter les tâches zombies", "admin.dashboard.stop_endless_tasks": "Arrêter les tâches interminables", - "admin.dashboard.cancel_abandoned_jobs": "Annuler les travaux abandonnés", + "admin.dashboard.cancel_abandoned_jobs": "Annuler les actions des tâches abandonnés", "admin.dashboard.start_schedule_tasks": "Démarrer les tâches planifiées", "admin.dashboard.sync_branch.started": "Début de la synchronisation des branches", "admin.dashboard.sync_tag.started": "Synchronisation des étiquettes", @@ -3644,6 +3675,7 @@ "actions.runners.id": "ID", "actions.runners.name": "Nom", "actions.runners.owner_type": "Type", + "actions.runners.availability": "Disponibilité", "actions.runners.description": "Description", "actions.runners.labels": "Labels", "actions.runners.last_online": "Dernière fois en ligne", @@ -3659,6 +3691,12 @@ "actions.runners.update_runner": "Appliquer les modifications", "actions.runners.update_runner_success": "Exécuteur mis à jour avec succès", "actions.runners.update_runner_failed": "Impossible d'actualiser l'Exécuteur", + "actions.runners.enable_runner": "Activer cet exécuteur", + "actions.runners.enable_runner_success": "Exécuteur activé avec succès", + "actions.runners.enable_runner_failed": "Impossible d’activer l’exécuteur", + "actions.runners.disable_runner": "Désactiver cet exécuteur", + "actions.runners.disable_runner_success": "Exécuteur désactivé avec succès", + "actions.runners.disable_runner_failed": "Impossible de désactiver l’exécuteur", "actions.runners.delete_runner": "Supprimer cet exécuteur", "actions.runners.delete_runner_success": "Exécuteur supprimé avec succès", "actions.runners.delete_runner_failed": "Impossible de supprimer l'Exécuteur", @@ -3700,6 +3738,10 @@ "actions.runs.not_done": "Cette exécution du flux de travail n’est pas terminée.", "actions.runs.view_workflow_file": "Voir le fichier du flux de travail", "actions.runs.workflow_graph": "Graphique du flux", + "actions.runs.summary": "Résumé", + "actions.runs.all_jobs": "Toutes les tâches", + "actions.runs.triggered_via": "Déclenché via %s", + "actions.runs.total_duration": "Durée totale :", "actions.workflow.disable": "Désactiver le flux de travail", "actions.workflow.disable_success": "Le flux de travail « %s » a bien été désactivé.", "actions.workflow.enable": "Activer le flux de travail", @@ -3749,5 +3791,24 @@ "git.filemode.normal_file": "Fichier normal", "git.filemode.executable_file": "Fichier exécutable", "git.filemode.symbolic_link": "Lien symbolique", - "git.filemode.submodule": "Sous-module" + "git.filemode.submodule": "Sous-module", + "org.repos.none": "Aucun dépôt.", + "actions.general.permissions": "Permissions du jeton des actions", + "actions.general.token_permissions.mode": "Permissions par défaut du jeton", + "actions.general.token_permissions.mode.desc": "Une tâche d’Actions utilisera les permissions par défaut si aucune n’est déclarée dans le fichier du flux de travail.", + "actions.general.token_permissions.mode.permissive": "Permissif", + "actions.general.token_permissions.mode.permissive.desc": "Permissions en lecture et écriture sur le dépôt de la tâche.", + "actions.general.token_permissions.mode.restricted": "Restreint", + "actions.general.token_permissions.mode.restricted.desc": "Permissions en lecture seule pour le contenu (code, publications) sur le dépôt de la tâche.", + "actions.general.token_permissions.override_owner": "Écraser la configuration faite par le propriétaire", + "actions.general.token_permissions.override_owner_desc": "Si actif, ce dépôt utilisera sa propre configuration pour les actions au lieu de respecter celle du propriétaire (utilisateur ou organisation).", + "actions.general.token_permissions.maximum": "Permissions maximales du jeton", + "actions.general.token_permissions.maximum.description": "Les permissions effectives de la tâche des actions seront limitées par les permissions maximales.", + "actions.general.token_permissions.fork_pr_note": "Si une tâche est démarrée par une demande de fusion depuis une bifurcation, ses permissions effectives ne dépasseront pas les permissions en lecture-seule.", + "actions.general.token_permissions.customize_max_permissions": "Personnaliser les permissions maximales", + "actions.general.cross_repo": "Accès inter-dépôt", + "actions.general.cross_repo_desc": "Permet aux dépôts sélectionnés d’être visible en lecture-seule par tous les dépôts de ce propriétaire à l’aide de GITEA_TOKEN lors de l’exécution des tâches d’actions.", + "actions.general.cross_repo_selected": "Dépôts sélectionnés", + "actions.general.cross_repo_target_repos": "Dépôts cibles", + "actions.general.cross_repo_add": "Ajouter un dépôt cible" } From b3c69174632de10796cc0d78c1438fb8ae5e0861 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 04:39:24 +0100 Subject: [PATCH 15/16] Update JS dependencies (#37001) - Update all JS dependencies via `make update-js` - `webpack-cli` 6 to 7: remove `--disable-interpret` from Makefile - Fix lint: remove unnecessary type args, `toThrowError` to `toThrow` - Fix duplicate CSS selector detected by `stylelint` 17.6.0 - Change `updates.config.ts` to use `pin`, needed for `tailwindcss` - Pin `typescript` pending typescript-eslint/typescript-eslint#12123 --------- Co-authored-by: Claude (claude-opus-4-6) Co-authored-by: Giteabot --- Makefile | 4 +- package.json | 48 +- pnpm-lock.yaml | 2321 ++++++++--------- .../assets/img/svg/octicon-lockup-github.svg | 1 + public/assets/img/svg/octicon-logo-github.svg | 2 +- public/assets/img/svg/octicon-mark-github.svg | 2 +- updates.config.ts | 11 +- web_src/css/modules/dropdown.css | 7 +- web_src/js/features/repo-projects.ts | 4 +- web_src/js/utils.test.ts | 2 +- web_src/js/utils/dom.test.ts | 2 +- 11 files changed, 1147 insertions(+), 1257 deletions(-) create mode 100644 public/assets/img/svg/octicon-lockup-github.svg diff --git a/Makefile b/Makefile index 4d1bd96ea51..a55493ab809 100644 --- a/Makefile +++ b/Makefile @@ -382,7 +382,7 @@ watch: ## watch everything and continuously rebuild .PHONY: watch-frontend watch-frontend: node_modules ## watch frontend files and continuously rebuild @rm -rf $(WEBPACK_DEST_ENTRIES) - NODE_ENV=development $(NODE_VARS) pnpm exec webpack --watch --progress --disable-interpret + NODE_ENV=development $(NODE_VARS) pnpm exec webpack --watch --progress .PHONY: watch-backend watch-backend: ## watch backend files and continuously rebuild @@ -783,7 +783,7 @@ $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) pnpm-lock.yaml @$(MAKE) -s node_modules @rm -rf $(WEBPACK_DEST_ENTRIES) @echo "Running webpack..." - @BROWSERSLIST_IGNORE_OLD_DATA=true $(NODE_VARS) pnpm exec webpack --disable-interpret + @BROWSERSLIST_IGNORE_OLD_DATA=true $(NODE_VARS) pnpm exec webpack @touch $(WEBPACK_DEST) .PHONY: svg diff --git a/package.json b/package.json index 4dd3f14e069..ccdb7f90a90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "type": "module", - "packageManager": "pnpm@10.30.3", + "packageManager": "pnpm@10.33.0", "engines": { "node": ">= 22.6.0", "pnpm": ">= 10.0.0" @@ -14,8 +14,8 @@ "@github/paste-markdown": "1.5.3", "@github/text-expander-element": "2.9.4", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@mermaid-js/layout-elk": "0.2.0", - "@primer/octicons": "19.22.0", + "@mermaid-js/layout-elk": "0.2.1", + "@primer/octicons": "19.23.1", "@resvg/resvg-wasm": "2.6.2", "@silverwind/vue3-calendar-heatmap": "2.1.1", "@techknowlogick/license-checker-webpack-plugin": "0.3.0", @@ -30,7 +30,7 @@ "compare-versions": "6.1.1", "cropperjs": "1.6.2", "css-loader": "7.1.4", - "dayjs": "1.11.19", + "dayjs": "1.11.20", "dropzone": "6.0.0-beta.2", "easymde": "2.20.0", "esbuild-loader": "4.4.2", @@ -38,9 +38,9 @@ "idiomorph": "0.7.4", "jquery": "4.0.0", "js-yaml": "4.1.1", - "katex": "0.16.37", - "mermaid": "11.12.3", - "mini-css-extract-plugin": "2.10.0", + "katex": "0.16.43", + "mermaid": "11.13.0", + "mini-css-extract-plugin": "2.10.2", "monaco-editor": "0.55.1", "monaco-editor-webpack-plugin": "7.1.1", "online-3d-viewer": "0.18.0", @@ -49,25 +49,25 @@ "postcss": "8.5.8", "postcss-loader": "8.2.1", "sortablejs": "1.15.7", - "swagger-ui-dist": "5.32.0", - "tailwindcss": "3.4.17", + "swagger-ui-dist": "5.32.1", + "tailwindcss": "3.4.19", "throttle-debounce": "5.0.2", "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", - "vue": "3.5.29", + "vue": "3.5.31", "vue-bar-graph": "2.2.0", "vue-chartjs": "5.3.3", "vue-loader": "17.4.2", "webpack": "5.105.4", - "webpack-cli": "6.0.1", + "webpack-cli": "7.0.2", "wrap-ansi": "10.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.7.1", - "@eslint/json": "1.1.0", + "@eslint/json": "1.2.0", "@playwright/test": "1.58.2", "@stylistic/eslint-plugin": "5.10.0", "@stylistic/stylelint-plugin": "5.0.1", @@ -76,16 +76,16 @@ "@types/jquery": "4.0.0", "@types/js-yaml": "4.0.9", "@types/katex": "0.16.8", - "@types/node": "25.3.5", + "@types/node": "25.5.0", "@types/pdfobject": "2.2.5", "@types/sortablejs": "1.15.9", "@types/swagger-ui-dist": "3.30.6", "@types/throttle-debounce": "5.0.2", "@types/toastify-js": "1.12.4", - "@typescript-eslint/parser": "8.57.1", - "@vitejs/plugin-vue": "6.0.4", - "@vitest/eslint-plugin": "1.6.12", - "eslint": "10.0.3", + "@typescript-eslint/parser": "8.57.2", + "@vitejs/plugin-vue": "6.0.5", + "@vitest/eslint-plugin": "1.6.13", + "eslint": "10.1.0", "eslint-import-resolver-typescript": "4.4.4", "eslint-plugin-array-func": "5.1.1", "eslint-plugin-github": "6.0.0", @@ -99,25 +99,25 @@ "eslint-plugin-vue-scoped-css": "3.0.0", "eslint-plugin-wc": "3.1.0", "globals": "17.4.0", - "happy-dom": "20.8.3", + "happy-dom": "20.8.8", "jiti": "2.6.1", "markdownlint-cli": "0.48.0", "material-icon-theme": "5.32.0", "nolyfill": "1.0.44", "postcss-html": "1.8.1", "spectral-cli-bundle": "1.0.7", - "stylelint": "17.4.0", + "stylelint": "17.6.0", "stylelint-config-recommended": "18.0.0", "stylelint-declaration-block-no-ignored-properties": "3.0.0", "stylelint-declaration-strict-value": "1.11.1", "stylelint-value-no-unknown-custom-properties": "6.1.1", "svgo": "4.0.1", "typescript": "5.9.3", - "typescript-eslint": "8.57.1", - "updates": "17.8.3", - "vite-string-plugin": "2.0.1", - "vitest": "4.0.18", - "vue-tsc": "3.2.5" + "typescript-eslint": "8.57.2", + "updates": "17.12.0", + "vite-string-plugin": "2.0.2", + "vitest": "4.1.2", + "vue-tsc": "3.2.6" }, "pnpm": { "peerDependencyRules": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bb595bf0ed..4c678b00970 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,17 +51,17 @@ importers: specifier: 0.1.0-alpha-3 version: 0.1.0-alpha-3 '@mermaid-js/layout-elk': - specifier: 0.2.0 - version: 0.2.0(mermaid@11.12.3) + specifier: 0.2.1 + version: 0.2.1(mermaid@11.13.0) '@primer/octicons': - specifier: 19.22.0 - version: 19.22.0 + specifier: 19.23.1 + version: 19.23.1 '@resvg/resvg-wasm': specifier: 2.6.2 version: 2.6.2 '@silverwind/vue3-calendar-heatmap': specifier: 2.1.1 - version: 2.1.1(tippy.js@6.3.7)(vue@3.5.29(typescript@5.9.3)) + version: 2.1.1(tippy.js@6.3.7)(vue@3.5.31(typescript@5.9.3)) '@techknowlogick/license-checker-webpack-plugin': specifier: 0.3.0 version: 0.3.0(webpack@5.105.4) @@ -79,7 +79,7 @@ importers: version: 4.5.1 chartjs-adapter-dayjs-4: specifier: 1.0.4 - version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.19) + version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.20) chartjs-plugin-zoom: specifier: 2.2.0 version: 2.2.0(chart.js@4.5.1) @@ -99,8 +99,8 @@ importers: specifier: 7.1.4 version: 7.1.4(webpack@5.105.4) dayjs: - specifier: 1.11.19 - version: 1.11.19 + specifier: 1.11.20 + version: 1.11.20 dropzone: specifier: 6.0.0-beta.2 version: 6.0.0-beta.2 @@ -123,14 +123,14 @@ importers: specifier: 4.1.1 version: 4.1.1 katex: - specifier: 0.16.37 - version: 0.16.37 + specifier: 0.16.43 + version: 0.16.43 mermaid: - specifier: 11.12.3 - version: 11.12.3 + specifier: 11.13.0 + version: 11.13.0 mini-css-extract-plugin: - specifier: 2.10.0 - version: 2.10.0(webpack@5.105.4) + specifier: 2.10.2 + version: 2.10.2(webpack@5.105.4) monaco-editor: specifier: 0.55.1 version: 0.55.1 @@ -156,11 +156,11 @@ importers: specifier: 1.15.7 version: 1.15.7 swagger-ui-dist: - specifier: 5.32.0 - version: 5.32.0 + specifier: 5.32.1 + version: 5.32.1 tailwindcss: - specifier: 3.4.17 - version: 3.4.17 + specifier: 3.4.19 + version: 3.4.19 throttle-debounce: specifier: 5.0.2 version: 5.0.2 @@ -180,42 +180,42 @@ importers: specifier: 0.7.2 version: 0.7.2 vue: - specifier: 3.5.29 - version: 3.5.29(typescript@5.9.3) + specifier: 3.5.31 + version: 3.5.31(typescript@5.9.3) vue-bar-graph: specifier: 2.2.0 version: 2.2.0(typescript@5.9.3) vue-chartjs: specifier: 5.3.3 - version: 5.3.3(chart.js@4.5.1)(vue@3.5.29(typescript@5.9.3)) + version: 5.3.3(chart.js@4.5.1)(vue@3.5.31(typescript@5.9.3)) vue-loader: specifier: 17.4.2 - version: 17.4.2(vue@3.5.29(typescript@5.9.3))(webpack@5.105.4) + version: 17.4.2(vue@3.5.31(typescript@5.9.3))(webpack@5.105.4) webpack: specifier: 5.105.4 - version: 5.105.4(webpack-cli@6.0.1) + version: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) webpack-cli: - specifier: 6.0.1 - version: 6.0.1(webpack@5.105.4) + specifier: 7.0.2 + version: 7.0.2(webpack@5.105.4) wrap-ansi: specifier: 10.0.0 version: 10.0.0 devDependencies: '@eslint-community/eslint-plugin-eslint-comments': specifier: 4.7.1 - version: 4.7.1(eslint@10.0.3(jiti@2.6.1)) + version: 4.7.1(eslint@10.1.0(jiti@2.6.1)) '@eslint/json': - specifier: 1.1.0 - version: 1.1.0 + specifier: 1.2.0 + version: 1.2.0 '@playwright/test': specifier: 1.58.2 version: 1.58.2 '@stylistic/eslint-plugin': specifier: 5.10.0 - version: 5.10.0(eslint@10.0.3(jiti@2.6.1)) + version: 5.10.0(eslint@10.1.0(jiti@2.6.1)) '@stylistic/stylelint-plugin': specifier: 5.0.1 - version: 5.0.1(stylelint@17.4.0(typescript@5.9.3)) + version: 5.0.1(stylelint@17.6.0(typescript@5.9.3)) '@types/codemirror': specifier: 5.60.17 version: 5.60.17 @@ -232,8 +232,8 @@ importers: specifier: 0.16.8 version: 0.16.8 '@types/node': - specifier: 25.3.5 - version: 25.3.5 + specifier: 25.5.0 + version: 25.5.0 '@types/pdfobject': specifier: 2.2.5 version: 2.2.5 @@ -250,59 +250,59 @@ importers: specifier: 1.12.4 version: 1.12.4 '@typescript-eslint/parser': - specifier: 8.57.1 - version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.57.2 + version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@vitejs/plugin-vue': - specifier: 6.0.4 - version: 6.0.4(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) + specifier: 6.0.5 + version: 6.0.5(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))(vue@3.5.31(typescript@5.9.3)) '@vitest/eslint-plugin': - specifier: 1.6.12 - version: 1.6.12(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + specifier: 1.6.13 + version: 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))) eslint: - specifier: 10.0.3 - version: 10.0.3(jiti@2.6.1) + specifier: 10.1.0 + version: 10.1.0(jiti@2.6.1) eslint-import-resolver-typescript: specifier: 4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.0.3(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-array-func: specifier: 5.1.1 - version: 5.1.1(eslint@10.0.3(jiti@2.6.1)) + version: 5.1.1(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-de-morgan: specifier: 2.1.1 - version: 2.1.1(eslint@10.0.3(jiti@2.6.1)) + version: 2.1.1(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-github: specifier: 6.0.0 - version: 6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) + version: 6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-import-x: specifier: 4.16.2 - version: 4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)) + version: 4.16.2(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-playwright: specifier: 2.10.1 - version: 2.10.1(eslint@10.0.3(jiti@2.6.1)) + version: 2.10.1(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-regexp: specifier: 3.1.0 - version: 3.1.0(eslint@10.0.3(jiti@2.6.1)) + version: 3.1.0(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-sonarjs: specifier: 4.0.2 - version: 4.0.2(eslint@10.0.3(jiti@2.6.1)) + version: 4.0.2(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-unicorn: specifier: 63.0.0 - version: 63.0.0(eslint@10.0.3(jiti@2.6.1)) + version: 63.0.0(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-vue: specifier: 10.8.0 - version: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.0.3(jiti@2.6.1)))(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))) + version: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1)))(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1))) eslint-plugin-vue-scoped-css: specifier: 3.0.0 - version: 3.0.0(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))) + version: 3.0.0(eslint@10.1.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1))) eslint-plugin-wc: specifier: 3.1.0 - version: 3.1.0(eslint@10.0.3(jiti@2.6.1)) + version: 3.1.0(eslint@10.1.0(jiti@2.6.1)) globals: specifier: 17.4.0 version: 17.4.0 happy-dom: - specifier: 20.8.3 - version: 20.8.3 + specifier: 20.8.8 + version: 20.8.8 jiti: specifier: 2.6.1 version: 2.6.1 @@ -322,20 +322,20 @@ importers: specifier: 1.0.7 version: 1.0.7 stylelint: - specifier: 17.4.0 - version: 17.4.0(typescript@5.9.3) + specifier: 17.6.0 + version: 17.6.0(typescript@5.9.3) stylelint-config-recommended: specifier: 18.0.0 - version: 18.0.0(stylelint@17.4.0(typescript@5.9.3)) + version: 18.0.0(stylelint@17.6.0(typescript@5.9.3)) stylelint-declaration-block-no-ignored-properties: specifier: 3.0.0 - version: 3.0.0(stylelint@17.4.0(typescript@5.9.3)) + version: 3.0.0(stylelint@17.6.0(typescript@5.9.3)) stylelint-declaration-strict-value: specifier: 1.11.1 - version: 1.11.1(stylelint@17.4.0(typescript@5.9.3)) + version: 1.11.1(stylelint@17.6.0(typescript@5.9.3)) stylelint-value-no-unknown-custom-properties: specifier: 6.1.1 - version: 6.1.1(stylelint@17.4.0(typescript@5.9.3)) + version: 6.1.1(stylelint@17.6.0(typescript@5.9.3)) svgo: specifier: 4.0.1 version: 4.0.1 @@ -343,20 +343,20 @@ importers: specifier: 5.9.3 version: 5.9.3 typescript-eslint: - specifier: 8.57.1 - version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.57.2 + version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) updates: - specifier: 17.8.3 - version: 17.8.3 + specifier: 17.12.0 + version: 17.12.0 vite-string-plugin: - specifier: 2.0.1 - version: 2.0.1(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + specifier: 2.0.2 + version: 2.0.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) vitest: - specifier: 4.0.18 - version: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + specifier: 4.1.2 + version: 4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) vue-tsc: - specifier: 3.2.5 - version: 3.2.5(typescript@5.9.3) + specifier: 3.2.6 + version: 3.2.6(typescript@5.9.3) packages: @@ -379,13 +379,13 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.28.6': - resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} '@babel/types@7.29.0': @@ -477,8 +477,13 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.0': - resolution: {integrity: sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==} + '@csstools/css-syntax-patches-for-csstree@1.1.1': + resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true '@csstools/css-tokenizer@4.0.0': resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} @@ -503,171 +508,171 @@ packages: peerDependencies: postcss-selector-parser: ^7.1.1 - '@discoveryjs/json-ext@0.6.3': - resolution: {integrity: sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==} + '@discoveryjs/json-ext@1.0.0': + resolution: {integrity: sha512-dDlz3W405VMFO4w5kIP9DOmELBcvFQGmLoKSdIRstBDubKFYwaNHV1NnlzMCQpXQFGWVALmeMORAuiLx18AvZQ==} engines: {node: '>=14.17.0'} - '@emnapi/core@1.8.1': - resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -713,16 +718,16 @@ packages: resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.3.4': - resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.3': - resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==} + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/json@1.1.0': - resolution: {integrity: sha512-noH9FUYqyhZSDf3Yq5HswsjDH/MWJAatMooWwT5YgQ0XHMekoFc/iyEufP+7kD1kaOj9qwFiXySqHsKii3zmlw==} + '@eslint/json@1.2.0': + resolution: {integrity: sha512-CEFEyNgvzu8zn5QwVYDg3FaG+ZKUeUsNYitFpMYJAqoAlnw68EQgNbUfheSmexZr4n0wZPrAkPLuvsLaXO6wRw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/object-schema@3.0.3': @@ -808,17 +813,20 @@ packages: '@mcaptcha/vanilla-glue@0.1.0-alpha-3': resolution: {integrity: sha512-GT6TJBgmViGXcXiT5VOr+h/6iOnThSlZuCoOWncubyTZU9R3cgU5vWPkF7G6Ob6ee2CBe3yqBxxk24CFVGTVXw==} - '@mermaid-js/layout-elk@0.2.0': - resolution: {integrity: sha512-vjjYGnCCjYlIA/rR7M//eFi0rHM6dsMyN1JQKfckpt30DTC/esrw36hcrvA2FNPHaqh3Q/SyBWzddyaky8EtUQ==} + '@mermaid-js/layout-elk@0.2.1': + resolution: {integrity: sha512-MX9jwhMyd5zDcFsYcl3duDUkKhjVRUCGEQrdCeNV5hCIR6+3FuDDbRbFmvVbAu15K1+juzsYGG+K8MDvCY1Amg==} peerDependencies: mermaid: ^11.0.2 - '@mermaid-js/parser@1.0.0': - resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==} + '@mermaid-js/parser@1.0.1': + resolution: {integrity: sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==} '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -890,6 +898,9 @@ packages: resolution: {integrity: sha512-3dsKlf4Ma7o+uxLIg5OI1Tgwfet2pE8WTbPjEGWvOe6CSjMtK0skJnnSVHaEVX4N4mYU81To0qDeZOPqjaUotg==} engines: {node: '>=12.4.0'} + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@package-json/types@0.0.12': resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} @@ -905,153 +916,113 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@primer/octicons@19.22.0': - resolution: {integrity: sha512-nWoh9PlE6u7xbiZF3KcUm3ktLpN2rQPt11trwp/t4EsKuYRNVWVbBp1LkCBsvZq7ScckNKUURLigIU0wS1FQdw==} + '@primer/octicons@19.23.1': + resolution: {integrity: sha512-CzjGmxkmNhyst6EekrS3SJPdtzgIkUMP/LSJch65y99/kmiFXbO1a+q7zoYe3hnI9NaOM0IN+ydDIbOmd8YqcA==} '@resvg/resvg-wasm@2.6.2': resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} engines: {node: '>= 10'} - '@rolldown/pluginutils@1.0.0-rc.2': - resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} - - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} - cpu: [x64] - os: [win32] + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + + '@rolldown/pluginutils@1.0.0-rc.2': + resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1213,8 +1184,8 @@ packages: '@types/d3@7.4.3': resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1261,8 +1232,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@25.3.5': - resolution: {integrity: sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==} + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} '@types/pdfobject@2.2.5': resolution: {integrity: sha512-7gD5tqc/RUDq0PyoLemL0vEHxBYi+zY0WVaFAx/Y0jBsXFgot1vB9No1GhDZGwRGJMCIZbgAb74QG9MTyTNU/g==} @@ -1294,115 +1265,63 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.56.1': - resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + '@typescript-eslint/eslint-plugin@8.57.2': + resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.56.1 + '@typescript-eslint/parser': ^8.57.2 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/eslint-plugin@8.57.1': - resolution: {integrity: sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.57.1 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/parser@8.57.1': - resolution: {integrity: sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==} + '@typescript-eslint/parser@8.57.2': + resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.56.1': - resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + '@typescript-eslint/project-service@8.57.2': + resolution: {integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.57.1': - resolution: {integrity: sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==} + '@typescript-eslint/scope-manager@8.57.2': + resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.57.2': + resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.56.1': - resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/scope-manager@8.57.1': - resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.56.1': - resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/tsconfig-utils@8.57.1': - resolution: {integrity: sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/type-utils@8.56.1': - resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + '@typescript-eslint/type-utils@8.57.2': + resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.57.1': - resolution: {integrity: sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==} + '@typescript-eslint/types@8.57.2': + resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.57.2': + resolution: {integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.57.2': + resolution: {integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.56.1': - resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/types@8.57.1': - resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.56.1': - resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/typescript-estree@8.57.1': - resolution: {integrity: sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.56.1': - resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.57.1': - resolution: {integrity: sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/visitor-keys@8.56.1': - resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.57.1': - resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==} + '@typescript-eslint/visitor-keys@8.57.2': + resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -1508,54 +1427,60 @@ packages: cpu: [x64] os: [win32] - '@vitejs/plugin-vue@6.0.4': - resolution: {integrity: sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==} + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + + '@vitejs/plugin-vue@6.0.5': + resolution: {integrity: sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 - '@vitest/eslint-plugin@1.6.12': - resolution: {integrity: sha512-4kI47BJNFE+EQ5bmPbHzBF+ibNzx2Fj0Jo9xhWsTPxMddlHwIWl6YAxagefh461hrwx/W0QwBZpxGS404kBXyg==} + '@vitest/eslint-plugin@1.6.13': + resolution: {integrity: sha512-ui7JGWBoQpS5NKKW0FDb1eTuFEZ5EupEv2Psemuyfba7DfA5K52SeDLelt6P4pQJJ/4UGkker/BgMk/KrjH3WQ==} engines: {node: '>=18'} peerDependencies: + '@typescript-eslint/eslint-plugin': '*' eslint: '>=8.57.0' typescript: '>=5.0.0' vitest: '*' peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true typescript: optional: true vitest: optional: true - '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} - '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.0.18': - resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} - '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} - '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} - '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} - '@vitest/utils@4.0.18': - resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} '@volar/language-core@2.4.28': resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} @@ -1566,37 +1491,37 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} - '@vue/compiler-core@3.5.29': - resolution: {integrity: sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==} + '@vue/compiler-core@3.5.31': + resolution: {integrity: sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==} - '@vue/compiler-dom@3.5.29': - resolution: {integrity: sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==} + '@vue/compiler-dom@3.5.31': + resolution: {integrity: sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==} - '@vue/compiler-sfc@3.5.29': - resolution: {integrity: sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==} + '@vue/compiler-sfc@3.5.31': + resolution: {integrity: sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==} - '@vue/compiler-ssr@3.5.29': - resolution: {integrity: sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==} + '@vue/compiler-ssr@3.5.31': + resolution: {integrity: sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==} - '@vue/language-core@3.2.5': - resolution: {integrity: sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==} + '@vue/language-core@3.2.6': + resolution: {integrity: sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==} - '@vue/reactivity@3.5.29': - resolution: {integrity: sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==} + '@vue/reactivity@3.5.31': + resolution: {integrity: sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==} - '@vue/runtime-core@3.5.29': - resolution: {integrity: sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==} + '@vue/runtime-core@3.5.31': + resolution: {integrity: sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==} - '@vue/runtime-dom@3.5.29': - resolution: {integrity: sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==} + '@vue/runtime-dom@3.5.31': + resolution: {integrity: sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==} - '@vue/server-renderer@3.5.29': - resolution: {integrity: sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==} + '@vue/server-renderer@3.5.31': + resolution: {integrity: sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==} peerDependencies: - vue: 3.5.29 + vue: 3.5.31 - '@vue/shared@3.5.29': - resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} + '@vue/shared@3.5.31': + resolution: {integrity: sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==} '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -1643,31 +1568,6 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - '@webpack-cli/configtest@3.0.1': - resolution: {integrity: sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==} - engines: {node: '>=18.12.0'} - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - - '@webpack-cli/info@3.0.1': - resolution: {integrity: sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==} - engines: {node: '>=18.12.0'} - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - - '@webpack-cli/serve@3.0.1': - resolution: {integrity: sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==} - engines: {node: '>=18.12.0'} - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - webpack-dev-server: '*' - peerDependenciesMeta: - webpack-dev-server: - optional: true - '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -1793,8 +1693,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.0: - resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + baseline-browser-mapping@2.10.11: + resolution: {integrity: sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==} engines: {node: '>=6.0.0'} hasBin: true @@ -1811,8 +1711,8 @@ packages: brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -1842,8 +1742,8 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cacheable@2.3.3: - resolution: {integrity: sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==} + cacheable@2.3.4: + resolution: {integrity: sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==} callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -1853,8 +1753,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001777: - resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + caniuse-lite@1.0.30001781: + resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} @@ -1945,17 +1845,10 @@ packages: colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} - colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} - commander@12.1.0: - resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} - engines: {node: '>=18'} - commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -1988,8 +1881,11 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - core-js-compat@3.48.0: - resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} core-js@3.32.2: resolution: {integrity: sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==} @@ -2212,14 +2108,14 @@ packages: resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} engines: {node: '>=12'} - dagre-d3-es@7.0.13: - resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - dayjs@1.11.19: - resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -2252,13 +2148,17 @@ packages: resolution: {integrity: sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==} engines: {node: '>=0.10.0'} - delaunator@5.0.1: - resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -2288,9 +2188,8 @@ packages: dompurify@3.2.7: resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} - dompurify@3.3.2: - resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==} - engines: {node: '>=20'} + dompurify@3.3.3: + resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2301,8 +2200,8 @@ packages: easymde@2.20.0: resolution: {integrity: sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==} - electron-to-chromium@1.5.307: - resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + electron-to-chromium@1.5.325: + resolution: {integrity: sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==} elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} @@ -2317,8 +2216,8 @@ packages: resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} engines: {node: '>= 4'} - enhanced-resolve@5.20.0: - resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -2341,9 +2240,6 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} @@ -2352,8 +2248,8 @@ packages: peerDependencies: webpack: ^4.40.0 || ^5.0.0 - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} engines: {node: '>=18'} hasBin: true @@ -2571,10 +2467,6 @@ packages: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} - eslint-scope@9.1.1: - resolution: {integrity: sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint-scope@9.1.2: resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -2591,8 +2483,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.0.3: - resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==} + eslint@10.1.0: + resolution: {integrity: sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2605,8 +2497,8 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@11.1.1: - resolution: {integrity: sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==} + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} esquery@1.7.0: @@ -2714,15 +2606,15 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flat-cache@6.1.20: - resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} + flat-cache@6.1.21: + resolution: {integrity: sha512-2u7cJfSf7Th7NxEk/VzQjnPoglok2YCsevS7TSbJjcDQWJPbqUUnSYtriHSvtnq+fRZHy1s0ugk4ApnQyhPGoQ==} flat@5.0.2: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true - flatted@3.3.4: - resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2744,8 +2636,8 @@ packages: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2799,8 +2691,8 @@ packages: resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} engines: {node: '>=0.8.0'} - happy-dom@20.8.3: - resolution: {integrity: sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ==} + happy-dom@20.8.8: + resolution: {integrity: sha512-5/F8wxkNxYtsN0bXfMwIyNLZ9WYsoOYPbmoluqVJqv8KBUbcyKZawJ7uYK4WTX8IHBLYv+VXIwfeNDPy1oKMwQ==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -2814,13 +2706,16 @@ packages: hash-sum@2.0.0: resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} - hashery@1.5.0: - resolution: {integrity: sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==} + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + hookified@2.1.0: + resolution: {integrity: sha512-ootKng4eaxNxa7rx6FJv2YKef3DuhqbEj3l70oGXwddPQEEnISm50TEZQclqiLTAtilT2nu7TErtCO523hHkyg==} + html-tags@5.1.0: resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==} engines: {node: '>=20.10'} @@ -3049,8 +2944,8 @@ packages: just-extend@5.1.1: resolution: {integrity: sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==} - katex@0.16.37: - resolution: {integrity: sha512-TIGjO2cCGYono+uUzgkE7RFF329mLLWGuHUlSr6cwIVj9O8f0VQZ783rsanmJpFUo32vvtj7XT04NGRPh+SZFg==} + katex@0.16.43: + resolution: {integrity: sha512-K7NL5JtGrFEglipOAjY4UYA69CnTuNmjArxeXF6+bw7h2OGySUPv6QWRjfb1gmutJ4Mw/qLeBqiROOEDULp4nA==} hasBin: true keyv@4.5.4: @@ -3091,6 +2986,80 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -3199,8 +3168,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.12.3: - resolution: {integrity: sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==} + mermaid@11.13.0: + resolution: {integrity: sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==} micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -3289,8 +3258,8 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mini-css-extract-plugin@2.10.0: - resolution: {integrity: sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==} + mini-css-extract-plugin@2.10.2: + resolution: {integrity: sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==} engines: {node: '>= 12.13.0'} peerDependencies: webpack: ^5.0.0 @@ -3305,8 +3274,8 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - mlly@1.8.1: - resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} monaco-editor-webpack-plugin@7.1.1: resolution: {integrity: sha512-WxdbFHS3Wtz4V9hzhe/Xog5hQRSMxmDLkEEYZwqMDHgJlkZo00HVFZR0j5d0nKypjTUkkygH3dDSXERLG4757A==} @@ -3469,12 +3438,12 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} pify@2.3.0: @@ -3528,16 +3497,22 @@ packages: peerDependencies: postcss: ^8.4.21 - postcss-load-config@4.0.2: - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} - engines: {node: '>= 14'} + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} peerDependencies: + jiti: '>=1.21.0' postcss: '>=8.0.9' - ts-node: '>=9.0.0' + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: + jiti: + optional: true postcss: optional: true - ts-node: + tsx: + optional: true + yaml: optional: true postcss-loader@8.2.1: @@ -3631,8 +3606,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qified@0.6.0: - resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==} + qified@0.9.0: + resolution: {integrity: sha512-4q61YgkHbY6gmwkqm0BsxyLDO3UYdrdiJTJ7JiaZb3xpW1duxn135SB7KqUEkCiuu5O4W+TtwEWP2VjmSRanvA==} engines: {node: '>=20'} queue-microtask@1.2.3: @@ -3697,12 +3672,12 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - robust-predicates@3.0.2: - resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true roughjs@4.6.6: @@ -3718,8 +3693,8 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - sax@1.5.0: - resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} schema-utils@4.3.3: @@ -3739,14 +3714,14 @@ packages: engines: {node: '>=10'} hasBin: true - seroval-plugins@1.5.0: - resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==} + seroval-plugins@1.5.1: + resolution: {integrity: sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 - seroval@1.5.0: - resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} + seroval@1.5.1: + resolution: {integrity: sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==} engines: {node: '>=10'} shallow-clone@3.0.1: @@ -3776,12 +3751,12 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} - smol-toml@1.6.0: - resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} - solid-js@1.9.11: - resolution: {integrity: sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==} + solid-js@1.9.12: + resolution: {integrity: sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==} solid-transition-group@0.2.3: resolution: {integrity: sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg==} @@ -3839,8 +3814,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -3901,8 +3876,8 @@ packages: peerDependencies: stylelint: '>=16' - stylelint@17.4.0: - resolution: {integrity: sha512-3kQ2/cHv3Zt8OBg+h2B8XCx9evEABQIrv4hh3uXahGz/ZEHrTR80zxBiK2NfXNaSoyBzxO1pjsz1Vhdzwn5XSw==} + stylelint@17.6.0: + resolution: {integrity: sha512-tokrsMIVAR9vAQ/q3UVEr7S0dGXCi7zkCezPRnS2kqPUulvUh5Vgfwngrk4EoAoW7wnrThqTdnTFN5Ra7CaxIg==} engines: {node: '>=20.19.0'} hasBin: true @@ -3951,8 +3926,8 @@ packages: svgson@5.3.1: resolution: {integrity: sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==} - swagger-ui-dist@5.32.0: - resolution: {integrity: sha512-nKZB0OuDvacB0s/lC2gbge+RigYvGRGpLLMWMFxaTUwfM+CfndVk9Th2IaTinqXiz6Mn26GK2zriCpv6/+5m3Q==} + swagger-ui-dist@5.32.1: + resolution: {integrity: sha512-6HQoo7+j8PA2QqP5kgAb9dl1uxUjvR0SAoL/WUp1sTEvm0F6D5npgU2OGCLwl++bIInqGlEUQ2mpuZRZYtyCzQ==} sync-fetch@0.4.5: resolution: {integrity: sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==} @@ -3966,17 +3941,17 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tailwindcss@3.4.17: - resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} hasBin: true - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} engines: {node: '>=6'} - terser-webpack-plugin@5.3.17: - resolution: {integrity: sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==} + terser-webpack-plugin@5.4.0: + resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -3991,8 +3966,8 @@ packages: uglify-js: optional: true - terser@5.46.0: - resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} + terser@5.46.1: + resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} engines: {node: '>=10'} hasBin: true @@ -4013,16 +3988,16 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} tippy.js@6.3.7: @@ -4041,8 +4016,8 @@ packages: tributejs@5.1.3: resolution: {integrity: sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==} - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -4064,8 +4039,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.57.1: - resolution: {integrity: sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==} + typescript-eslint@8.57.2: + resolution: {integrity: sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4104,8 +4079,8 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - updates@17.8.3: - resolution: {integrity: sha512-YTCzRy4rGdrDCuX8wrnDKn2KXYp5kJS1T94k/JKQED60+lMAxdxoasX61EG0zbWYBxQQpTvC3uAzyDu3U+2zZA==} + updates@17.12.0: + resolution: {integrity: sha512-BQvF31tGVSa79ykyonkSkS5AN91x46qZgJi0pHiIQnPH+eUkT+Xq9jIE+O0gRUKPvIDyjCmb31rn7Uf/YD6rLQ==} engines: {node: '>=22'} hasBin: true @@ -4122,20 +4097,21 @@ packages: vanilla-colorful@0.7.2: resolution: {integrity: sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg==} - vite-string-plugin@2.0.1: - resolution: {integrity: sha512-L5B86yQkYrqH5d966w1vI91B0d+0vmICgB6tqjINvtBIGU9qhFY7izqjytED/ApggFC4QTDWNjfF6nWMqY/fQg==} + vite-string-plugin@2.0.2: + resolution: {integrity: sha512-pHU9lZuUoMSYyZixdn2XBYko9IAhk3dr41CG6VsXrjB+wN2th06SZsO9mJm6+2NhKBJKNfRERaRej8TBcoq9tQ==} peerDependencies: vite: '*' - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -4146,12 +4122,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -4167,20 +4145,21 @@ packages: yaml: optional: true - vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + vitest@4.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.18 - '@vitest/browser-preview': 4.0.18 - '@vitest/browser-webdriverio': 4.0.18 - '@vitest/ui': 4.0.18 + '@vitest/browser-playwright': 4.1.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -4248,14 +4227,14 @@ packages: vue: optional: true - vue-tsc@3.2.5: - resolution: {integrity: sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==} + vue-tsc@3.2.6: + resolution: {integrity: sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==} hasBin: true peerDependencies: typescript: '>=5.0.0' - vue@3.5.29: - resolution: {integrity: sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==} + vue@3.5.31: + resolution: {integrity: sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -4269,14 +4248,14 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webpack-cli@6.0.1: - resolution: {integrity: sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==} - engines: {node: '>=18.12.0'} + webpack-cli@7.0.2: + resolution: {integrity: sha512-dB0R4T+C/8YuvM+fabdvil6QE44/ChDXikV5lOOkrUeCkW5hTJv2pGLE3keh+D5hjYw8icBaJkZzpFoaHV4T+g==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: - webpack: ^5.82.0 - webpack-bundle-analyzer: '*' - webpack-dev-server: '*' + webpack: ^5.101.0 + webpack-bundle-analyzer: ^4.0.0 || ^5.0.0 + webpack-dev-server: ^5.0.0 peerDependenciesMeta: webpack-bundle-analyzer: optional: true @@ -4347,8 +4326,8 @@ packages: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4369,11 +4348,6 @@ packages: xml-reader@2.4.3: resolution: {integrity: sha512-xWldrIxjeAMAu6+HSf9t50ot1uL5M+BtOidRCWHXIeewvSeIpscWCsp4Zxjk8kHHhdqFBrfK8U0EJeCcnyQ/gA==} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} - engines: {node: '>= 14.6'} - hasBin: true - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -4385,7 +4359,7 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 - tinyexec: 1.0.2 + tinyexec: 1.0.4 '@babel/code-frame@7.29.0': dependencies: @@ -4397,11 +4371,11 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} - '@babel/parser@7.29.0': + '@babel/parser@7.29.2': dependencies: '@babel/types': 7.29.0 - '@babel/runtime@7.28.6': {} + '@babel/runtime@7.29.2': {} '@babel/types@7.29.0': dependencies: @@ -4419,7 +4393,7 @@ snapshots: '@cacheable/utils@2.4.0': dependencies: - hashery: 1.5.0 + hashery: 1.5.1 keyv: 5.6.0 '@chevrotain/cst-dts-gen@11.1.2': @@ -4506,7 +4480,9 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.0': {} + '@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 '@csstools/css-tokenizer@4.0.0': {} @@ -4523,120 +4499,120 @@ snapshots: dependencies: postcss-selector-parser: 7.1.1 - '@discoveryjs/json-ext@0.6.3': {} + '@discoveryjs/json-ext@1.0.0': {} - '@emnapi/core@1.8.1': + '@emnapi/core@1.9.1': dependencies: - '@emnapi/wasi-threads': 1.1.0 + '@emnapi/wasi-threads': 1.2.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.8.1': + '@emnapi/runtime@1.9.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.1.0': + '@emnapi/wasi-threads@1.2.0': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.3': + '@esbuild/aix-ppc64@0.27.4': optional: true - '@esbuild/android-arm64@0.27.3': + '@esbuild/android-arm64@0.27.4': optional: true - '@esbuild/android-arm@0.27.3': + '@esbuild/android-arm@0.27.4': optional: true - '@esbuild/android-x64@0.27.3': + '@esbuild/android-x64@0.27.4': optional: true - '@esbuild/darwin-arm64@0.27.3': + '@esbuild/darwin-arm64@0.27.4': optional: true - '@esbuild/darwin-x64@0.27.3': + '@esbuild/darwin-x64@0.27.4': optional: true - '@esbuild/freebsd-arm64@0.27.3': + '@esbuild/freebsd-arm64@0.27.4': optional: true - '@esbuild/freebsd-x64@0.27.3': + '@esbuild/freebsd-x64@0.27.4': optional: true - '@esbuild/linux-arm64@0.27.3': + '@esbuild/linux-arm64@0.27.4': optional: true - '@esbuild/linux-arm@0.27.3': + '@esbuild/linux-arm@0.27.4': optional: true - '@esbuild/linux-ia32@0.27.3': + '@esbuild/linux-ia32@0.27.4': optional: true - '@esbuild/linux-loong64@0.27.3': + '@esbuild/linux-loong64@0.27.4': optional: true - '@esbuild/linux-mips64el@0.27.3': + '@esbuild/linux-mips64el@0.27.4': optional: true - '@esbuild/linux-ppc64@0.27.3': + '@esbuild/linux-ppc64@0.27.4': optional: true - '@esbuild/linux-riscv64@0.27.3': + '@esbuild/linux-riscv64@0.27.4': optional: true - '@esbuild/linux-s390x@0.27.3': + '@esbuild/linux-s390x@0.27.4': optional: true - '@esbuild/linux-x64@0.27.3': + '@esbuild/linux-x64@0.27.4': optional: true - '@esbuild/netbsd-arm64@0.27.3': + '@esbuild/netbsd-arm64@0.27.4': optional: true - '@esbuild/netbsd-x64@0.27.3': + '@esbuild/netbsd-x64@0.27.4': optional: true - '@esbuild/openbsd-arm64@0.27.3': + '@esbuild/openbsd-arm64@0.27.4': optional: true - '@esbuild/openbsd-x64@0.27.3': + '@esbuild/openbsd-x64@0.27.4': optional: true - '@esbuild/openharmony-arm64@0.27.3': + '@esbuild/openharmony-arm64@0.27.4': optional: true - '@esbuild/sunos-x64@0.27.3': + '@esbuild/sunos-x64@0.27.4': optional: true - '@esbuild/win32-arm64@0.27.3': + '@esbuild/win32-arm64@0.27.4': optional: true - '@esbuild/win32-ia32@0.27.3': + '@esbuild/win32-ia32@0.27.4': optional: true - '@esbuild/win32-x64@0.27.3': + '@esbuild/win32-x64@0.27.4': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.0.3(jiti@2.6.1))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.1.0(jiti@2.6.1))': dependencies: escape-string-regexp: 4.0.0 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) ignore: 7.0.5 - '@eslint-community/eslint-utils@4.9.1(eslint@10.0.3(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0(jiti@2.6.1))': dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@1.4.1(eslint@10.0.3(jiti@2.6.1))': + '@eslint/compat@1.4.1(eslint@10.1.0(jiti@2.6.1))': dependencies: '@eslint/core': 0.17.0 optionalDependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) '@eslint/config-array@0.23.3': dependencies: @@ -4658,7 +4634,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.4': + '@eslint/eslintrc@3.3.5': dependencies: ajv: 6.14.0 debug: 4.4.3 @@ -4672,9 +4648,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.39.3': {} + '@eslint/js@9.39.4': {} - '@eslint/json@1.1.0': + '@eslint/json@1.2.0': dependencies: '@eslint/core': 1.1.1 '@eslint/plugin-kit': 0.6.1 @@ -4720,7 +4696,7 @@ snapshots: dependencies: '@antfu/install-pkg': 1.1.0 '@iconify/types': 2.0.0 - mlly: 1.8.1 + mlly: 1.8.2 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -4743,7 +4719,7 @@ snapshots: '@keyv/bigmap@1.3.1(keyv@5.6.0)': dependencies: - hashery: 1.5.0 + hashery: 1.5.1 hookified: 1.15.1 keyv: 5.6.0 @@ -4757,20 +4733,27 @@ snapshots: dependencies: '@mcaptcha/core-glue': 0.1.0-alpha-5 - '@mermaid-js/layout-elk@0.2.0(mermaid@11.12.3)': + '@mermaid-js/layout-elk@0.2.1(mermaid@11.13.0)': dependencies: d3: 7.9.0 elkjs: 0.9.3 - mermaid: 11.12.3 + mermaid: 11.13.0 - '@mermaid-js/parser@1.0.0': + '@mermaid-js/parser@1.0.1': dependencies: langium: 4.2.1 '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.8.1 + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 '@tybys/wasm-util': 0.10.1 optional: true @@ -4836,6 +4819,8 @@ snapshots: dependencies: '@nolyfill/shared': 1.0.44 + '@oxc-project/types@0.122.0': {} + '@package-json/types@0.0.12': {} '@pkgr/core@0.2.9': {} @@ -4846,97 +4831,71 @@ snapshots: '@popperjs/core@2.11.8': {} - '@primer/octicons@19.22.0': + '@primer/octicons@19.23.1': dependencies: object-assign: 4.1.1 '@resvg/resvg-wasm@2.6.2': {} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + '@rolldown/pluginutils@1.0.0-rc.2': {} - '@rollup/rollup-android-arm-eabi@4.59.0': - optional: true - - '@rollup/rollup-android-arm64@4.59.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.59.0': - optional: true - - '@rollup/rollup-darwin-x64@4.59.0': - optional: true - - '@rollup/rollup-freebsd-arm64@4.59.0': - optional: true - - '@rollup/rollup-freebsd-x64@4.59.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.59.0': - optional: true - - '@rollup/rollup-openbsd-x64@4.59.0': - optional: true - - '@rollup/rollup-openharmony-arm64@4.59.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.59.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.59.0': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.59.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.59.0': - optional: true - '@rtsao/scc@1.1.0': {} '@scarf/scarf@1.4.0': {} - '@silverwind/vue3-calendar-heatmap@2.1.1(tippy.js@6.3.7)(vue@3.5.29(typescript@5.9.3))': + '@silverwind/vue3-calendar-heatmap@2.1.1(tippy.js@6.3.7)(vue@3.5.31(typescript@5.9.3))': dependencies: tippy.js: 6.3.7 - vue: 3.5.29(typescript@5.9.3) + vue: 3.5.31(typescript@5.9.3) '@simonwep/pickr@1.9.0': dependencies: @@ -4945,32 +4904,32 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@solid-primitives/refs@1.1.3(solid-js@1.9.11)': + '@solid-primitives/refs@1.1.3(solid-js@1.9.12)': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@1.9.11) - solid-js: 1.9.11 + '@solid-primitives/utils': 6.4.0(solid-js@1.9.12) + solid-js: 1.9.12 - '@solid-primitives/transition-group@1.1.2(solid-js@1.9.11)': + '@solid-primitives/transition-group@1.1.2(solid-js@1.9.12)': dependencies: - solid-js: 1.9.11 + solid-js: 1.9.12 - '@solid-primitives/utils@6.4.0(solid-js@1.9.11)': + '@solid-primitives/utils@6.4.0(solid-js@1.9.12)': dependencies: - solid-js: 1.9.11 + solid-js: 1.9.12 '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@5.10.0(eslint@10.0.3(jiti@2.6.1))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/types': 8.56.1 - eslint: 10.0.3(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/types': 8.57.2 + eslint: 10.1.0(jiti@2.6.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 - picomatch: 4.0.3 + picomatch: 4.0.4 - '@stylistic/stylelint-plugin@5.0.1(stylelint@17.4.0(typescript@5.9.3))': + '@stylistic/stylelint-plugin@5.0.1(stylelint@17.6.0(typescript@5.9.3))': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 @@ -4979,7 +4938,7 @@ snapshots: postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 style-search: 0.1.0 - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@5.9.3) '@swc/helpers@0.2.14': {} @@ -4992,7 +4951,7 @@ snapshots: spdx-expression-validate: 2.0.0 spdx-satisfies: 5.0.1 superstruct: 0.10.13 - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) webpack-sources: 1.4.3 wrap-ansi: 6.2.0 @@ -5127,7 +5086,7 @@ snapshots: '@types/d3-transition': 3.0.9 '@types/d3-zoom': 3.0.8 - '@types/debug@4.1.12': + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -5169,7 +5128,7 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@25.3.5': + '@types/node@25.5.0': dependencies: undici-types: 7.18.2 @@ -5196,176 +5155,97 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.56.1 - eslint: 10.0.3(jiti@2.6.1) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.2 + eslint: 10.1.0(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/type-utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 - eslint: 10.0.3(jiti@2.6.1) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.57.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)': + '@typescript-eslint/scope-manager@8.57.2': dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/visitor-keys': 8.57.2 + + '@typescript-eslint/tsconfig-utils@8.57.2(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 + eslint: 10.1.0(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.56.1': + '@typescript-eslint/types@8.57.2': {} + + '@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 - - '@typescript-eslint/scope-manager@8.57.1': - dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 - - '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/type-utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/type-utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.56.1': {} - - '@typescript-eslint/types@8.57.1': {} - - '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/project-service': 8.57.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3 minimatch: 10.2.4 semver: 7.7.4 tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)': + '@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.57.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 - debug: 4.4.3 - minimatch: 10.2.4 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/visitor-keys@8.57.2': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.56.1': - dependencies: - '@typescript-eslint/types': 8.56.1 - eslint-visitor-keys: 5.0.1 - - '@typescript-eslint/visitor-keys@8.57.1': - dependencies: - '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/types': 8.57.2 eslint-visitor-keys: 5.0.1 '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -5427,61 +5307,69 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-vue@6.0.4(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + '@vitejs/plugin-vue@6.0.5(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))(vue@3.5.31(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) - vue: 3.5.29(typescript@5.9.3) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) + vue: 3.5.31(typescript@5.9.3) - '@vitest/eslint-plugin@1.6.12(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': + '@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)))': dependencies: - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) typescript: 5.9.3 - vitest: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vitest: 4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) transitivePeerDependencies: - supports-color - '@vitest/expect@4.0.18': + '@vitest/expect@4.1.2': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': + '@vitest/mocker@4.1.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))': dependencies: - '@vitest/spy': 4.0.18 + '@vitest/spy': 4.1.2 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) - '@vitest/pretty-format@4.0.18': + '@vitest/pretty-format@4.1.2': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/runner@4.0.18': + '@vitest/runner@4.1.2': dependencies: - '@vitest/utils': 4.0.18 + '@vitest/utils': 4.1.2 pathe: 2.0.3 - '@vitest/snapshot@4.0.18': + '@vitest/snapshot@4.1.2': dependencies: - '@vitest/pretty-format': 4.0.18 + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.2': {} - '@vitest/utils@4.0.18': + '@vitest/utils@4.1.2': dependencies: - '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.0.3 + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 '@volar/language-core@2.4.28': dependencies: @@ -5495,69 +5383,69 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue/compiler-core@3.5.29': + '@vue/compiler-core@3.5.31': dependencies: - '@babel/parser': 7.29.0 - '@vue/shared': 3.5.29 + '@babel/parser': 7.29.2 + '@vue/shared': 3.5.31 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.29': + '@vue/compiler-dom@3.5.31': dependencies: - '@vue/compiler-core': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/compiler-core': 3.5.31 + '@vue/shared': 3.5.31 - '@vue/compiler-sfc@3.5.29': + '@vue/compiler-sfc@3.5.31': dependencies: - '@babel/parser': 7.29.0 - '@vue/compiler-core': 3.5.29 - '@vue/compiler-dom': 3.5.29 - '@vue/compiler-ssr': 3.5.29 - '@vue/shared': 3.5.29 + '@babel/parser': 7.29.2 + '@vue/compiler-core': 3.5.31 + '@vue/compiler-dom': 3.5.31 + '@vue/compiler-ssr': 3.5.31 + '@vue/shared': 3.5.31 estree-walker: 2.0.2 magic-string: 0.30.21 postcss: 8.5.8 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.29': + '@vue/compiler-ssr@3.5.31': dependencies: - '@vue/compiler-dom': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/compiler-dom': 3.5.31 + '@vue/shared': 3.5.31 - '@vue/language-core@3.2.5': + '@vue/language-core@3.2.6': dependencies: '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/compiler-dom': 3.5.31 + '@vue/shared': 3.5.31 alien-signals: 3.1.2 muggle-string: 0.4.1 path-browserify: 1.0.1 - picomatch: 4.0.3 + picomatch: 4.0.4 - '@vue/reactivity@3.5.29': + '@vue/reactivity@3.5.31': dependencies: - '@vue/shared': 3.5.29 + '@vue/shared': 3.5.31 - '@vue/runtime-core@3.5.29': + '@vue/runtime-core@3.5.31': dependencies: - '@vue/reactivity': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/reactivity': 3.5.31 + '@vue/shared': 3.5.31 - '@vue/runtime-dom@3.5.29': + '@vue/runtime-dom@3.5.31': dependencies: - '@vue/reactivity': 3.5.29 - '@vue/runtime-core': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/reactivity': 3.5.31 + '@vue/runtime-core': 3.5.31 + '@vue/shared': 3.5.31 csstype: 3.2.3 - '@vue/server-renderer@3.5.29(vue@3.5.29(typescript@5.9.3))': + '@vue/server-renderer@3.5.31(vue@3.5.31(typescript@5.9.3))': dependencies: - '@vue/compiler-ssr': 3.5.29 - '@vue/shared': 3.5.29 - vue: 3.5.29(typescript@5.9.3) + '@vue/compiler-ssr': 3.5.31 + '@vue/shared': 3.5.31 + vue: 3.5.31(typescript@5.9.3) - '@vue/shared@3.5.29': {} + '@vue/shared@3.5.31': {} '@webassemblyjs/ast@1.14.1': dependencies: @@ -5635,21 +5523,6 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': - dependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) - - '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': - dependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) - - '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': - dependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) - '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -5666,7 +5539,7 @@ snapshots: add-asset-webpack-plugin@3.1.1(webpack@5.105.4): optionalDependencies: - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: @@ -5710,7 +5583,7 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.2 arg@5.0.2: {} @@ -5722,9 +5595,9 @@ snapshots: asciinema-player@3.15.1: dependencies: - '@babel/runtime': 7.28.6 - solid-js: 1.9.11 - solid-transition-group: 0.2.3(solid-js@1.9.11) + '@babel/runtime': 7.29.2 + solid-js: 1.9.12 + solid-transition-group: 0.2.3(solid-js@1.9.12) assertion-error@2.0.1: {} @@ -5742,7 +5615,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.0: {} + baseline-browser-mapping@2.10.11: {} big.js@5.2.2: {} @@ -5755,7 +5628,7 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.4: + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -5765,9 +5638,9 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.10.0 - caniuse-lite: 1.0.30001777 - electron-to-chromium: 1.5.307 + baseline-browser-mapping: 2.10.11 + caniuse-lite: 1.0.30001781 + electron-to-chromium: 1.5.325 node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -5784,19 +5657,19 @@ snapshots: bytes@3.1.2: {} - cacheable@2.3.3: + cacheable@2.3.4: dependencies: '@cacheable/memory': 2.0.8 '@cacheable/utils': 2.4.0 hookified: 1.15.1 keyv: 5.6.0 - qified: 0.6.0 + qified: 0.9.0 callsites@3.1.0: {} camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001777: {} + caniuse-lite@1.0.30001781: {} chai@6.2.2: {} @@ -5817,10 +5690,10 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 - chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.19): + chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.20): dependencies: chart.js: 4.5.1 - dayjs: 1.11.19 + dayjs: 1.11.20 chartjs-plugin-zoom@2.2.0(chart.js@4.5.1): dependencies: @@ -5888,12 +5761,8 @@ snapshots: colord@2.9.3: {} - colorette@2.0.20: {} - commander@11.1.0: {} - commander@12.1.0: {} - commander@14.0.3: {} commander@2.20.3: {} @@ -5912,7 +5781,9 @@ snapshots: confbox@0.1.8: {} - core-js-compat@3.48.0: + convert-source-map@2.0.0: {} + + core-js-compat@3.49.0: dependencies: browserslist: 4.28.1 @@ -5956,7 +5827,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) css-select@5.2.2: dependencies: @@ -6028,7 +5899,7 @@ snapshots: d3-delaunay@6.0.4: dependencies: - delaunator: 5.0.1 + delaunator: 5.1.0 d3-dispatch@3.0.1: {} @@ -6165,14 +6036,14 @@ snapshots: d3-transition: 3.0.1(d3-selection@3.0.0) d3-zoom: 3.0.0 - dagre-d3-es@7.0.13: + dagre-d3-es@7.0.14: dependencies: d3: 7.9.0 lodash-es: 4.17.23 damerau-levenshtein@1.0.8: {} - dayjs@1.11.19: {} + dayjs@1.11.20: {} debug@3.2.7: dependencies: @@ -6195,12 +6066,14 @@ snapshots: kind-of: 3.2.2 rename-keys: 1.2.0 - delaunator@5.0.1: + delaunator@5.1.0: dependencies: - robust-predicates: 3.0.2 + robust-predicates: 3.0.3 dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -6231,7 +6104,7 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 - dompurify@3.3.2: + dompurify@3.3.3: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -6254,7 +6127,7 @@ snapshots: codemirror-spell-checker: 1.1.2 marked: 4.3.0 - electron-to-chromium@1.5.307: {} + electron-to-chromium@1.5.325: {} elkjs@0.9.3: {} @@ -6264,10 +6137,10 @@ snapshots: emojis-list@3.0.0: {} - enhanced-resolve@5.20.0: + enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.0 + tapable: 2.3.2 entities@4.5.0: {} @@ -6281,46 +6154,44 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-module-lexer@1.7.0: {} - es-module-lexer@2.0.0: {} esbuild-loader@4.4.2(webpack@5.105.4): dependencies: - esbuild: 0.27.3 - get-tsconfig: 4.13.6 + esbuild: 0.27.4 + get-tsconfig: 4.13.7 loader-utils: 2.0.4 - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) webpack-sources: 1.4.3 - esbuild@0.27.3: + esbuild@0.27.4: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 escalade@3.2.0: {} @@ -6328,13 +6199,13 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: - get-tsconfig: 4.13.6 + get-tsconfig: 4.13.7 stable-hash-x: 0.2.0 optionalDependencies: unrs-resolver: 1.11.1 @@ -6347,103 +6218,103 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.0.3(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.1.0(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) - get-tsconfig: 4.13.6 + get-tsconfig: 4.13.7 is-bun-module: 2.0.0 stable-hash-x: 0.2.0 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.0.3(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.1.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-array-func@5.1.1(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-array-func@5.1.1(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-de-morgan@2.1.1(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-de-morgan@2.1.1(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-escompat@3.11.4(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-escompat@3.11.4(eslint@10.1.0(jiti@2.6.1)): dependencies: browserslist: 4.28.1 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-eslint-comments@3.2.0(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-eslint-comments@3.2.0(eslint@10.1.0(jiti@2.6.1)): dependencies: escape-string-regexp: 1.0.5 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) ignore: 5.3.2 - eslint-plugin-filenames@1.3.2(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-filenames@1.3.2(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) lodash.camelcase: 4.3.0 lodash.kebabcase: 4.1.1 lodash.snakecase: 4.1.1 lodash.upperfirst: 4.3.1 - eslint-plugin-github@6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-github@6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)): dependencies: - '@eslint/compat': 1.4.1(eslint@10.0.3(jiti@2.6.1)) - '@eslint/eslintrc': 3.3.4 - '@eslint/js': 9.39.3 + '@eslint/compat': 1.4.1(eslint@10.1.0(jiti@2.6.1)) + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 '@github/browserslist-config': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) aria-query: 5.3.2 - eslint: 10.0.3(jiti@2.6.1) - eslint-config-prettier: 10.1.8(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-escompat: 3.11.4(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-eslint-comments: 3.2.0(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-filenames: 1.3.2(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-i18n-text: 1.0.1(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@10.0.3(jiti@2.6.1)) + eslint: 10.1.0(jiti@2.6.1) + eslint-config-prettier: 10.1.8(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-escompat: 3.11.4(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-eslint-comments: 3.2.0(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-filenames: 1.3.2(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-i18n-text: 1.0.1(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)))(eslint@10.0.3(jiti@2.6.1))(prettier@3.8.1) + eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1))(prettier@3.8.1) eslint-rule-documentation: 1.0.23 globals: 16.5.0 jsx-ast-utils: 3.3.5 prettier: 3.8.1 svg-element-attributes: 1.3.1 typescript: 5.9.3 - typescript-eslint: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + typescript-eslint: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - '@types/eslint' - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-i18n-text@1.0.1(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-i18n-text@1.0.1(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)): dependencies: '@package-json/types': 0.0.12 - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/types': 8.57.2 comment-parser: 1.4.5 debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.2.4 @@ -6451,12 +6322,12 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: '@nolyfill/array-includes@1.0.44' @@ -6465,9 +6336,9 @@ snapshots: array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' debug: 3.2.7 doctrine: 2.1.0 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) hasown: '@nolyfill/hasown@1.0.44' is-core-module: '@nolyfill/is-core-module@1.0.39' is-glob: 4.0.3 @@ -6479,13 +6350,13 @@ snapshots: string.prototype.trimend: '@nolyfill/string.prototype.trimend@1.0.44' tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@10.1.0(jiti@2.6.1)): dependencies: aria-query: 5.3.2 array-includes: '@nolyfill/array-includes@1.0.44' @@ -6495,7 +6366,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) hasown: '@nolyfill/hasown@1.0.44' jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -6506,38 +6377,38 @@ snapshots: eslint-plugin-no-only-tests@3.3.0: {} - eslint-plugin-playwright@2.10.1(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-playwright@2.10.1(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) globals: 17.4.0 - eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)))(eslint@10.0.3(jiti@2.6.1))(prettier@3.8.1): + eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1))(prettier@3.8.1): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) prettier: 3.8.1 prettier-linter-helpers: 1.0.1 synckit: 0.11.12 optionalDependencies: '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@10.0.3(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-regexp@3.1.0(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-regexp@3.1.0(eslint@10.1.0(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 comment-parser: 1.4.5 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) jsdoc-type-pratt-parser: 7.1.1 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-sonarjs@4.0.2(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-sonarjs@4.0.2(eslint@10.1.0(jiti@2.6.1)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) functional-red-black-tree: 1.0.1 globals: 17.4.0 jsx-ast-utils-x: 0.1.0 @@ -6545,18 +6416,18 @@ snapshots: minimatch: 10.2.4 scslre: 0.3.0 semver: 7.7.4 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 - eslint-plugin-unicorn@63.0.0(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-unicorn@63.0.0(eslint@10.1.0(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) change-case: 5.4.4 ci-info: 4.4.0 clean-regexp: 1.0.0 - core-js-compat: 3.48.0 - eslint: 10.0.3(jiti@2.6.1) + core-js-compat: 3.49.0 + eslint: 10.1.0(jiti@2.6.1) find-up-simple: 1.0.1 globals: 16.5.0 indent-string: 5.0.0 @@ -6568,33 +6439,33 @@ snapshots: semver: 7.7.4 strip-indent: 4.1.1 - eslint-plugin-vue-scoped-css@3.0.0(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))): + eslint-plugin-vue-scoped-css@3.0.0(eslint@10.1.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - eslint: 10.0.3(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + eslint: 10.1.0(jiti@2.6.1) lodash: 4.17.23 postcss: 8.5.8 postcss-safe-parser: 7.0.1(postcss@8.5.8) postcss-selector-parser: 7.1.1 - vue-eslint-parser: 10.4.0(eslint@10.0.3(jiti@2.6.1)) + vue-eslint-parser: 10.4.0(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.0.3(jiti@2.6.1)))(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))): + eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1)))(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - eslint: 10.0.3(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + eslint: 10.1.0(jiti@2.6.1) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.1 semver: 7.7.4 - vue-eslint-parser: 10.4.0(eslint@10.0.3(jiti@2.6.1)) + vue-eslint-parser: 10.4.0(eslint@10.1.0(jiti@2.6.1)) xml-name-validator: 4.0.0 optionalDependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-wc@3.1.0(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-wc@3.1.0(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) is-valid-element-name: 1.0.0 js-levenshtein-esm: 2.0.0 @@ -6605,13 +6476,6 @@ snapshots: esrecurse: 4.3.0 estraverse: 4.3.0 - eslint-scope@9.1.1: - dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 @@ -6625,9 +6489,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.0.3(jiti@2.6.1): + eslint@10.1.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.3 '@eslint/config-helpers': 0.5.3 @@ -6643,7 +6507,7 @@ snapshots: escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 - espree: 11.1.1 + espree: 11.2.0 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -6668,7 +6532,7 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 - espree@11.1.1: + espree@11.2.0: dependencies: acorn: 8.16.0 acorn-jsx: 5.3.2(acorn@8.16.0) @@ -6724,9 +6588,9 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 fetch-ponyfill@7.1.0: dependencies: @@ -6738,7 +6602,7 @@ snapshots: file-entry-cache@11.1.2: dependencies: - flat-cache: 6.1.20 + flat-cache: 6.1.21 file-entry-cache@8.0.0: dependencies: @@ -6762,18 +6626,18 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.4 + flatted: 3.4.2 keyv: 4.5.4 - flat-cache@6.1.20: + flat-cache@6.1.21: dependencies: - cacheable: 2.3.3 - flatted: 3.3.4 + cacheable: 2.3.4 + flatted: 3.4.2 hookified: 1.15.1 flat@5.0.2: {} - flatted@3.3.4: {} + flatted@3.4.2: {} fs.realpath@1.0.0: {} @@ -6787,7 +6651,7 @@ snapshots: get-east-asian-width@1.5.0: {} - get-tsconfig@4.13.6: + get-tsconfig@4.13.7: dependencies: resolve-pkg-maps: 1.0.0 @@ -6843,14 +6707,14 @@ snapshots: hammerjs@2.0.8: {} - happy-dom@20.8.3: + happy-dom@20.8.8: dependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.19.0 + ws: 8.20.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -6861,12 +6725,14 @@ snapshots: hash-sum@2.0.0: {} - hashery@1.5.0: + hashery@1.5.1: dependencies: hookified: 1.15.1 hookified@1.15.1: {} + hookified@2.1.0: {} + html-tags@5.1.0: {} htmlparser2@8.0.2: @@ -6984,7 +6850,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -7039,7 +6905,7 @@ snapshots: just-extend@5.1.1: {} - katex@0.16.37: + katex@0.16.43: dependencies: commander: 8.3.0 @@ -7082,6 +6948,55 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -7147,7 +7062,7 @@ snapshots: markdownlint: 0.40.0 minimatch: 10.2.4 run-con: 1.3.2 - smol-toml: 1.6.0 + smol-toml: 1.6.1 tinyglobby: 0.2.15 transitivePeerDependencies: - supports-color @@ -7193,21 +7108,22 @@ snapshots: merge2@1.4.1: {} - mermaid@11.12.3: + mermaid@11.13.0: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.0.0 + '@mermaid-js/parser': 1.0.1 '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 cytoscape: 3.33.1 cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) cytoscape-fcose: 2.2.0(cytoscape@3.33.1) d3: 7.9.0 d3-sankey: 0.12.3 - dagre-d3-es: 7.0.13 - dayjs: 1.11.19 - dompurify: 3.3.2 - katex: 0.16.37 + dagre-d3-es: 7.0.14 + dayjs: 1.11.20 + dompurify: 3.3.3 + katex: 0.16.43 khroma: 2.1.0 lodash-es: 4.17.23 marked: 16.4.2 @@ -7275,7 +7191,7 @@ snapshots: dependencies: '@types/katex': 0.16.8 devlop: 1.1.0 - katex: 0.16.37 + katex: 0.16.43 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 @@ -7368,7 +7284,7 @@ snapshots: micromark@4.0.2: dependencies: - '@types/debug': 4.1.12 + '@types/debug': 4.1.13 debug: 4.4.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 @@ -7391,7 +7307,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 mime-db@1.52.0: {} @@ -7399,15 +7315,15 @@ snapshots: dependencies: mime-db: 1.52.0 - mini-css-extract-plugin@2.10.0(webpack@5.105.4): + mini-css-extract-plugin@2.10.2(webpack@5.105.4): dependencies: schema-utils: 4.3.3 - tapable: 2.3.0 - webpack: 5.105.4(webpack-cli@6.0.1) + tapable: 2.3.2 + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) minimatch@10.2.4: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 5.0.5 minimatch@3.1.5: dependencies: @@ -7415,7 +7331,7 @@ snapshots: minimist@1.2.8: {} - mlly@1.8.1: + mlly@1.8.2: dependencies: acorn: 8.16.0 pathe: 2.0.3 @@ -7426,7 +7342,7 @@ snapshots: dependencies: loader-utils: 2.0.4 monaco-editor: 0.55.1 - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) monaco-editor@0.55.1: dependencies: @@ -7559,9 +7475,9 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} - picomatch@4.0.3: {} + picomatch@4.0.4: {} pify@2.3.0: {} @@ -7574,7 +7490,7 @@ snapshots: pkg-types@1.3.1: dependencies: confbox: 0.1.8 - mlly: 1.8.1 + mlly: 1.8.2 pathe: 2.0.3 playwright-core@1.58.2: {} @@ -7613,11 +7529,11 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.8 - postcss-load-config@4.0.2(postcss@8.5.8): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8): dependencies: lilconfig: 3.1.3 - yaml: 2.8.2 optionalDependencies: + jiti: 1.21.7 postcss: 8.5.8 postcss-loader@8.2.1(postcss@8.5.8)(typescript@5.9.3)(webpack@5.105.4): @@ -7627,7 +7543,7 @@ snapshots: postcss: 8.5.8 semver: 7.7.4 optionalDependencies: - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) transitivePeerDependencies: - typescript @@ -7695,9 +7611,9 @@ snapshots: punycode@2.3.1: {} - qified@0.6.0: + qified@0.9.0: dependencies: - hookified: 1.15.1 + hookified: 2.1.0 queue-microtask@1.2.3: {} @@ -7707,7 +7623,7 @@ snapshots: readdirp@3.6.0: dependencies: - picomatch: 2.3.1 + picomatch: 2.3.2 rechoir@0.8.0: dependencies: @@ -7750,38 +7666,28 @@ snapshots: reusify@1.1.0: {} - robust-predicates@3.0.2: {} + robust-predicates@3.0.3: {} - rollup@4.59.0: + rolldown@1.0.0-rc.12: dependencies: - '@types/estree': 1.0.8 + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 - fsevents: 2.3.3 + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 roughjs@4.6.6: dependencies: @@ -7803,7 +7709,7 @@ snapshots: rw@1.3.3: {} - sax@1.5.0: {} + sax@1.6.0: {} schema-utils@4.3.3: dependencies: @@ -7822,11 +7728,11 @@ snapshots: semver@7.7.4: {} - seroval-plugins@1.5.0(seroval@1.5.0): + seroval-plugins@1.5.1(seroval@1.5.1): dependencies: - seroval: 1.5.0 + seroval: 1.5.1 - seroval@1.5.0: {} + seroval@1.5.1: {} shallow-clone@3.0.1: dependencies: @@ -7850,19 +7756,19 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - smol-toml@1.6.0: {} + smol-toml@1.6.1: {} - solid-js@1.9.11: + solid-js@1.9.12: dependencies: csstype: 3.2.3 - seroval: 1.5.0 - seroval-plugins: 1.5.0(seroval@1.5.0) + seroval: 1.5.1 + seroval-plugins: 1.5.1(seroval@1.5.1) - solid-transition-group@0.2.3(solid-js@1.9.11): + solid-transition-group@0.2.3(solid-js@1.9.12): dependencies: - '@solid-primitives/refs': 1.1.3(solid-js@1.9.11) - '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.11) - solid-js: 1.9.11 + '@solid-primitives/refs': 1.1.3(solid-js@1.9.12) + '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.12) + solid-js: 1.9.12 sortablejs@1.15.7: {} @@ -7912,7 +7818,7 @@ snapshots: stackback@0.0.2: {} - std-env@3.10.0: {} + std-env@4.0.0: {} string-width@4.2.3: dependencies: @@ -7946,29 +7852,29 @@ snapshots: style-search@0.1.0: {} - stylelint-config-recommended@18.0.0(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-recommended@18.0.0(stylelint@17.6.0(typescript@5.9.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@5.9.3) - stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.4.0(typescript@5.9.3)): + stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.6.0(typescript@5.9.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@5.9.3) - stylelint-declaration-strict-value@1.11.1(stylelint@17.4.0(typescript@5.9.3)): + stylelint-declaration-strict-value@1.11.1(stylelint@17.6.0(typescript@5.9.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@5.9.3) - stylelint-value-no-unknown-custom-properties@6.1.1(stylelint@17.4.0(typescript@5.9.3)): + stylelint-value-no-unknown-custom-properties@6.1.1(stylelint@17.6.0(typescript@5.9.3)): dependencies: postcss-value-parser: 4.2.0 resolve: 1.22.11 - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@5.9.3) - stylelint@17.4.0(typescript@5.9.3): + stylelint@17.6.0(typescript@5.9.3): dependencies: '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-syntax-patches-for-csstree': 1.1.0 + '@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) @@ -7987,7 +7893,6 @@ snapshots: html-tags: 5.1.0 ignore: 7.0.5 import-meta-resolve: 4.2.0 - imurmurhash: 0.1.4 is-plain-object: 5.0.0 mathml-tag-names: 4.0.0 meow: 14.1.0 @@ -8050,14 +7955,14 @@ snapshots: css-what: 6.2.2 csso: 5.0.5 picocolors: 1.1.1 - sax: 1.5.0 + sax: 1.6.0 svgson@5.3.1: dependencies: deep-rename-keys: 0.2.1 xml-reader: 2.4.3 - swagger-ui-dist@5.32.0: + swagger-ui-dist@5.32.1: dependencies: '@scarf/scarf': 1.4.0 @@ -8080,7 +7985,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tailwindcss@3.4.17: + tailwindcss@3.4.19: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -8099,25 +8004,28 @@ snapshots: postcss: 8.5.8 postcss-import: 15.1.0(postcss@8.5.8) postcss-js: 4.1.0(postcss@8.5.8) - postcss-load-config: 4.0.2(postcss@8.5.8) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8) postcss-nested: 6.2.0(postcss@8.5.8) postcss-selector-parser: 6.1.2 resolve: 1.22.11 sucrase: 3.35.1 transitivePeerDependencies: - - ts-node + - tsx + - yaml - tapable@2.3.0: {} + tapable@2.3.2: {} - terser-webpack-plugin@5.3.17(webpack@5.105.4): + terser-webpack-plugin@5.4.0(esbuild@0.27.4)(webpack@5.105.4): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - terser: 5.46.0 - webpack: 5.105.4(webpack-cli@6.0.1) + terser: 5.46.1 + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) + optionalDependencies: + esbuild: 0.27.4 - terser@5.46.0: + terser@5.46.1: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.16.0 @@ -8138,14 +8046,14 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.0.2: {} + tinyexec@1.0.4: {} tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 - tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} tippy.js@6.3.7: dependencies: @@ -8161,7 +8069,7 @@ snapshots: tributejs@5.1.3: {} - ts-api-utils@2.4.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -8183,13 +8091,13 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -8238,7 +8146,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - updates@17.8.3: {} + updates@17.12.0: {} uri-js@4.4.1: dependencies: @@ -8250,62 +8158,51 @@ snapshots: vanilla-colorful@0.7.2: {} - vite-string-plugin@2.0.1(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): + vite-string-plugin@2.0.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)): dependencies: - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) - vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): + vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1): dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + lightningcss: 1.32.0 + picomatch: 4.0.4 postcss: 8.5.8 - rollup: 4.59.0 + rolldown: 1.0.0-rc.12 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 + esbuild: 0.27.4 fsevents: 2.3.3 jiti: 2.6.1 - terser: 5.46.0 - yaml: 2.8.2 + terser: 5.46.1 - vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): + vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)): dependencies: - '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) - '@vitest/pretty-format': 4.0.18 - '@vitest/runner': 4.0.18 - '@vitest/snapshot': 4.0.18 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - es-module-lexer: 1.7.0 + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 + picomatch: 4.0.4 + std-env: 4.0.0 tinybench: 2.9.0 - tinyexec: 1.0.2 + tinyexec: 1.0.4 tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + tinyrainbow: 3.1.0 + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.3.5 - happy-dom: 20.8.3 + '@types/node': 25.5.0 + happy-dom: 20.8.8 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml vscode-jsonrpc@8.2.0: {} @@ -8326,49 +8223,49 @@ snapshots: vue-bar-graph@2.2.0(typescript@5.9.3): dependencies: - vue: 3.5.29(typescript@5.9.3) + vue: 3.5.31(typescript@5.9.3) transitivePeerDependencies: - typescript - vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.29(typescript@5.9.3)): + vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.31(typescript@5.9.3)): dependencies: chart.js: 4.5.1 - vue: 3.5.29(typescript@5.9.3) + vue: 3.5.31(typescript@5.9.3) - vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1)): + vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) - eslint-scope: 9.1.1 + eslint: 10.1.0(jiti@2.6.1) + eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 - espree: 11.1.1 + espree: 11.2.0 esquery: 1.7.0 semver: 7.7.4 transitivePeerDependencies: - supports-color - vue-loader@17.4.2(vue@3.5.29(typescript@5.9.3))(webpack@5.105.4): + vue-loader@17.4.2(vue@3.5.31(typescript@5.9.3))(webpack@5.105.4): dependencies: chalk: 4.1.2 hash-sum: 2.0.0 watchpack: 2.5.1 - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) optionalDependencies: - vue: 3.5.29(typescript@5.9.3) + vue: 3.5.31(typescript@5.9.3) - vue-tsc@3.2.5(typescript@5.9.3): + vue-tsc@3.2.6(typescript@5.9.3): dependencies: '@volar/typescript': 2.4.28 - '@vue/language-core': 3.2.5 + '@vue/language-core': 3.2.6 typescript: 5.9.3 - vue@3.5.29(typescript@5.9.3): + vue@3.5.31(typescript@5.9.3): dependencies: - '@vue/compiler-dom': 3.5.29 - '@vue/compiler-sfc': 3.5.29 - '@vue/runtime-dom': 3.5.29 - '@vue/server-renderer': 3.5.29(vue@3.5.29(typescript@5.9.3)) - '@vue/shared': 3.5.29 + '@vue/compiler-dom': 3.5.31 + '@vue/compiler-sfc': 3.5.31 + '@vue/runtime-dom': 3.5.31 + '@vue/server-renderer': 3.5.31(vue@3.5.31(typescript@5.9.3)) + '@vue/shared': 3.5.31 optionalDependencies: typescript: 5.9.3 @@ -8379,21 +8276,17 @@ snapshots: webidl-conversions@3.0.1: {} - webpack-cli@6.0.1(webpack@5.105.4): + webpack-cli@7.0.2(webpack@5.105.4): dependencies: - '@discoveryjs/json-ext': 0.6.3 - '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) - '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) - '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) - colorette: 2.0.20 - commander: 12.1.0 + '@discoveryjs/json-ext': 1.0.0 + commander: 14.0.3 cross-spawn: 7.0.6 envinfo: 7.21.0 fastest-levenshtein: 1.0.16 import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) webpack-merge: 6.0.1 webpack-merge@6.0.1: @@ -8409,7 +8302,7 @@ snapshots: webpack-sources@3.3.4: {} - webpack@5.105.4(webpack-cli@6.0.1): + webpack@5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -8421,7 +8314,7 @@ snapshots: acorn-import-phases: 1.0.4(acorn@8.16.0) browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.20.0 + enhanced-resolve: 5.20.1 es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -8432,12 +8325,12 @@ snapshots: mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 4.3.3 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.17(webpack@5.105.4) + tapable: 2.3.2 + terser-webpack-plugin: 5.4.0(esbuild@0.27.4)(webpack@5.105.4) watchpack: 2.5.1 webpack-sources: 3.3.4 optionalDependencies: - webpack-cli: 6.0.1(webpack@5.105.4) + webpack-cli: 7.0.2(webpack@5.105.4) transitivePeerDependencies: - '@swc/core' - esbuild @@ -8485,7 +8378,7 @@ snapshots: dependencies: signal-exit: 4.1.0 - ws@8.19.0: {} + ws@8.20.0: {} xml-lexer@0.2.2: dependencies: @@ -8498,6 +8391,4 @@ snapshots: eventemitter3: 2.0.3 xml-lexer: 0.2.2 - yaml@2.8.2: {} - yocto-queue@0.1.0: {} diff --git a/public/assets/img/svg/octicon-lockup-github.svg b/public/assets/img/svg/octicon-lockup-github.svg new file mode 100644 index 00000000000..746317496c4 --- /dev/null +++ b/public/assets/img/svg/octicon-lockup-github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-logo-github.svg b/public/assets/img/svg/octicon-logo-github.svg index 8aae451ae5d..cd09f6ac143 100644 --- a/public/assets/img/svg/octicon-logo-github.svg +++ b/public/assets/img/svg/octicon-logo-github.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-mark-github.svg b/public/assets/img/svg/octicon-mark-github.svg index 6d6dc408862..a46d882513b 100644 --- a/public/assets/img/svg/octicon-mark-github.svg +++ b/public/assets/img/svg/octicon-mark-github.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/updates.config.ts b/updates.config.ts index a7eb364b444..afe0f8e04a6 100644 --- a/updates.config.ts +++ b/updates.config.ts @@ -1,9 +1,10 @@ import type {Config} from 'updates'; export default { - exclude: [ - '@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled - 'cropperjs', // need to migrate to v2 but v2 is not compatible with v1 - 'tailwindcss', // need to migrate - ], + pin: { + '@mcaptcha/vanilla-glue': '^0.1', // breaking changes in rc versions need to be handled + 'cropperjs': '^1', // need to migrate to v2 but v2 is not compatible with v1 + 'tailwindcss': '^3', // need to migrate + 'typescript': '^5', // wait on https://github.com/typescript-eslint/typescript-eslint/issues/12123 + }, } satisfies Config; diff --git a/web_src/css/modules/dropdown.css b/web_src/css/modules/dropdown.css index 1c6e7f85523..62ca91e61f2 100644 --- a/web_src/css/modules/dropdown.css +++ b/web_src/css/modules/dropdown.css @@ -274,6 +274,8 @@ select.ui.dropdown { .ui.selection.active.dropdown { border-color: var(--color-primary); box-shadow: 0 6px 18px var(--color-shadow); + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; } .ui.selection.active.dropdown .menu { @@ -311,11 +313,6 @@ select.ui.dropdown { z-index: 3; } -.ui.active.selection.dropdown { - border-bottom-left-radius: 0 !important; - border-bottom-right-radius: 0 !important; -} - .ui.active.empty.selection.dropdown { border-radius: 0.28571429rem !important; box-shadow: none !important; diff --git a/web_src/js/features/repo-projects.ts b/web_src/js/features/repo-projects.ts index 1b1b4e2d248..2432d1b0353 100644 --- a/web_src/js/features/repo-projects.ts +++ b/web_src/js/features/repo-projects.ts @@ -129,11 +129,11 @@ function initRepoProjectColumnEdit(writableProjectBoard: Element): void { const textColor = contrastColor(elColumnColor.value); elBoardColumn.style.setProperty('background', elColumnColor.value, 'important'); elBoardColumn.style.setProperty('color', textColor, 'important'); - queryElemChildren(elBoardColumn, '.divider', (divider) => divider.style.color = textColor); + queryElemChildren(elBoardColumn, '.divider', (divider: HTMLElement) => divider.style.color = textColor); } else { elBoardColumn.style.removeProperty('background'); elBoardColumn.style.removeProperty('color'); - queryElemChildren(elBoardColumn, '.divider', (divider) => divider.style.removeProperty('color')); + queryElemChildren(elBoardColumn, '.divider', (divider: HTMLElement) => divider.style.removeProperty('color')); } fomanticQuery(elModal).modal('hide'); diff --git a/web_src/js/utils.test.ts b/web_src/js/utils.test.ts index edfc7631480..f041a2cecad 100644 --- a/web_src/js/utils.test.ts +++ b/web_src/js/utils.test.ts @@ -114,7 +114,7 @@ test('toAbsoluteUrl', () => { expect(toAbsoluteUrl('')).toEqual('http://localhost:3000'); expect(toAbsoluteUrl('/user/repo')).toEqual('http://localhost:3000/user/repo'); - expect(() => toAbsoluteUrl('path')).toThrowError('unsupported'); + expect(() => toAbsoluteUrl('path')).toThrow('unsupported'); }); test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => { diff --git a/web_src/js/utils/dom.test.ts b/web_src/js/utils/dom.test.ts index 61361e0168a..3edbe94ce4a 100644 --- a/web_src/js/utils/dom.test.ts +++ b/web_src/js/utils/dom.test.ts @@ -34,7 +34,7 @@ test('querySingleVisibleElem', () => { el = createElementFromHTML('
foobar
'); expect(querySingleVisibleElem(el, 'span')!.textContent).toEqual('bar'); el = createElementFromHTML('
foobar
'); - expect(() => querySingleVisibleElem(el, 'span')).toThrowError('Expected exactly one visible element'); + expect(() => querySingleVisibleElem(el, 'span')).toThrow('Expected exactly one visible element'); }); test('queryElemChildren', () => { From de478c4b6f14a5cd745dc8234f55be859b444de1 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 11:49:11 +0100 Subject: [PATCH 16/16] Add e2e tests for server push events (#36879) Add e2e tests for the three server push features: - **Notification count**: verifies badge appears when another user creates an issue - **Stopwatch**: verifies stopwatch element is rendered when a stopwatch is active - **Logout propagation**: verifies logout in one tab triggers redirect in another Tests are transport-agnostic in preparation for a future WebSocket migration. --------- Co-authored-by: Claude (Opus 4.6) Co-authored-by: wxiaoguang --- Makefile | 2 +- tests/e2e/events.test.ts | 83 +++++++++++++++++++++++++++++ tests/e2e/register.test.ts | 7 +-- tests/e2e/utils.ts | 62 ++++++++++++++++++--- tools/test-e2e.sh | 3 ++ web_src/js/features/notification.ts | 57 +++----------------- web_src/js/features/stopwatch.ts | 56 +++---------------- web_src/js/modules/worker.ts | 68 ++++++++++++++++++++--- 8 files changed, 222 insertions(+), 116 deletions(-) create mode 100644 tests/e2e/events.test.ts diff --git a/Makefile b/Makefile index a55493ab809..5ca1c0eda6d 100644 --- a/Makefile +++ b/Makefile @@ -672,7 +672,7 @@ ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),) endif CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ -$(EXECUTABLE_E2E): $(GO_SOURCES) +$(EXECUTABLE_E2E): $(GO_SOURCES) $(WEBPACK_DEST) CGO_ENABLED=1 $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TEST_TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ .PHONY: release diff --git a/tests/e2e/events.test.ts b/tests/e2e/events.test.ts new file mode 100644 index 00000000000..61f1a3c8817 --- /dev/null +++ b/tests/e2e/events.test.ts @@ -0,0 +1,83 @@ +import {test, expect} from '@playwright/test'; +import {loginUser, baseUrl, apiUserHeaders, apiCreateUser, apiDeleteUser, apiCreateRepo, apiCreateIssue, apiStartStopwatch} from './utils.ts'; + +// These tests rely on a short EVENT_SOURCE_UPDATE_TIME in the e2e server config. +test.describe('events', () => { + test('notification count', async ({page, request}) => { + const id = `ev-notif-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const owner = `${id}-owner`; + const commenter = `${id}-commenter`; + const repoName = id; + + await Promise.all([apiCreateUser(request, owner), apiCreateUser(request, commenter)]); + + // Create repo and login in parallel — repo is needed for the issue, login for the event stream + await Promise.all([ + apiCreateRepo(request, {name: repoName, headers: apiUserHeaders(owner)}), + loginUser(page, owner), + ]); + const badge = page.locator('a.not-mobile .notification_count'); + await expect(badge).toBeHidden(); + + // Create issue as another user — this generates a notification delivered via server push + await apiCreateIssue(request, owner, repoName, {title: 'events notification test', headers: apiUserHeaders(commenter)}); + + // Wait for the notification badge to appear via server event + await expect(badge).toBeVisible({timeout: 15000}); + + // Cleanup + await Promise.all([apiDeleteUser(request, commenter), apiDeleteUser(request, owner)]); + }); + + test('stopwatch', async ({page, request}) => { + const name = `ev-sw-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const headers = apiUserHeaders(name); + + await apiCreateUser(request, name); + + // Create repo, issue, and start stopwatch before login + await apiCreateRepo(request, {name, headers}); + await apiCreateIssue(request, name, name, {title: 'events stopwatch test', headers}); + await apiStartStopwatch(request, name, name, 1, {headers}); + + // Login — page renders with the active stopwatch element + await loginUser(page, name); + + // Verify stopwatch is visible and links to the correct issue + const stopwatch = page.locator('.active-stopwatch.not-mobile'); + await expect(stopwatch).toBeVisible(); + + // Cleanup + await apiDeleteUser(request, name); + }); + + test('logout propagation', async ({browser, request}) => { + const name = `ev-logout-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + + await apiCreateUser(request, name); + + // Use a single context so both pages share the same session and SharedWorker + const context = await browser.newContext({baseURL: baseUrl()}); + const page1 = await context.newPage(); + const page2 = await context.newPage(); + + await loginUser(page1, name); + + // Navigate page2 so it connects to the shared event stream + await page2.goto('/'); + + // Verify page2 is logged in + await expect(page2.getByRole('link', {name: 'Sign In'})).toBeHidden(); + + // Logout from page1 — this sends a logout event to all tabs + await page1.goto('/user/logout'); + + // page2 should be redirected via the logout event + await expect(page2.getByRole('link', {name: 'Sign In'})).toBeVisible(); + + await context.close(); + + // Cleanup + await apiDeleteUser(request, name); + }); +}); diff --git a/tests/e2e/register.test.ts b/tests/e2e/register.test.ts index 425fc7e40c2..5c70541747f 100644 --- a/tests/e2e/register.test.ts +++ b/tests/e2e/register.test.ts @@ -1,6 +1,6 @@ import {env} from 'node:process'; import {test, expect} from '@playwright/test'; -import {login, logout} from './utils.ts'; +import {login, logout, apiDeleteUser} from './utils.ts'; test.beforeEach(async ({page}) => { await page.goto('/user/sign_up'); @@ -50,10 +50,7 @@ test('register then login', async ({page}) => { await login(page, username, password); // delete via API because of issues related to form-fetch-action - const response = await page.request.delete(`/api/v1/admin/users/${username}?purge=true`, { - headers: {Authorization: `Basic ${btoa(`${env.GITEA_TEST_E2E_USER}:${env.GITEA_TEST_E2E_PASSWORD}`)}`}, - }); - expect(response.ok()).toBeTruthy(); + await apiDeleteUser(page.request, username); }); test('register with existing username shows error', async ({page}) => { diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts index 6ee16b32f86..aded8586002 100644 --- a/tests/e2e/utils.ts +++ b/tests/e2e/utils.ts @@ -1,13 +1,18 @@ +import {randomBytes} from 'node:crypto'; import {env} from 'node:process'; import {expect} from '@playwright/test'; import type {APIRequestContext, Locator, Page} from '@playwright/test'; -export function apiBaseUrl() { +export function baseUrl() { return env.GITEA_TEST_E2E_URL?.replace(/\/$/g, ''); } +function apiAuthHeader(username: string, password: string) { + return {Authorization: `Basic ${globalThis.btoa(`${username}:${password}`)}`}; +} + export function apiHeaders() { - return {Authorization: `Basic ${globalThis.btoa(`${env.GITEA_TEST_E2E_USER}:${env.GITEA_TEST_E2E_PASSWORD}`)}`}; + return apiAuthHeader(env.GITEA_TEST_E2E_USER, env.GITEA_TEST_E2E_PASSWORD); } async function apiRetry(fn: () => Promise<{ok: () => boolean; status: () => number; text: () => Promise}>, label: string) { @@ -24,30 +29,73 @@ async function apiRetry(fn: () => Promise<{ok: () => boolean; status: () => numb } } -export async function apiCreateRepo(requestContext: APIRequestContext, {name, autoInit = true}: {name: string; autoInit?: boolean}) { - await apiRetry(() => requestContext.post(`${apiBaseUrl()}/api/v1/user/repos`, { - headers: apiHeaders(), +export async function apiCreateRepo(requestContext: APIRequestContext, {name, autoInit = true, headers}: {name: string; autoInit?: boolean; headers?: Record}) { + await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/user/repos`, { + headers: headers || apiHeaders(), data: {name, auto_init: autoInit}, }), 'apiCreateRepo'); } +export async function apiCreateIssue(requestContext: APIRequestContext, owner: string, repo: string, {title, headers}: {title: string; headers?: Record}) { + await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues`, { + headers: headers || apiHeaders(), + data: {title}, + }), 'apiCreateIssue'); +} + +export async function apiStartStopwatch(requestContext: APIRequestContext, owner: string, repo: string, issueIndex: number, {headers}: {headers?: Record} = {}) { + await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues/${issueIndex}/stopwatch/start`, { + headers: headers || apiHeaders(), + }), 'apiStartStopwatch'); +} + export async function apiDeleteRepo(requestContext: APIRequestContext, owner: string, name: string) { - await apiRetry(() => requestContext.delete(`${apiBaseUrl()}/api/v1/repos/${owner}/${name}`, { + await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/repos/${owner}/${name}`, { headers: apiHeaders(), }), 'apiDeleteRepo'); } export async function apiDeleteOrg(requestContext: APIRequestContext, name: string) { - await apiRetry(() => requestContext.delete(`${apiBaseUrl()}/api/v1/orgs/${name}`, { + await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/orgs/${name}`, { headers: apiHeaders(), }), 'apiDeleteOrg'); } +/** Generate a random password that satisfies the complexity requirements. */ +function generatePassword() { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + return `${Array.from(randomBytes(12), (b) => chars[b % chars.length]).join('')}!aA1`; +} + +/** Random password shared by all test users — used for both API user creation and browser login. */ +const testUserPassword = generatePassword(); + +export function apiUserHeaders(username: string) { + return apiAuthHeader(username, testUserPassword); +} + +export async function apiCreateUser(requestContext: APIRequestContext, username: string) { + await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/admin/users`, { + headers: apiHeaders(), + data: {username, password: testUserPassword, email: `${username}@${env.GITEA_TEST_E2E_DOMAIN}`, must_change_password: false}, + }), 'apiCreateUser'); +} + +export async function apiDeleteUser(requestContext: APIRequestContext, username: string) { + await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/admin/users/${username}?purge=true`, { + headers: apiHeaders(), + }), 'apiDeleteUser'); +} + export async function clickDropdownItem(page: Page, trigger: Locator, itemText: string) { await trigger.click(); await page.getByText(itemText).click(); } +export async function loginUser(page: Page, username: string) { + return login(page, username, testUserPassword); +} + export async function login(page: Page, username = env.GITEA_TEST_E2E_USER, password = env.GITEA_TEST_E2E_PASSWORD) { await page.goto('/user/login'); await page.getByLabel('Username or Email Address').fill(username); diff --git a/tools/test-e2e.sh b/tools/test-e2e.sh index d8608a85bbb..1ee513c1093 100755 --- a/tools/test-e2e.sh +++ b/tools/test-e2e.sh @@ -34,6 +34,9 @@ INSTALL_LOCK = true [service] ENABLE_CAPTCHA = false +[ui.notification] +EVENT_SOURCE_UPDATE_TIME = 500ms + [log] MODE = console LEVEL = Warn diff --git a/web_src/js/features/notification.ts b/web_src/js/features/notification.ts index 915f65f88d8..acb1b68f28a 100644 --- a/web_src/js/features/notification.ts +++ b/web_src/js/features/notification.ts @@ -1,8 +1,8 @@ import {GET} from '../modules/fetch.ts'; import {toggleElem, createElementFromHTML} from '../utils/dom.ts'; -import {logoutFromWorker} from '../modules/worker.ts'; +import {UserEventsSharedWorker} from '../modules/worker.ts'; -const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config; +const {appSubUrl, notificationSettings} = window.config; let notificationSequenceNumber = 0; async function receiveUpdateCount(event: MessageEvent<{type: string, data: string}>) { @@ -33,56 +33,15 @@ export function initNotificationCount() { if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) { // Try to connect to the event source via the shared worker first - const worker = new SharedWorker(`${window.__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker'); - worker.addEventListener('error', (event) => { - console.error('worker error', event); - }); - worker.port.addEventListener('messageerror', () => { - console.error('unable to deserialize message'); - }); - worker.port.postMessage({ - type: 'start', - url: `${window.location.origin}${appSubUrl}/user/events`, - }); - worker.port.addEventListener('message', (event: MessageEvent<{type: string, data: string}>) => { - if (!event.data || !event.data.type) { - console.error('unknown worker message event', event); - return; - } - if (event.data.type === 'notification-count') { - receiveUpdateCount(event); // no await - } else if (event.data.type === 'no-event-source') { - // browser doesn't support EventSource, falling back to periodic poller + const worker = new UserEventsSharedWorker('notification-worker'); + worker.addMessageEventListener((event: MessageEvent) => { + if (event.data.type === 'no-event-source') { if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout); - } else if (event.data.type === 'error') { - console.error('worker port event error', event.data); - } else if (event.data.type === 'logout') { - if (event.data.data !== 'here') { - return; - } - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); - logoutFromWorker(); - } else if (event.data.type === 'close') { - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); + } else if (event.data.type === 'notification-count') { + receiveUpdateCount(event); // no await } }); - worker.port.addEventListener('error', (e) => { - console.error('worker port error', e); - }); - worker.port.start(); - window.addEventListener('beforeunload', () => { - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); - }); - + worker.startPort(); return; } diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts index 34e985332b3..6fa8fbbdf36 100644 --- a/web_src/js/features/stopwatch.ts +++ b/web_src/js/features/stopwatch.ts @@ -1,9 +1,9 @@ import {createTippy} from '../modules/tippy.ts'; import {GET} from '../modules/fetch.ts'; import {hideElem, queryElems, showElem} from '../utils/dom.ts'; -import {logoutFromWorker} from '../modules/worker.ts'; +import {UserEventsSharedWorker} from '../modules/worker.ts'; -const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config; +const {appSubUrl, notificationSettings, enableTimeTracking} = window.config; export function initStopwatch() { if (!enableTimeTracking) { @@ -47,56 +47,16 @@ export function initStopwatch() { // if the browser supports EventSource and SharedWorker, use it instead of the periodic poller if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) { // Try to connect to the event source via the shared worker first - const worker = new SharedWorker(`${window.__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker'); - worker.addEventListener('error', (event) => { - console.error('worker error', event); - }); - worker.port.addEventListener('messageerror', () => { - console.error('unable to deserialize message'); - }); - worker.port.postMessage({ - type: 'start', - url: `${window.location.origin}${appSubUrl}/user/events`, - }); - worker.port.addEventListener('message', (event) => { - if (!event.data || !event.data.type) { - console.error('unknown worker message event', event); - return; - } - if (event.data.type === 'stopwatches') { - updateStopwatchData(JSON.parse(event.data.data)); - } else if (event.data.type === 'no-event-source') { + const worker = new UserEventsSharedWorker('stopwatch-worker'); + worker.addMessageEventListener((event) => { + if (event.data.type === 'no-event-source') { // browser doesn't support EventSource, falling back to periodic poller if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout); - } else if (event.data.type === 'error') { - console.error('worker port event error', event.data); - } else if (event.data.type === 'logout') { - if (event.data.data !== 'here') { - return; - } - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); - logoutFromWorker(); - } else if (event.data.type === 'close') { - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); + } else if (event.data.type === 'stopwatches') { + updateStopwatchData(JSON.parse(event.data.data)); } }); - worker.port.addEventListener('error', (e) => { - console.error('worker port error', e); - }); - worker.port.start(); - window.addEventListener('beforeunload', () => { - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); - }); - + worker.startPort(); return; } diff --git a/web_src/js/modules/worker.ts b/web_src/js/modules/worker.ts index af2e52f411e..b730e30bb2e 100644 --- a/web_src/js/modules/worker.ts +++ b/web_src/js/modules/worker.ts @@ -1,9 +1,65 @@ -import {sleep} from '../utils.ts'; +const {appSubUrl, assetVersionEncoded} = window.config; -const {appSubUrl} = window.config; +export class UserEventsSharedWorker { + sharedWorker: SharedWorker; -export async function logoutFromWorker(): Promise { - // wait for a while because other requests (eg: logout) may be in the flight - await sleep(5000); - window.location.href = `${appSubUrl}/`; + // options can be either a string (the debug name of the worker) or an object of type WorkerOptions + constructor(options?: string | WorkerOptions) { + const worker = new SharedWorker(`${window.__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, options); + this.sharedWorker = worker; + worker.addEventListener('error', (event) => { + console.error('worker error', event); + }); + worker.port.addEventListener('messageerror', () => { + console.error('unable to deserialize message'); + }); + worker.port.postMessage({ + type: 'start', + url: `${window.location.origin}${appSubUrl}/user/events`, + }); + worker.port.addEventListener('error', (e) => { + console.error('worker port error', e); + }); + window.addEventListener('beforeunload', () => { + // FIXME: this logic is not quite right. + // "beforeunload" can be canceled by some actions like "are-you-sure" and the navigation can be cancelled. + // In this case: the worker port is incorrectly closed while the page is still there. + worker.port.postMessage({type: 'close'}); + worker.port.close(); + }); + } + + addMessageEventListener(listener: (event: MessageEvent) => void) { + this.sharedWorker.port.addEventListener('message', (event: MessageEvent) => { + if (!event.data || !event.data.type) { + console.error('unknown worker message event', event); + return; + } + + if (event.data.type === 'error') { + console.error('worker port event error', event.data); + } else if (event.data.type === 'logout') { + if (event.data.data !== 'here') return; + this.sharedWorker.port.postMessage({type: 'close'}); + this.sharedWorker.port.close(); + // slightly delay our "logout" for a short while, in case there are other logout requests in-flight. + // * if the logout is triggered by a page redirection (e.g.: user clicks "/user/logout") + // * "beforeunload" event is triggered, this code path won't execute + // * if the logout is triggered by a fetch call + // * "beforeunload" event is not triggered until JS does the redirection. + // * in this case, the logout fetch call already completes and has sent the "logout" message to the worker + // * there can be a data-race between the fetch call's redirection and the "logout" message from the worker + // * the fetch call's logout redirection should always win over the worker message, because it might have a custom location + setTimeout(() => { window.location.href = `${appSubUrl}/` }, 1000); + } else if (event.data.type === 'close') { + this.sharedWorker.port.postMessage({type: 'close'}); + this.sharedWorker.port.close(); + } + listener(event); + }); + } + + startPort() { + this.sharedWorker.port.start(); + } }