diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 7619f46529..2793dd1ca0 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1024,9 +1024,18 @@ LEVEL = Info ;; Default private when using push-to-create ;DEFAULT_PUSH_CREATE_PRIVATE = true ;; -;; Global limit of repositories per user, applied at creation time. -1 means no limit +;; Global limit of repositories per user or org, applied at creation time. -1 means no limit +;; To configure independent limits for users and orgs, use USER_MAX_CREATION_LIMIT and ORG_MAX_CREATION_LIMIT ;MAX_CREATION_LIMIT = -1 ;; +;; Global limit of repositories per user, applied at creation time. -1 means no limit +;; Takes precedence over MAX_CREATION_LIMIT when set +;USER_MAX_CREATION_LIMIT = -1 +;; +;; Global limit of repositories per organization, applied at creation time. -1 means no limit +;; Takes precedence over MAX_CREATION_LIMIT when set +;ORG_MAX_CREATION_LIMIT = -1 +;; ;; Preferred Licenses to place at the top of the List ;; The name here must match the filename in options/license or custom/options/license ;PREFERRED_LICENSES = Apache License 2.0,MIT License diff --git a/go.mod b/go.mod index e13a10c62f..5c3be61c69 100644 --- a/go.mod +++ b/go.mod @@ -113,7 +113,6 @@ require ( google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 gopkg.in/ini.v1 v1.67.2 - gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.50.1 mvdan.cc/xurls/v2 v2.6.0 strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab @@ -282,6 +281,7 @@ require ( golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.72.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/models/unittest/fixtures_loader.go b/models/unittest/fixtures_loader.go index 8f4b9691bb..7ad3bc9058 100644 --- a/models/unittest/fixtures_loader.go +++ b/models/unittest/fixtures_loader.go @@ -16,7 +16,7 @@ import ( "gitea.dev/models/db" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v4" "xorm.io/xorm" "xorm.io/xorm/schemas" ) diff --git a/models/user/user.go b/models/user/user.go index 5c7a7148e5..66e8d49b42 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -244,12 +244,15 @@ func (u *User) IsOAuth2() bool { return u.LoginType == auth.OAuth2 } -// MaxCreationLimit returns the number of repositories a user is allowed to create +// MaxCreationLimit returns the number of repositories a user or an organization is allowed to create func (u *User) MaxCreationLimit() int { - if u.MaxRepoCreation <= -1 { - return setting.Repository.MaxCreationLimit + if u.MaxRepoCreation > -1 { + return u.MaxRepoCreation } - return u.MaxRepoCreation + if u.IsOrganization() { + return setting.Repository.OrgMaxCreationLimit + } + return setting.Repository.UserMaxCreationLimit } // CanCreateRepoIn checks whether the doer(u) can create a repository in the owner @@ -264,13 +267,11 @@ func (u *User) CanCreateRepoIn(owner *User) bool { return true } const noLimit = -1 - if owner.MaxRepoCreation == noLimit { - if setting.Repository.MaxCreationLimit == noLimit { - return true - } - return owner.NumRepos < setting.Repository.MaxCreationLimit + limit := owner.MaxCreationLimit() + if limit == noLimit { + return true } - return owner.NumRepos < owner.MaxRepoCreation + return owner.NumRepos < limit } // CanCreateOrganization returns true if user can create organisation. diff --git a/models/user/user_test.go b/models/user/user_test.go index 47eb00881f..2bf32a5038 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -674,12 +674,18 @@ func TestGetInactiveUsers(t *testing.T) { func TestCanCreateRepo(t *testing.T) { defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)() + defer test.MockVariableValue(&setting.Repository.UserMaxCreationLimit)() + defer test.MockVariableValue(&setting.Repository.OrgMaxCreationLimit)() const noLimit = -1 doerActions := user_model.NewActionsUser() doerNormal := &user_model.User{ID: 2} doerAdmin := &user_model.User{ID: 1, IsAdmin: true} + orgOwner := func(numRepos, maxRepoCreation int) *user_model.User { + return &user_model.User{ID: 3, Type: user_model.UserTypeOrganization, NumRepos: numRepos, MaxRepoCreation: maxRepoCreation} + } t.Run("NoGlobalLimit", func(t *testing.T) { - setting.Repository.MaxCreationLimit = noLimit + setting.Repository.UserMaxCreationLimit = noLimit + setting.Repository.OrgMaxCreationLimit = noLimit assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) @@ -693,7 +699,8 @@ func TestCanCreateRepo(t *testing.T) { }) t.Run("GlobalLimit50", func(t *testing.T) { - setting.Repository.MaxCreationLimit = 50 + setting.Repository.UserMaxCreationLimit = 50 + setting.Repository.OrgMaxCreationLimit = 50 assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit @@ -707,4 +714,33 @@ func TestCanCreateRepo(t *testing.T) { assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100})) }) + + t.Run("UserBlockedOrgsUnlimited", func(t *testing.T) { + // User and org limits are independent: a deployment can block personal repos while leaving orgs unrestricted. + setting.Repository.UserMaxCreationLimit = 0 + setting.Repository.OrgMaxCreationLimit = noLimit + + // regular user is blocked + assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 0, MaxRepoCreation: noLimit})) + // per-user override grants individual exceptions even when the global user limit is 0 + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 3, MaxRepoCreation: 5})) + assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 5, MaxRepoCreation: 5})) + + // organization can create unlimited repos + assert.True(t, doerNormal.CanCreateRepoIn(orgOwner(10, noLimit))) + assert.True(t, doerNormal.CanCreateRepoIn(orgOwner(999, noLimit))) + // per-org override still wins over the global org limit + assert.False(t, doerNormal.CanCreateRepoIn(orgOwner(5, 5))) + }) + + t.Run("OrgGlobalLimitWithPerOrgOverride", func(t *testing.T) { + setting.Repository.UserMaxCreationLimit = noLimit + setting.Repository.OrgMaxCreationLimit = 10 + + assert.True(t, doerNormal.CanCreateRepoIn(orgOwner(5, noLimit))) + assert.False(t, doerNormal.CanCreateRepoIn(orgOwner(10, noLimit))) + + // per-org override bypasses the global org limit + assert.True(t, doerNormal.CanCreateRepoIn(orgOwner(10, 100))) + }) } diff --git a/modules/actions/jobparser/model.go b/modules/actions/jobparser/model.go index 2c4bd1f93a..96450849ab 100644 --- a/modules/actions/jobparser/model.go +++ b/modules/actions/jobparser/model.go @@ -298,6 +298,9 @@ func toGitContext(input map[string]any) *model.GithubContext { return gitContext } +// workflowCallEvent is only fired by another workflow's `uses:`, so it is excluded from trigger detection. +const workflowCallEvent = "workflow_call" + func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) { switch rawOn.Kind { case yaml.ScalarNode: @@ -306,6 +309,9 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) { if err != nil { return nil, err } + if val == workflowCallEvent { + return []*Event{}, nil + } return []*Event{ {Name: val}, }, nil @@ -319,6 +325,9 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) { for _, v := range val { switch t := v.(type) { case string: + if t == workflowCallEvent { + continue + } res = append(res, &Event{Name: t}) default: return nil, fmt.Errorf("invalid type %T", t) @@ -332,6 +341,9 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) { } res := make([]*Event, 0, len(events)) for i, k := range events { + if k == workflowCallEvent { + continue + } v := triggers[i] switch v.Kind { case yaml.ScalarNode: diff --git a/modules/actions/jobparser/model_test.go b/modules/actions/jobparser/model_test.go index bd29cb9425..567c160f92 100644 --- a/modules/actions/jobparser/model_test.go +++ b/modules/actions/jobparser/model_test.go @@ -254,6 +254,53 @@ func TestParseRawOn(t *testing.T) { }, }, }, + { + // `workflow_call` is only fired by another workflow's `uses:`, so ParseRawOn intentionally excludes it from trigger detection. + input: `on: + workflow_call: + inputs: + env: + type: string + required: true + outputs: + sha: + value: ${{ jobs.build.outputs.commit }} + secrets: + DEPLOY_KEY: + required: true +`, + result: []*Event{}, + }, + { + // Mixed: a workflow that is both callable AND triggered by push. Only the "push" event surfaces. + input: `on: + workflow_call: + inputs: + env: + type: string + push: + branches: [main] +`, + result: []*Event{ + { + Name: "push", + acts: map[string][]string{"branches": {"main"}}, + }, + }, + }, + { + // Scalar form: a purely reusable workflow has no event triggers. + input: "on: workflow_call", + result: []*Event{}, + }, + { + // Sequence form: `workflow_call` is excluded while sibling events are kept. + input: "on:\n - push\n - workflow_call\n - pull_request", + result: []*Event{ + {Name: "push"}, + {Name: "pull_request"}, + }, + }, } for _, kase := range kases { t.Run(kase.input, func(t *testing.T) { diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go index e313360b1b..e0d6881cf5 100644 --- a/modules/issue/template/unmarshal.go +++ b/modules/issue/template/unmarshal.go @@ -14,7 +14,7 @@ import ( api "gitea.dev/modules/structs" "gitea.dev/modules/util" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v4" ) // CouldBe indicates a file with the filename could be a template, diff --git a/modules/label/parser.go b/modules/label/parser.go index f3c59f45f3..f56ef0e255 100644 --- a/modules/label/parser.go +++ b/modules/label/parser.go @@ -10,7 +10,7 @@ import ( "gitea.dev/modules/options" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v4" ) type labelFile struct { diff --git a/modules/markup/external/frontend.go b/modules/markup/external/frontend.go index 26e83ec99a..34fa2715f7 100644 --- a/modules/markup/external/frontend.go +++ b/modules/markup/external/frontend.go @@ -90,6 +90,6 @@ func (p *frontendRenderer) Render(ctx *markup.RenderContext, input io.Reader, ou `, p.name, ctx.RenderOptions.RelativePath, contentEncoding, contentString, - public.AssetURI("js/external-render-frontend.js")) + public.AssetURI("web_src/js/external-render-frontend.ts")) return err } diff --git a/modules/markup/html.go b/modules/markup/html.go index a635ce219b..4943bdf4a5 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -175,16 +175,25 @@ var emojiProcessors = []processor{ emojiProcessor, } +// isBareURLSubject reports whether the (HTML-escaped) commit subject content +// is entirely a single URL, ignoring leading/trailing whitespace. +func isBareURLSubject(content string) bool { + s := strings.TrimSpace(html.UnescapeString(content)) + if s == "" { + return false + } + m := common.GlobalVars().LinkRegex.FindStringIndex(s) + return m != nil && m[0] == 0 && m[1] == len(s) +} + // PostProcessCommitMessageSubject will use the same logic as PostProcess and // PostProcessCommitMessage, but will disable the shortLinkProcessor and -// emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set, -// which changes every text node into a link to the passed default link. +// emailAddressProcessor, and wraps the whole subject in defaultLink. func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) { procs := []processor{ fullIssuePatternProcessor, comparePatternProcessor, fullHashPatternProcessor, - linkProcessor, mentionProcessor, issueIndexPatternProcessor, commitCrossReferencePatternProcessor, @@ -192,6 +201,15 @@ func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content st emojiShortCodeProcessor, emojiProcessor, } + // When the whole subject is a bare URL, linkProcessor would turn it into + // a competing anchor and hijack the surrounding defaultLink wrapper, leaving + // the subject visually unclickable. Match GitHub: render such subjects as + // plain text inside defaultLink. Partial URLs inside larger text still become + // their own links (nested anchors aren't legal HTML, so the outer defaultLink + // naturally breaks on that span, same as on GitHub). + if !isBareURLSubject(content) { + procs = append(procs, linkProcessor) + } procs = append(procs, func(ctx *RenderContext, node *html.Node) { ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data} node.Type = html.ElementNode diff --git a/modules/markup/markdown/convertyaml.go b/modules/markup/markdown/convertyaml.go index 61dfbf20df..0c69901929 100644 --- a/modules/markup/markdown/convertyaml.go +++ b/modules/markup/markdown/convertyaml.go @@ -11,7 +11,7 @@ import ( "github.com/yuin/goldmark/ast" east "github.com/yuin/goldmark/extension/ast" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v4" ) func nodeToTable(meta *yaml.Node) ast.Node { diff --git a/modules/markup/markdown/meta.go b/modules/markup/markdown/meta.go index 6ddd892110..ecc0b81098 100644 --- a/modules/markup/markdown/meta.go +++ b/modules/markup/markdown/meta.go @@ -9,7 +9,7 @@ import ( "unicode" "unicode/utf8" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v4" ) func isYAMLSeparator(line []byte) bool { diff --git a/modules/markup/markdown/renderconfig.go b/modules/markup/markdown/renderconfig.go index 36a7b1a9a1..59f3e873ec 100644 --- a/modules/markup/markdown/renderconfig.go +++ b/modules/markup/markdown/renderconfig.go @@ -10,7 +10,7 @@ import ( "gitea.dev/modules/markup" "github.com/yuin/goldmark/ast" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v4" ) // RenderConfig represents rendering configuration for this file diff --git a/modules/markup/markdown/renderconfig_test.go b/modules/markup/markdown/renderconfig_test.go index 53c52177a7..84e4ac33f6 100644 --- a/modules/markup/markdown/renderconfig_test.go +++ b/modules/markup/markdown/renderconfig_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v4" ) func TestRenderConfig_UnmarshalYAML(t *testing.T) { diff --git a/modules/markup/render.go b/modules/markup/render.go index 44bdcd0791..6f43434485 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -243,7 +243,7 @@ func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, return RenderIFrame(ctx, &extOpts, output) } // else: this is a standalone page, fallthrough to the real rendering, and add extra JS/CSS - extraScriptSrc := public.AssetURI("js/external-render-helper.js") + extraScriptSrc := public.AssetURI("web_src/js/external-render-helper.ts") extraLinkHref := ctx.RenderOptions.StandalonePageOptions.CurrentWebTheme.PublicAssetURI() // " -{{ctx.ScriptImport "js/iife.js"}} +{{ctx.ScriptImport "web_src/js/iife.ts"}} diff --git a/templates/base/head_style.tmpl b/templates/base/head_style.tmpl index 4a4fb9d96f..dba782f9bb 100644 --- a/templates/base/head_style.tmpl +++ b/templates/base/head_style.tmpl @@ -1,2 +1,2 @@ - +{{AssetCSSLinks "web_src/js/index.ts" "web_src/css/index.css"}} diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl index c9d7b3047f..ad153364e0 100644 --- a/templates/devtest/devtest-header.tmpl +++ b/templates/devtest/devtest-header.tmpl @@ -1,4 +1,4 @@ {{template "base/head" ctx.RootData}} - +
{{template "base/alert" ctx.RootData}}
diff --git a/templates/swagger/openapi-viewer.tmpl b/templates/swagger/openapi-viewer.tmpl index 792364157d..2e3e9bf149 100644 --- a/templates/swagger/openapi-viewer.tmpl +++ b/templates/swagger/openapi-viewer.tmpl @@ -4,14 +4,14 @@ {{ctx.HeadMetaContentSecurityPolicy}} Gitea API - {{/* HINT: SWAGGER-CSS-IMPORT: import swagger styles ahead to avoid UI flicker (e.g.: the swagger-back-link element) */}} - + {{/* HINT: SWAGGER-CSS-IMPORT: load swagger styles ahead to avoid flicker (e.g. the swagger-back-link) */}} + {{AssetCSSLinks "web_src/js/swagger.ts" "web_src/css/swagger-standalone.css"}} {{/* TODO: add Help & Glossary to help users understand the API, and explain some concepts like "Owner" */}} {{svg "octicon-reply"}}{{ctx.Locale.Tr "return_to_gitea"}}
- {{ctx.ScriptImport "js/swagger.js" "module"}} + {{ctx.ScriptImport "web_src/js/swagger.ts" "module"}} diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 197abe54d0..cd87bc8b1a 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -931,6 +931,76 @@ jobs: }) } +func TestWorkflowDispatchPublicApiRequiresWorkflowDispatchTrigger(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + repo, err := repo_service.CreateRepository(t.Context(), user2, user2, repo_service.CreateRepoOptions{ + Name: "workflow-dispatch-requires-trigger", + Description: "test workflow dispatch requires workflow_dispatch", + AutoInit: true, + Gitignores: "Go", + License: "MIT", + Readme: "Default", + DefaultBranch: "main", + IsPrivate: false, + }) + require.NoError(t, err) + require.NotNil(t, repo) + + addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/push-only.yml", + ContentReader: strings.NewReader(` +on: + push: +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo helloworld +`), + }, + }, + Message: "add workflow", + OldBranch: "main", + NewBranch: "main", + Author: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + require.NoError(t, err) + require.NotNil(t, addWorkflowToBaseResp) + + values := url.Values{} + values.Set("ref", "main") + req := NewRequestWithURLValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/push-only.yml/dispatches", repo.FullName()), values). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusUnprocessableEntity) + apiError := DecodeJSON(t, resp, &api.APIError{}) + assert.Contains(t, apiError.Message, "has no workflow_dispatch event trigger") + + unittest.AssertNotExistsBean(t, &actions_model.ActionRun{ + RepoID: repo.ID, + Event: "workflow_dispatch", + WorkflowID: "push-only.yml", + }) + }) +} + func TestWorkflowDispatchPublicApiWithInputs(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) diff --git a/tests/integration/api_issue_config_test.go b/tests/integration/api_issue_config_test.go index 95ea4af783..4227f33237 100644 --- a/tests/integration/api_issue_config_test.go +++ b/tests/integration/api_issue_config_test.go @@ -16,7 +16,7 @@ import ( "gitea.dev/tests" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v4" ) func createIssueConfig(t *testing.T, user *user_model.User, repo *repo_model.Repository, issueConfig map[string]any) { diff --git a/tests/integration/api_packages_helm_test.go b/tests/integration/api_packages_helm_test.go index 003be3ff14..f66676b81d 100644 --- a/tests/integration/api_packages_helm_test.go +++ b/tests/integration/api_packages_helm_test.go @@ -20,7 +20,7 @@ import ( "gitea.dev/tests" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v4" ) func TestPackageHelm(t *testing.T) { diff --git a/tests/integration/api_packages_npm_test.go b/tests/integration/api_packages_npm_test.go index 5ec018aad8..1159b64da8 100644 --- a/tests/integration/api_packages_npm_test.go +++ b/tests/integration/api_packages_npm_test.go @@ -304,8 +304,8 @@ func TestPackageNpm(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) rendered, _ := doc.Find(".markup.markdown").Html() - assert.Equal(t, `

docs -logo

+ assertHTMLEq(t, `

docs +logo

`, rendered) }) diff --git a/tests/integration/dump_restore_test.go b/tests/integration/dump_restore_test.go index 0cb9001d8a..92e0ed4664 100644 --- a/tests/integration/dump_restore_test.go +++ b/tests/integration/dump_restore_test.go @@ -24,7 +24,7 @@ import ( "gitea.dev/services/migrations" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v4" ) func TestDumpRestore(t *testing.T) { diff --git a/tests/integration/html_helper.go b/tests/integration/html_helper.go index c2045453e6..cefe5592c4 100644 --- a/tests/integration/html_helper.go +++ b/tests/integration/html_helper.go @@ -5,10 +5,13 @@ package integration import ( "io" + "slices" + "strings" "testing" "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" + "golang.org/x/net/html" ) // HTMLDoc struct @@ -47,3 +50,39 @@ func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string assert.Equal(t, v, sel.Length()) } } + +func assertHTMLEq(t testing.TB, expected, actual string) { + t.Helper() + if expected == actual { + return + } + exp, err := html.Parse(strings.NewReader(expected)) + if !assert.NoError(t, err) { + return + } + act, err := html.Parse(strings.NewReader(actual)) + if !assert.NoError(t, err) { + return + } + var normalize func(n *html.Node) + normalize = func(n *html.Node) { + slices.SortFunc(n.Attr, func(a, b html.Attribute) int { + if cmp := strings.Compare(a.Namespace, b.Namespace); cmp != 0 { + return cmp + } + if cmp := strings.Compare(a.Key, b.Key); cmp != 0 { + return cmp + } + return strings.Compare(a.Val, b.Val) + }) + for c := n.FirstChild; c != nil; c = c.NextSibling { + normalize(c) + } + } + normalize(exp) + normalize(act) + var expNormalized, actNormalized strings.Builder + assert.NoError(t, html.Render(&expNormalized, exp)) + assert.NoError(t, html.Render(&actNormalized, act)) + assert.Equal(t, expNormalized.String(), actNormalized.String()) +} diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index 458fa23948..9217fd7e5a 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -109,8 +109,8 @@ func TestExternalMarkupRenderer(t *testing.T) { assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy")) // FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "`+ - ``+ + ``+ + ``+ `
<script></script>
`, respSub.Body.String(), ) @@ -137,8 +137,8 @@ func TestExternalMarkupRenderer(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer?a=1%2f2") respSub := MakeRequest(t, req, http.StatusOK) assert.Equal(t, - ``+ - ``+ + ``+ + ``+ ``, respSub.Body.String(), ) diff --git a/vite.config.ts b/vite.config.ts index 53717813a1..e077fed6ae 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -267,7 +267,6 @@ export default defineConfig(commonViteOpts({ manifest: true, rolldownOptions: { input: { - // FIXME: INCORRECT-VITE-MANIFEST-PARSER: the "css importing" logic in backend is wrong index: join(import.meta.dirname, 'web_src/js/index.ts'), swagger: join(import.meta.dirname, 'web_src/js/swagger.ts'), 'external-render-frontend': join(import.meta.dirname, 'web_src/js/external-render-frontend.ts'), diff --git a/web_src/js/components/ActionRunView.test.ts b/web_src/js/components/ActionRunView.test.ts index 1f972b73c0..b625b15e71 100644 --- a/web_src/js/components/ActionRunView.test.ts +++ b/web_src/js/components/ActionRunView.test.ts @@ -16,6 +16,7 @@ test('LogLineMessage', () => { '::warning file=test.js,line=1::foo': 'Warning: foo', '::notice::foo': 'Notice: foo', '::debug::foo': 'Debug: foo', + '##[command] foo': ' 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 91fe8329bd..a9a6695241 100644 --- a/web_src/js/components/ActionRunView.ts +++ b/web_src/js/components/ActionRunView.ts @@ -20,6 +20,7 @@ const LogLinePrefixCommandMap: Record = { '##[warning]': 'warning', '##[notice]': 'notice', '##[debug]': 'debug', + '##[command]': 'command', '[command]': 'command', // https://github.com/actions/toolkit/blob/master/docs/commands.md diff --git a/web_src/js/swagger.ts b/web_src/js/swagger.ts index ee0fd28936..c73002a6f1 100644 --- a/web_src/js/swagger.ts +++ b/web_src/js/swagger.ts @@ -1,5 +1,3 @@ -// FIXME: INCORRECT-VITE-MANIFEST-PARSER: it just happens to work for current dependencies -// If this module depends on another one and that one imports "swagger.css", then {{AssetURI "css/swagger.css"}} won't work import '../css/swagger-standalone.css'; import {initSwaggerUI} from './render/swagger.ts';