From 9d737a6400d34e4df2212f75109bef1abec7ddaf Mon Sep 17 00:00:00 2001 From: Giteabot Date: Thu, 21 May 2026 23:44:13 -0700 Subject: [PATCH 01/18] fix(deps): update module golang.org/x/crypto to v0.52.0 [security] (#37806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) | [`v0.51.0` → `v0.52.0`](https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.51.0...refs/tags/v0.52.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fcrypto/v0.52.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fcrypto/v0.51.0/v0.52.0?slim=true) | --- ### Invoking key constraints not enforced in golang.org/x/crypto/ssh/agent [CVE-2026-39833](https://nvd.nist.gov/vuln/detail/CVE-2026-39833) / [GO-2026-5005](https://pkg.go.dev/vuln/GO-2026-5005)
More information #### Details The in-memory keyring returned by NewKeyring() silently accepted keys with the ConfirmBeforeUse constraint but never enforced it. The key would sign without any confirmation prompt, with no indication to the caller that the constraint was not in effect. NewKeyring() now returns an error when unsupported constraints are requested. #### Severity Unknown #### References - [https://go.dev/issue/79436](https://go.dev/issue/79436) - [https://go.dev/cl/778640](https://go.dev/cl/778640) - [https://go.dev/cl/778641](https://go.dev/cl/778641) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5005) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Invoking agent constraints dropped when forwarding keys in golang.org/x/crypto/ssh/agent [CVE-2026-39832](https://nvd.nist.gov/vuln/detail/CVE-2026-39832) / [GO-2026-5006](https://pkg.go.dev/vuln/GO-2026-5006)
More information #### Details When adding a key to a remote agent constraint extensions such as restrict-destination-v00@​openssh.com were not serialized in the request. Destination restrictions were silently stripped when forwarding keys, allowing unrestricted use of the key on the remote host. The client now serializes all constraint extensions. Additionally, the in-memory keyring returned by NewKeyring() now rejects keys with unsupported constraint extensions instead of silently ignoring them. #### Severity Unknown #### References - [https://go.dev/issue/79435](https://go.dev/issue/79435) - [https://go.dev/cl/778642](https://go.dev/cl/778642) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5006) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Invoking byte arithmetic causes underflow and panic in golang.org/x/crypto/ssh [CVE-2026-46597](https://nvd.nist.gov/vuln/detail/CVE-2026-46597) / [GO-2026-5013](https://pkg.go.dev/vuln/GO-2026-5013)
More information #### Details An incorrectly placed cast from bytes to int allowed for server-side panic in the AES-GCM packet decoder for well-crafted inputs. #### Severity Unknown #### References - [https://go.dev/issue/79561](https://go.dev/issue/79561) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) - [https://go.dev/cl/781620](https://go.dev/cl/781620) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5013) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Invoking bypass of certificate restrictions in golang.org/x/crypto/ssh [CVE-2026-39828](https://nvd.nist.gov/vuln/detail/CVE-2026-39828) / [GO-2026-5014](https://pkg.go.dev/vuln/GO-2026-5014)
More information #### Details When an SSH server authentication callback returned PartialSuccessError with non-nil Permissions, those permissions were silently discarded, potentially dropping certificate restrictions such as force-command after a second factor succeeded. Returning non-nil Permissions with PartialSuccessError now results in a connection error. #### Severity Unknown #### References - [https://go.dev/issue/79562](https://go.dev/issue/79562) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) - [https://go.dev/cl/781621](https://go.dev/cl/781621) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5014) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Invoking server panic during CheckHostKey/Authenticate in golang.org/x/crypto/ssh [CVE-2026-39835](https://nvd.nist.gov/vuln/detail/CVE-2026-39835) / [GO-2026-5015](https://pkg.go.dev/vuln/GO-2026-5015)
More information #### Details SSH servers which use CertChecker as a public key callback without setting IsUserAuthority or IsHostAuthority could be caused to panic by a client presenting a certificate. CertChecker now returns an error instead of panicking when these callbacks are nil. #### Severity Unknown #### References - [https://go.dev/issue/79563](https://go.dev/issue/79563) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) - [https://go.dev/cl/781660](https://go.dev/cl/781660) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5015) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Invoking memory leak when rejecting channels can lead to DoS in golang.org/x/crypto/ssh [CVE-2026-39827](https://nvd.nist.gov/vuln/detail/CVE-2026-39827) / [GO-2026-5016](https://pkg.go.dev/vuln/GO-2026-5016)
More information #### Details An authenticated SSH client that repeatedly opened channels which were rejected by the server caused unbounded memory growth, eventually crashing the server process and affecting all connected users. Rejected channels are now properly removed from the connection's internal state and released for garbage collection. #### Severity Unknown #### References - [https://go.dev/issue/35127](https://go.dev/issue/35127) - [https://go.dev/cl/781320](https://go.dev/cl/781320) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5016) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Invoking client can cause server deadlock on unexpected responses in golang.org/x/crypto/ssh [CVE-2026-39830](https://nvd.nist.gov/vuln/detail/CVE-2026-39830) / [GO-2026-5017](https://pkg.go.dev/vuln/GO-2026-5017)
More information #### Details A malicious SSH peer could send unsolicited global request responses to fill an internal buffer, blocking the connection's read loop. The blocked goroutine could not be released by calling Close(), resulting in a resource leak per connection. Unsolicited global responses are now discarded. #### Severity Unknown #### References - [https://go.dev/issue/79564](https://go.dev/issue/79564) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) - [https://go.dev/cl/781640](https://go.dev/cl/781640) - [https://go.dev/cl/781664](https://go.dev/cl/781664) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5017) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Invoking pathological RSA/DSA parameters may cause DoS in golang.org/x/crypto/ssh [CVE-2026-39829](https://nvd.nist.gov/vuln/detail/CVE-2026-39829) / [GO-2026-5018](https://pkg.go.dev/vuln/GO-2026-5018)
More information #### Details The RSA and DSA public key parsers did not enforce size limits on key parameters. A crafted public key with an excessively large modulus or DSA parameter could cause several minutes of CPU consumption during signature verification. This could be triggered by unauthenticated clients during public key authentication. RSA moduli are now limited to 8192 bits, and DSA parameters are validated per FIPS 186-2. #### Severity Unknown #### References - [https://go.dev/issue/79565](https://go.dev/issue/79565) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) - [https://go.dev/cl/781641](https://go.dev/cl/781641) - [https://go.dev/cl/781661](https://go.dev/cl/781661) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5018) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Invoking bypass of FIDO/U2F security keys physical interaction in golang.org/x/crypto/ssh [CVE-2026-39831](https://nvd.nist.gov/vuln/detail/CVE-2026-39831) / [GO-2026-5019](https://pkg.go.dev/vuln/GO-2026-5019)
More information #### Details The Verify() method for FIDO/U2F security key types (sk-ecdsa-sha2-nistp256@​openssh.com, sk-ssh-ed25519@​openssh.com) did not check the User Presence flag. Signatures generated without physical touch were accepted, allowing unattended use of a hardware security key. To restore the previous behavior, return a "no-touch-required" extension in Permissions.Extensions from PublicKeyCallback. #### Severity Unknown #### References - [https://go.dev/issue/79566](https://go.dev/issue/79566) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) - [https://go.dev/cl/781662](https://go.dev/cl/781662) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5019) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Invoking infinite loop on large channel writes in golang.org/x/crypto/ssh [CVE-2026-39834](https://nvd.nist.gov/vuln/detail/CVE-2026-39834) / [GO-2026-5020](https://pkg.go.dev/vuln/GO-2026-5020)
More information #### Details When writing data larger than 4GB in a single Write call on an SSH channel, an integer overflow in the internal payload size calculation caused the write loop to spin indefinitely, sending empty packets without making progress. The size comparison now uses int64 to prevent truncation. #### Severity Unknown #### References - [https://go.dev/issue/79567](https://go.dev/issue/79567) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) - [https://go.dev/cl/781663](https://go.dev/cl/781663) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5020) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Invoking auth bypass via unenforced @​revoked status in golang.org/x/crypto/ssh/knownhosts [CVE-2026-42508](https://nvd.nist.gov/vuln/detail/CVE-2026-42508) / [GO-2026-5021](https://pkg.go.dev/vuln/GO-2026-5021)
More information #### Details Previously, a revoked 'SignatureKey' belonging to a CA was not correctly checked for revocation. Now, both the 'key' and 'key.SignatureKey' are checked for @​revoked. #### Severity Unknown #### References - [https://go.dev/issue/79568](https://go.dev/issue/79568) - [https://go.dev/cl/781220](https://go.dev/cl/781220) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5021) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Invoking VerifiedPublicKeyCallback permissions skip enforcement in golang.org/x/crypto/ssh [CVE-2026-46595](https://nvd.nist.gov/vuln/detail/CVE-2026-46595) / [GO-2026-5023](https://pkg.go.dev/vuln/GO-2026-5023)
More information #### Details Previously, CVE-2024-45337 fixed an authorization bypass for misused ssh server configurations; if any other type of callback is passed other than public key, then the source-address validation would be skipped. #### Severity Unknown #### References - [https://go.dev/issue/79570](https://go.dev/issue/79570) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) - [https://go.dev/cl/781642](https://go.dev/cl/781642) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5023) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Invoking pathological inputs can lead to client panic in golang.org/x/crypto/ssh/agent [CVE-2026-46598](https://nvd.nist.gov/vuln/detail/CVE-2026-46598) / [GO-2026-5033](https://pkg.go.dev/vuln/GO-2026-5033)
More information #### Details For certain crafted inputs, a 'ed25519.PrivateKey' was created by casting malformed wire bytes, leading to a panic when used. #### Severity Unknown #### References - [https://go.dev/issue/79596](https://go.dev/issue/79596) - [https://go.dev/cl/781360](https://go.dev/cl/781360) - [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5033) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 76eea6cc7d..1636ce976f 100644 --- a/go.mod +++ b/go.mod @@ -109,12 +109,12 @@ require ( github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc gitlab.com/gitlab-org/api/client-go/v2 v2.26.0 go.yaml.in/yaml/v4 v4.0.0-rc.3 - golang.org/x/crypto v0.51.0 + golang.org/x/crypto v0.52.0 golang.org/x/image v0.40.0 golang.org/x/net v0.54.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.44.0 + golang.org/x/sys v0.45.0 golang.org/x/text v0.37.0 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 diff --git a/go.sum b/go.sum index 0510378cb4..2864a2fec1 100644 --- a/go.sum +++ b/go.sum @@ -790,8 +790,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8= @@ -870,8 +870,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= From bf1b54c3e34df542ea7448f0962009b3951bd48c Mon Sep 17 00:00:00 2001 From: Mohamed Sekour Date: Fri, 22 May 2026 11:53:19 +0200 Subject: [PATCH 02/18] fix(api): handle partial failures in push mirror synchronization gracefully (#37782) This MR fixes an issue in the sync push mirrors endpoint. Previously, when triggering the synchronization of all push mirrors for a specific repository, the entire operation would stop if a single mirror failed for any reason. As a result, the remaining mirrors were not processed. With this fix, failures on individual push mirrors no longer abort the whole synchronization process. --------- Signed-off-by: Nicolas Co-authored-by: Nicolas Co-authored-by: silverwind Co-authored-by: Claude (Opus 4.7) --- routers/api/v1/repo/mirror.go | 13 ++++++-- routers/api/v1/repo/mirror_test.go | 40 +++++++++++++++++++++++++ templates/swagger/v1_json.tmpl | 3 ++ templates/swagger/v1_openapi3_json.tmpl | 3 ++ 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 routers/api/v1/repo/mirror_test.go diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index ac2d8bba06..bdca030b76 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -6,6 +6,7 @@ package repo import ( "errors" "net/http" + "strings" "time" "code.gitea.io/gitea/models/db" @@ -101,6 +102,8 @@ func PushMirrorSync(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" if !setting.Mirror.Enabled { ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled") @@ -112,14 +115,18 @@ func PushMirrorSync(ctx *context.APIContext) { ctx.APIError(http.StatusNotFound, err) return } + + failedPushMirrors := make([]string, 0) for _, mirror := range pushMirrors { ok := mirror_service.SyncPushMirror(ctx, mirror.ID) if !ok { - ctx.APIErrorInternal(errors.New("error occurred when syncing push mirror " + mirror.RemoteName)) - return + failedPushMirrors = append(failedPushMirrors, mirror.RemoteName) } } - + if len(failedPushMirrors) != 0 { + ctx.APIError(http.StatusUnprocessableEntity, "error occurred when syncing push mirrors: "+strings.Join(failedPushMirrors, ", ")) + return + } ctx.Status(http.StatusOK) } diff --git a/routers/api/v1/repo/mirror_test.go b/routers/api/v1/repo/mirror_test.go new file mode 100644 index 0000000000..6cbed49d89 --- /dev/null +++ b/routers/api/v1/repo/mirror_test.go @@ -0,0 +1,40 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/services/contexttest" + + "github.com/stretchr/testify/assert" +) + +// TestPushMirrorSync verifies the endpoint attempts every push mirror instead +// of aborting on the first failure, reporting all failed remotes with a 422. +// Each remote name is not a configured git remote, so SyncPushMirror fails fast +// without any network access. +func TestPushMirrorSync(t *testing.T) { + unittest.PrepareTestEnv(t) + defer test.MockVariableValue(&setting.Mirror.Enabled, true)() + + for _, remoteName := range []string{"broken_remote_1", "broken_remote_2"} { + assert.NoError(t, db.Insert(t.Context(), &repo_model.PushMirror{RepoID: 1, RemoteName: remoteName})) + } + + ctx, resp := contexttest.MockAPIContext(t, "user2/repo1") + contexttest.LoadRepo(t, ctx, 1) + + PushMirrorSync(ctx) + + assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus()) + assert.Contains(t, resp.Body.String(), "broken_remote_1") + assert.Contains(t, resp.Body.String(), "broken_remote_2") +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 312eb25fd8..d95699a48d 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -15646,6 +15646,9 @@ }, "404": { "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" } } } diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index 04b8bd2d62..642154c553 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -27446,6 +27446,9 @@ }, "404": { "$ref": "#/components/responses/notFound" + }, + "422": { + "$ref": "#/components/responses/validationError" } }, "summary": "Sync all push mirrored repository", From bc9d53a5a89d9e1debf08ee52374c6d4e8cac038 Mon Sep 17 00:00:00 2001 From: Alaa Abdelwahab <82750565+Powerscore@users.noreply.github.com> Date: Fri, 22 May 2026 16:18:32 +0200 Subject: [PATCH 03/18] =?UTF-8?q?fix(issues):=20clear=20stale=20ReviewType?= =?UTF-8?q?Request=20when=20submitting=20pending=20re=E2=80=A6=20(#37809)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When SubmitReview updates an existing pending review in-place, it was not deleting the reviewer's ReviewTypeRequest row, unlike the CreateReview path. That leftover row causes AddReviewRequest to bail out silently, making the re-request icon in the PR sidebar a no-op. Fixes #37808 (Claude Opus 4.7) --------- Co-authored-by: silverwind Co-authored-by: Claude (Opus 4.7) Co-authored-by: Nicolas --- models/issues/review.go | 8 ++++++++ models/issues/review_test.go | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/models/issues/review.go b/models/issues/review.go index 78ef0d20c2..9936622219 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -483,6 +483,14 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil { return nil, nil, err } + + // make sure the leftover review request is cleared, consistent with CreateReview + if reviewType != ReviewTypePending { + if _, err := sess.Where(builder.Eq{"reviewer_id": doer.ID, "issue_id": issue.ID, "type": ReviewTypeRequest}). + Delete(new(Review)); err != nil { + return nil, nil, err + } + } } comm, err := CreateComment(ctx, &CreateCommentOptions{ diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 092d88d174..a384dbd30f 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -303,6 +303,46 @@ func TestDeleteDismissedReview(t *testing.T) { unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID}) } +func TestSubmitReviewClearsStaleReviewRequest(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) + assert.NoError(t, issue.LoadRepo(t.Context())) + assert.NoError(t, issue.Repo.LoadOwner(t.Context())) + reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // the reviewer is requested to review the pull request + requestReview, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{ + Type: issues_model.ReviewTypeRequest, + Issue: issue, + Reviewer: reviewer, + }) + assert.NoError(t, err) + + // the reviewer starts a pending review (e.g. by adding code comments) + pendingReview, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{ + Type: issues_model.ReviewTypePending, + Issue: issue, + Reviewer: reviewer, + }) + assert.NoError(t, err) + + // submitting the pending review must clear the leftover review request, + // otherwise the reviewer can no longer be re-requested afterwards + review, _, err := issues_model.SubmitReview(t.Context(), reviewer, issue, issues_model.ReviewTypeComment, "looks good", "", false, nil) + assert.NoError(t, err) + assert.Equal(t, pendingReview.ID, review.ID) + assert.Equal(t, issues_model.ReviewTypeComment, review.Type) + + unittest.AssertNotExistsBean(t, &issues_model.Review{ID: requestReview.ID}) + + // the reviewer can be re-requested afterwards (no-op before the fix) + comment, err := issues_model.AddReviewRequest(t.Context(), issue, reviewer, doer, false) + assert.NoError(t, err) + assert.NotNil(t, comment) +} + func TestAddReviewRequest(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) From 4c37f4dacbac022f7beca75272439331f0368830 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Fri, 22 May 2026 10:28:54 -0700 Subject: [PATCH 04/18] fix(deps): update module golang.org/x/net to v0.55.0 [security] (#37813) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1636ce976f..9015b9ec49 100644 --- a/go.mod +++ b/go.mod @@ -111,7 +111,7 @@ require ( go.yaml.in/yaml/v4 v4.0.0-rc.3 golang.org/x/crypto v0.52.0 golang.org/x/image v0.40.0 - golang.org/x/net v0.54.0 + golang.org/x/net v0.55.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 golang.org/x/sys v0.45.0 diff --git a/go.sum b/go.sum index 2864a2fec1..9ade1fab9e 100644 --- a/go.sum +++ b/go.sum @@ -822,8 +822,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 7c12446c1f14d91ca9118ff1a98a81e9bd9ed684 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 22 May 2026 20:52:04 +0200 Subject: [PATCH 05/18] test(e2e): add comment, release, star, PR and fork tests (#37800) Adds Playwright e2e coverage for five high-value workflows, each driven through semantic locators with API-based setup: - comment on and close an issue - publish a release - star and watch a repository - create a pull request from the compare page - fork a repository Also passes `autoInit: false` in existing tests that only exercise DB-backed units (issues, reactions, milestones, projects, events), skipping an unused initial commit to speed up their setup and reduce parallel git contention. --- This PR was written with the help of Claude Opus 4.7 --------- Co-authored-by: Claude (Opus 4.7) Co-authored-by: Nicolas --- tests/e2e/events.test.ts | 4 ++-- tests/e2e/fork.test.ts | 18 ++++++++++++++++++ tests/e2e/issue-comment.test.ts | 24 ++++++++++++++++++++++++ tests/e2e/issue-project.test.ts | 18 +++++++++--------- tests/e2e/mermaid.test.ts | 2 +- tests/e2e/milestone.test.ts | 2 +- tests/e2e/pr-create.test.ts | 23 +++++++++++++++++++++++ tests/e2e/reactions.test.ts | 2 +- tests/e2e/release.test.ts | 19 +++++++++++++++++++ tests/e2e/repo-star-watch.test.ts | 20 ++++++++++++++++++++ 10 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 tests/e2e/fork.test.ts create mode 100644 tests/e2e/issue-comment.test.ts create mode 100644 tests/e2e/pr-create.test.ts create mode 100644 tests/e2e/release.test.ts create mode 100644 tests/e2e/repo-star-watch.test.ts diff --git a/tests/e2e/events.test.ts b/tests/e2e/events.test.ts index c7cfef6e9c..54b4774a5c 100644 --- a/tests/e2e/events.test.ts +++ b/tests/e2e/events.test.ts @@ -12,7 +12,7 @@ test.describe('events', () => { // 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)}), + apiCreateRepo(request, {name: repoName, autoInit: false, headers: apiUserHeaders(owner)}), loginUser(page, owner), ]); await page.goto('/'); @@ -36,7 +36,7 @@ test.describe('events', () => { await Promise.all([ loginUser(page, name), (async () => { - await apiCreateRepo(request, {name, headers}); + await apiCreateRepo(request, {name, autoInit: false, headers}); await apiCreateIssue(request, {owner: name, repo: name, title: 'events stopwatch test', headers}); await apiStartStopwatch(request, name, name, 1, {headers}); })(), diff --git a/tests/e2e/fork.test.ts b/tests/e2e/fork.test.ts new file mode 100644 index 0000000000..77ccda1242 --- /dev/null +++ b/tests/e2e/fork.test.ts @@ -0,0 +1,18 @@ +import {env} from 'node:process'; +import {test, expect} from '@playwright/test'; +import {login, apiCreateRepo, apiCreateUser, apiUserHeaders, randomString} from './utils.ts'; + +test('fork a repository', async ({page, request}) => { + const upstream = `fork-owner-${randomString(8)}`; + const repoName = `e2e-fork-${randomString(8)}`; + await apiCreateUser(request, upstream); + await Promise.all([ + apiCreateRepo(request, {name: repoName, headers: apiUserHeaders(upstream)}), + login(page), + ]); + await page.goto(`/${upstream}/${repoName}/fork`); + + await page.getByRole('button', {name: 'Fork Repository'}).click(); + await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}$`)); + await expect(page.getByRole('link', {name: `${upstream}/${repoName}`})).toBeVisible(); +}); diff --git a/tests/e2e/issue-comment.test.ts b/tests/e2e/issue-comment.test.ts new file mode 100644 index 0000000000..d3de59ba5f --- /dev/null +++ b/tests/e2e/issue-comment.test.ts @@ -0,0 +1,24 @@ +import {env} from 'node:process'; +import {test, expect} from '@playwright/test'; +import {login, apiCreateRepo, apiCreateIssue, randomString} from './utils.ts'; + +test('comment on and close an issue', async ({page, request}) => { + const repoName = `e2e-issue-comment-${randomString(8)}`; + const owner = env.GITEA_TEST_E2E_USER; + await apiCreateRepo(request, {name: repoName, autoInit: false}); + await Promise.all([ + apiCreateIssue(request, {owner, repo: repoName, title: 'Comment test'}), + login(page), + ]); + await page.goto(`/${owner}/${repoName}/issues/1`); + + const body = `e2e-comment-${randomString(8)}`; + await page.getByPlaceholder('Leave a comment').fill(body); + // exact match: the status button reads "Close with Comment" while the box has content, which substring-matches "Comment" + await page.getByRole('button', {name: 'Comment', exact: true}).click(); + await expect(page.locator('.comment-body').filter({hasText: body})).toBeVisible(); + + // posting reloaded the page with an empty box, so the status button now reads "Close Issue" + await page.getByRole('button', {name: 'Close Issue'}).click(); + await expect(page.getByRole('button', {name: 'Reopen Issue'})).toBeVisible(); +}); diff --git a/tests/e2e/issue-project.test.ts b/tests/e2e/issue-project.test.ts index b1fed72a6f..1595cfadc5 100644 --- a/tests/e2e/issue-project.test.ts +++ b/tests/e2e/issue-project.test.ts @@ -5,7 +5,7 @@ import {login, apiCreateRepo, apiCreateIssue, apiDeleteRepo, createProject, crea test('assign issue to project and change column', async ({page}) => { const repoName = `e2e-issue-project-${randomString(8)}`; const user = env.GITEA_TEST_E2E_USER; - await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]); + await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName, autoInit: false})]); await page.goto(`/${user}/${repoName}/projects/new`); await page.locator('input[name="title"]').fill('Kanban Board'); await page.getByRole('button', {name: 'Create Project'}).click(); @@ -33,7 +33,7 @@ test('create a project', async ({page}) => { const projectTitle = 'Test Project'; await login(page); - await apiCreateRepo(page.request, {name: repoName}); + await apiCreateRepo(page.request, {name: repoName, autoInit: false}); try { // Navigate to new project page @@ -62,7 +62,7 @@ test('assign issue to multiple projects via sidebar', async ({page}) => { const issueTitle = 'Test issue for multiple projects'; await login(page); - await apiCreateRepo(page.request, {name: repoName}); + await apiCreateRepo(page.request, {name: repoName, autoInit: false}); try { // Create two projects via UI @@ -112,7 +112,7 @@ test('create issue with multiple projects pre-selected', async ({page}) => { const issueTitle = 'Issue with multiple projects'; await login(page); - await apiCreateRepo(page.request, {name: repoName}); + await apiCreateRepo(page.request, {name: repoName, autoInit: false}); try { // Create two projects via UI @@ -163,7 +163,7 @@ test('filter issues by multiple projects in issue list', async ({page}) => { const project2Title = 'Filter Project B'; await login(page); - await apiCreateRepo(page.request, {name: repoName}); + await apiCreateRepo(page.request, {name: repoName, autoInit: false}); try { // Create two projects via UI @@ -229,7 +229,7 @@ test('remove issue from one project keeping others', async ({page}) => { const issueTitle = 'Issue to modify projects'; await login(page); - await apiCreateRepo(page.request, {name: repoName}); + await apiCreateRepo(page.request, {name: repoName, autoInit: false}); try { // Create two projects via UI @@ -288,7 +288,7 @@ test('filter issues with no project using project=-1', async ({page}) => { const projectTitle = 'Some Project'; await login(page); - await apiCreateRepo(page.request, {name: repoName}); + await apiCreateRepo(page.request, {name: repoName, autoInit: false}); try { // Create a project via UI @@ -349,7 +349,7 @@ test('close project and view in closed projects list', async ({page}) => { const closedProjectTitle = 'Project To Close'; await login(page); - await apiCreateRepo(page.request, {name: repoName}); + await apiCreateRepo(page.request, {name: repoName, autoInit: false}); try { // Create two projects via UI @@ -404,7 +404,7 @@ test('select projects on new issue page shows in sidebar', async ({page}) => { const project2Title = 'Project Two'; await login(page); - await apiCreateRepo(page.request, {name: repoName}); + await apiCreateRepo(page.request, {name: repoName, autoInit: false}); try { // Create two projects diff --git a/tests/e2e/mermaid.test.ts b/tests/e2e/mermaid.test.ts index b2e066e551..1523577eaa 100644 --- a/tests/e2e/mermaid.test.ts +++ b/tests/e2e/mermaid.test.ts @@ -5,7 +5,7 @@ import {apiCreateRepo, apiCreateIssue, assertNoJsError, randomString} from './ut test('mermaid diagram in issue', async ({page, request}) => { const repoName = `e2e-mermaid-${randomString(8)}`; const owner = env.GITEA_TEST_E2E_USER; - await apiCreateRepo(request, {name: repoName}); + await apiCreateRepo(request, {name: repoName, autoInit: false}); const body = '```mermaid\nflowchart LR\n Alpha --> Beta\n Beta --> Gamma\n```\n'; const {index} = await apiCreateIssue(request, {owner, repo: repoName, title: 'mermaid test', body}); await page.goto(`/${owner}/${repoName}/issues/${index}`); diff --git a/tests/e2e/milestone.test.ts b/tests/e2e/milestone.test.ts index 5a688fb128..106381e64f 100644 --- a/tests/e2e/milestone.test.ts +++ b/tests/e2e/milestone.test.ts @@ -4,7 +4,7 @@ import {login, apiCreateRepo, randomString} from './utils.ts'; test('create a milestone', async ({page}) => { const repoName = `e2e-milestone-${randomString(8)}`; - await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]); + await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName, autoInit: false})]); await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/milestones/new`); await page.getByPlaceholder('Title').fill('Test Milestone'); await page.getByRole('button', {name: 'Create Milestone'}).click(); diff --git a/tests/e2e/pr-create.test.ts b/tests/e2e/pr-create.test.ts new file mode 100644 index 0000000000..592512683f --- /dev/null +++ b/tests/e2e/pr-create.test.ts @@ -0,0 +1,23 @@ +import {env} from 'node:process'; +import {test, expect} from '@playwright/test'; +import {login, apiCreateRepo, apiCreateFile, randomString} from './utils.ts'; + +test('create a pull request from the compare page', async ({page, request}) => { + const repoName = `e2e-pr-create-${randomString(8)}`; + const owner = env.GITEA_TEST_E2E_USER; + await apiCreateRepo(request, {name: repoName}); + await Promise.all([ + apiCreateFile(request, owner, repoName, 'feat.txt', 'feature content\n', {branch: 'main', newBranch: 'feat'}), + login(page), + ]); + // expand=1 renders the PR form directly, skipping the "New Pull Request" toggle click + await page.goto(`/${owner}/${repoName}/compare/main...feat?expand=1`); + + const title = `e2e-pr-${randomString(8)}`; + await page.getByPlaceholder('Title').fill(title); + await page.getByRole('button', {name: 'Create Pull Request'}).click(); + + // commit, not full load: the PR title heading is server-rendered, so the assertion can resolve before the heavy diff/timeline finishes + await page.waitForURL(new RegExp(`/${owner}/${repoName}/pulls/\\d+$`), {waitUntil: 'commit'}); + await expect(page.getByRole('heading', {name: title})).toBeVisible(); +}); diff --git a/tests/e2e/reactions.test.ts b/tests/e2e/reactions.test.ts index 2048b938cf..a6b6183741 100644 --- a/tests/e2e/reactions.test.ts +++ b/tests/e2e/reactions.test.ts @@ -5,7 +5,7 @@ import {login, apiCreateRepo, apiCreateIssue, randomString} from './utils.ts'; test('toggle issue reactions', async ({page, request}) => { const repoName = `e2e-reactions-${randomString(8)}`; const owner = env.GITEA_TEST_E2E_USER; - await apiCreateRepo(request, {name: repoName}); + await apiCreateRepo(request, {name: repoName, autoInit: false}); await Promise.all([ apiCreateIssue(request, {owner, repo: repoName, title: 'Reaction test'}), login(page), diff --git a/tests/e2e/release.test.ts b/tests/e2e/release.test.ts new file mode 100644 index 0000000000..219a16d2c5 --- /dev/null +++ b/tests/e2e/release.test.ts @@ -0,0 +1,19 @@ +import {env} from 'node:process'; +import {test, expect} from '@playwright/test'; +import {login, apiCreateRepo, randomString} from './utils.ts'; + +test('create a release', async ({page, request}) => { + const repoName = `e2e-release-${randomString(8)}`; + const owner = env.GITEA_TEST_E2E_USER; + await Promise.all([apiCreateRepo(request, {name: repoName}), login(page)]); + await page.goto(`/${owner}/${repoName}/releases/new`); + + const tag = `v1.0.0-${randomString(8)}`; + const title = `e2e-release-${randomString(8)}`; + await page.getByLabel('Tag name').fill(tag); + await page.getByLabel('Release title').fill(title); + await page.getByRole('button', {name: 'Publish Release'}).click(); + + await page.waitForURL(new RegExp(`/${owner}/${repoName}/releases$`)); + await expect(page.locator('.release-list-title')).toContainText(title); +}); diff --git a/tests/e2e/repo-star-watch.test.ts b/tests/e2e/repo-star-watch.test.ts new file mode 100644 index 0000000000..d5f8449165 --- /dev/null +++ b/tests/e2e/repo-star-watch.test.ts @@ -0,0 +1,20 @@ +import {test, expect} from '@playwright/test'; +import {login, apiCreateRepo, apiCreateUser, apiUserHeaders, randomString} from './utils.ts'; + +test('star and watch a repository', async ({page, request}) => { + const owner = `sw-owner-${randomString(8)}`; + const repoName = `e2e-star-watch-${randomString(8)}`; + await apiCreateUser(request, owner); + await Promise.all([ + apiCreateRepo(request, {name: repoName, autoInit: false, headers: apiUserHeaders(owner)}), + login(page), + ]); + await page.goto(`/${owner}/${repoName}`); + + // exact match so "Star"/"Watch" don't also match "Unstar"/"Unwatch" + await page.getByRole('button', {name: 'Star', exact: true}).click(); + await expect(page.getByRole('button', {name: 'Unstar'})).toBeVisible(); + + await page.getByRole('button', {name: 'Watch', exact: true}).click(); + await expect(page.getByRole('button', {name: 'Unwatch'})).toBeVisible(); +}); From f95c210abe7333808fc7b140d91000b0b33efaf4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 22 May 2026 21:07:35 +0200 Subject: [PATCH 06/18] ci: split pgsql shards into plain jobs, dedupe setup actions (#37802) 1. Split the psql matrix jobs into composite actions. Matrix jobs that can skip do not work with required checks on GitHub because skipped and unskipped emit different job names (GitHub bug https://github.com/orgs/community/discussions/9141). 2. Dedupe node and go setup steps into composite actions Currently test-psql branch protection is disabled, will re-enable when merging this. --------- Signed-off-by: silverwind Co-authored-by: Claude (Opus 4.7) Co-authored-by: Nicolas --- .github/actions/go-setup/action.yml | 23 +++++++ .github/actions/node-setup/action.yml | 22 ++++++ .github/actions/pgsql-shard/action.yml | 40 +++++++++++ .github/workflows/cache-seeder.yml | 15 +--- .github/workflows/files-changed.yml | 1 + .github/workflows/pull-compliance.yml | 39 +++-------- .github/workflows/pull-db-tests.yml | 95 ++++++++++++-------------- .github/workflows/pull-e2e-tests.yml | 14 +--- 8 files changed, 143 insertions(+), 106 deletions(-) create mode 100644 .github/actions/go-setup/action.yml create mode 100644 .github/actions/node-setup/action.yml create mode 100644 .github/actions/pgsql-shard/action.yml diff --git a/.github/actions/go-setup/action.yml b/.github/actions/go-setup/action.yml new file mode 100644 index 0000000000..d6f88bbc6d --- /dev/null +++ b/.github/actions/go-setup/action.yml @@ -0,0 +1,23 @@ +name: go-setup +description: Set up go and restore caches + +inputs: + cache: + description: Restore go caches + default: "true" + lint-cache: + description: Also restore the golangci-lint cache + default: "false" + +runs: + using: composite + steps: + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + check-latest: true + cache: false + - if: ${{ inputs.cache == 'true' }} + uses: ./.github/actions/go-cache + with: + lint-cache: ${{ inputs.lint-cache }} diff --git a/.github/actions/node-setup/action.yml b/.github/actions/node-setup/action.yml new file mode 100644 index 0000000000..c9ab484d57 --- /dev/null +++ b/.github/actions/node-setup/action.yml @@ -0,0 +1,22 @@ +name: node-setup +description: Set up pnpm and node and restore caches + +inputs: + cache: + description: Cache pnpm downloads + default: "true" + +runs: + using: composite + steps: + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - if: ${{ inputs.cache == 'true' }} + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + - if: ${{ inputs.cache != 'true' }} + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24 diff --git a/.github/actions/pgsql-shard/action.yml b/.github/actions/pgsql-shard/action.yml new file mode 100644 index 0000000000..9a5a03ae0d --- /dev/null +++ b/.github/actions/pgsql-shard/action.yml @@ -0,0 +1,40 @@ +name: pgsql-shard +description: Run one pgsql integration test shard + +inputs: + shard: + description: Shard index + required: true + total-shards: + description: Total shard count + required: true + run-migration: + description: Also run migration tests + default: "false" + +runs: + using: composite + steps: + - name: Add hosts to /etc/hosts + shell: bash + run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts' + - shell: bash + run: make deps-backend + - shell: bash + run: make backend + env: + TAGS: bindata + - name: run migration tests + if: ${{ inputs.run-migration == 'true' }} + shell: bash + run: GITEA_TEST_DATABASE=pgsql make test-migration + - name: run tests + shell: bash + run: GITEA_TEST_DATABASE=pgsql make test-integration + env: + # pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower. + GOTEST_FLAGS: -race -timeout=40m + TAGS: bindata gogit + TEST_LDAP: 1 + TEST_SHARD: ${{ inputs.shard }} + TEST_TOTAL_SHARDS: ${{ inputs.total-shards }} diff --git a/.github/workflows/cache-seeder.yml b/.github/workflows/cache-seeder.yml index 91109b1c06..8ec7adee07 100644 --- a/.github/workflows/cache-seeder.yml +++ b/.github/workflows/cache-seeder.yml @@ -15,6 +15,7 @@ on: - "go.sum" - ".golangci.yml" - ".github/actions/go-cache/action.yml" + - ".github/actions/go-setup/action.yml" - ".github/workflows/cache-seeder.yml" concurrency: @@ -29,12 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: ./.github/actions/go-cache + - uses: ./.github/actions/go-setup - run: make deps-backend deps-tools - run: TAGS="bindata" make backend - run: TAGS="bindata gogit" GOEXPERIMENT="" make backend @@ -64,12 +60,7 @@ jobs: - { tags: "bindata", target: "lint-backend" } steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: ./.github/actions/go-cache + - uses: ./.github/actions/go-setup with: lint-cache: "true" - run: make deps-backend deps-tools diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index ac2bd1a5ba..2975a0cfcb 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -81,6 +81,7 @@ jobs: actions: - ".github/workflows/*" + - ".github/actions/**" - "Makefile" templates: diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index ada537a188..9ccfc76315 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -20,12 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: ./.github/actions/go-cache + - uses: ./.github/actions/go-setup with: lint-cache: "true" - run: make deps-backend deps-tools @@ -37,15 +32,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - uses: ./.github/actions/go-setup with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + cache: "false" + - uses: ./.github/actions/node-setup with: - node-version: 24 + cache: "false" - run: make lint-spell @@ -68,12 +60,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: ./.github/actions/go-cache + - uses: ./.github/actions/go-setup - run: make deps-backend deps-tools - run: make --always-make checks-backend # ensure the "go-licenses" make target runs @@ -83,12 +70,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml + - uses: ./.github/actions/node-setup - run: make deps-frontend - run: make lint-frontend - run: make checks-frontend @@ -101,12 +83,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: ./.github/actions/go-cache + - uses: ./.github/actions/go-setup - run: make deps-backend generate-go # no frontend build here as backend should be able to build, even without any frontend files # CGO is not used when cross-compile, so these steps also test if the code is compatible with CGO disabled diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index bc73d6391c..e4de01e711 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -14,14 +14,11 @@ jobs: files-changed: uses: ./.github/workflows/files-changed.yml - test-pgsql-shards: + test-pgsql-shard-1: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - shard: [1, 2] + timeout-minutes: 50 services: pgsql: image: postgres:14 @@ -46,31 +43,47 @@ jobs: - "9000:9000" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - uses: ./.github/actions/go-setup + - uses: ./.github/actions/pgsql-shard with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: ./.github/actions/go-cache - - name: Add hosts to /etc/hosts - run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts' - - run: make deps-backend - - run: make backend + shard: 1 + total-shards: 2 + run-migration: "true" + + test-pgsql-shard-2: + if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' + needs: files-changed + runs-on: ubuntu-latest + timeout-minutes: 50 + services: + pgsql: + image: postgres:14 env: - TAGS: bindata - - name: run migration tests - if: matrix.shard == 1 - run: GITEA_TEST_DATABASE=pgsql make test-migration - - name: run tests - run: GITEA_TEST_DATABASE=pgsql make test-integration - timeout-minutes: 50 + POSTGRES_DB: test + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + ldap: + image: gitea/test-openldap:latest@sha256:4ac633b01d684e6b2a458cc0c8530c92f9b3702f6e040ce5f365607df34fbda0 + ports: + - "389:389" + - "636:636" + minio: + # as github actions doesn't support "entrypoint", we need to use a non-official image + # that has a custom entrypoint set to "minio server /data" + image: bitnamilegacy/minio:2025.7.23 env: - # pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower. - GOTEST_FLAGS: -race -timeout=40m - TAGS: bindata gogit - TEST_LDAP: 1 - TEST_SHARD: ${{ matrix.shard }} - TEST_TOTAL_SHARDS: ${{ strategy.job-total }} + MINIO_ROOT_USER: 123456 + MINIO_ROOT_PASSWORD: 12345678 + ports: + - "9000:9000" + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./.github/actions/go-setup + - uses: ./.github/actions/pgsql-shard + with: + shard: 2 + total-shards: 2 test-sqlite: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' @@ -78,12 +91,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: ./.github/actions/go-cache + - uses: ./.github/actions/go-setup - run: make deps-backend - run: make backend env: @@ -141,12 +149,7 @@ jobs: - 10000:10000 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: ./.github/actions/go-cache + - uses: ./.github/actions/go-setup - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts' - run: make deps-backend @@ -200,12 +203,7 @@ jobs: - "993:993" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: ./.github/actions/go-cache + - uses: ./.github/actions/go-setup - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts' - run: make deps-backend @@ -238,12 +236,7 @@ jobs: - 10000:10000 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: ./.github/actions/go-cache + - uses: ./.github/actions/go-setup - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts' - run: make deps-backend diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index 7bf73d4a0f..f0283f4022 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -20,18 +20,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: ./.github/actions/go-cache - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml + - uses: ./.github/actions/go-setup + - uses: ./.github/actions/node-setup - run: make deps-frontend - run: make frontend - run: make deps-backend From 12fcd78e8e1b16c580e0b3a26ba2fb663b47af2f Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Fri, 22 May 2026 15:27:09 -0400 Subject: [PATCH 07/18] build(snap): publish nightly version to snapcraft via actions (#37814) The secret is added to the repo already. Right now this only publishes commits to main branch to the "latest/edge" snap channel, but if this is successful we can add more workflows/logic to be able to publish RCs/fully tagged versions too. --------- Co-authored-by: Nicolas --- .../workflows/release-nightly-snapcraft.yml | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/release-nightly-snapcraft.yml diff --git a/.github/workflows/release-nightly-snapcraft.yml b/.github/workflows/release-nightly-snapcraft.yml new file mode 100644 index 0000000000..0f9ac1d423 --- /dev/null +++ b/.github/workflows/release-nightly-snapcraft.yml @@ -0,0 +1,41 @@ +name: release-nightly-snapcraft + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install snapcraft + run: sudo snap install snapcraft --classic + + - name: Remote build + run: | + snapcraft remote-build \ + --launchpad-accept-public-upload \ + --build-for=amd64,arm64,armhf + + - name: List built snaps + run: find . -maxdepth 1 -type f -name '*.snap' -print + + - name: Upload and release snapcraft nightly build + run: | + set -euo pipefail + + for snap in ./*.snap; do + echo "Uploading $snap to edge" + snapcraft upload --release="latest/edge" "$snap" + done From f0eb065df7597f4364124f8b62b79b4741d159c4 Mon Sep 17 00:00:00 2001 From: metiftikci Date: Fri, 22 May 2026 22:44:48 +0300 Subject: [PATCH 08/18] enhance(ui): add gap between file tree and diff on pr changes page (#37798) ### Before file-tree-before ### After file-tree-after Co-authored-by: Lunny Xiao --- web_src/css/repo.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web_src/css/repo.css b/web_src/css/repo.css index f3b169aeb9..b47e03b8ce 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1578,6 +1578,7 @@ tbody.commit-list { #diff-container { display: flex; + gap: var(--page-spacing); } #diff-file-boxes { From 570173b4093409e461f53f1f021310a5aeb32a15 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 22 May 2026 22:22:04 +0200 Subject: [PATCH 09/18] ci: add shellcheck linter (#37682) - Adds `make lint-shell`. uses local `shellcheck` if its version matches, otherwise runs the pinned image via docker or podman - `.shellcheckrc` disables the most annoying rules - Fixes findings across existing scripts Fixes: #37648 Signed-off-by: silverwind Co-authored-by: Claude (Opus 4.7) Co-authored-by: TheFox0x7 Co-authored-by: Nicolas --- .github/workflows/files-changed.yml | 7 ++++++ .github/workflows/pull-compliance.yml | 3 +++ .shellcheckrc | 1 + Makefile | 13 +++++++--- contrib/upgrade.sh | 1 + .../usr/local/bin/docker-entrypoint.sh | 2 +- docker/rootless/usr/local/bin/docker-setup.sh | 24 +++++++++---------- renovate.json5 | 1 + snap/part-gitea-build.sh | 2 +- snap/part-gitea-pull.sh | 2 +- tools/lint-shell.sh | 11 +++++++++ tools/test-e2e.sh | 2 ++ 12 files changed, 51 insertions(+), 18 deletions(-) create mode 100644 .shellcheckrc create mode 100755 tools/lint-shell.sh diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index 2975a0cfcb..a7b116fec7 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -25,6 +25,8 @@ on: value: ${{ jobs.detect.outputs.json }} e2e: value: ${{ jobs.detect.outputs.e2e }} + shell: + value: ${{ jobs.detect.outputs.shell }} permissions: contents: read @@ -45,6 +47,7 @@ jobs: yaml: ${{ steps.changes.outputs.yaml }} json: ${{ steps.changes.outputs.json }} e2e: ${{ steps.changes.outputs.e2e }} + shell: ${{ steps.changes.outputs.shell }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 @@ -124,3 +127,7 @@ jobs: - "tests/e2e/**" - "tools/test-e2e.sh" - "playwright.config.ts" + + shell: + - "**/*.sh" + - ".shellcheckrc" diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 9ccfc76315..801966e144 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -54,6 +54,9 @@ jobs: - if: needs.files-changed.outputs.actions == 'true' run: make lint-actions + - if: needs.files-changed.outputs.shell == 'true' + run: make lint-shell + checks-backend: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000000..a012fba96e --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1 @@ +disable=SC1091,SC2001,SC2002,SC2016,SC2028,SC2046,SC2124,SC2128,SC2129,SC2154,SC2155,SC2164,SC2181,SC2207 diff --git a/Makefile b/Makefile index 94f07a4412..52a266fa39 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,9 @@ SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.2 # renova XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.0 # renovate: datasource=go ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go +SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker + +CONTAINER_RUNTIME ?= $(shell hash docker >/dev/null 2>&1 && echo docker || echo podman) HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes) ifeq ($(HAS_GO), yes) @@ -271,7 +274,7 @@ checks-frontend: lockfile-check svg-check ## check frontend files checks-backend: tidy-check swagger-check openapi3-check fmt-check swagger-validate security-check ## check backend files .PHONY: lint -lint: lint-frontend lint-backend lint-templates lint-swagger lint-spell lint-md lint-actions lint-json lint-yaml ## lint everything +lint: lint-frontend lint-backend lint-templates lint-swagger lint-spell lint-md lint-actions lint-json lint-yaml lint-shell ## lint everything .PHONY: lint-fix lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues @@ -348,6 +351,10 @@ lint-actions: .venv ## lint action workflow files @$(GO) run $(ACTIONLINT_PACKAGE) @uv run --frozen zizmor --quiet --min-confidence=medium .github +.PHONY: lint-shell +lint-shell: ## lint shell scripts + @SHELLCHECK_IMAGE=$(SHELLCHECK_IMAGE) CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) ./tools/lint-shell.sh $$(git ls-files '*.sh') + .PHONY: lint-templates lint-templates: .venv node_modules ## lint template files @node tools/lint-templates-svg.ts @@ -473,11 +480,11 @@ migrations.individual.test\#%: .PHONY: playwright playwright: deps-frontend - @./tools/test-e2e.sh install + @CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) ./tools/test-e2e.sh install .PHONY: test-e2e test-e2e: playwright frontend backend - @EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh run $(GITEA_TEST_E2E_FLAGS) + @CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh run $(GITEA_TEST_E2E_FLAGS) .PHONY: build build: frontend backend ## build everything diff --git a/contrib/upgrade.sh b/contrib/upgrade.sh index 2593d24509..71d0435d14 100755 --- a/contrib/upgrade.sh +++ b/contrib/upgrade.sh @@ -126,6 +126,7 @@ giteacmd manager flush-queues echo "Stopping gitea at $(date)" $service_stop echo "Creating backup in $giteahome" +# shellcheck disable=SC2086 # flag string giteacmd dump $backupopts echo "Updating binary at $giteabin" cp -f "$giteabin" "$giteabin.bak" && mv -f "$binname" "$giteabin" diff --git a/docker/rootless/usr/local/bin/docker-entrypoint.sh b/docker/rootless/usr/local/bin/docker-entrypoint.sh index ca509214bf..4ffc4ff3a1 100755 --- a/docker/rootless/usr/local/bin/docker-entrypoint.sh +++ b/docker/rootless/usr/local/bin/docker-entrypoint.sh @@ -13,5 +13,5 @@ fi if [ $# -gt 0 ]; then exec "$@" else - exec /usr/local/bin/gitea -c ${GITEA_APP_INI} web + exec /usr/local/bin/gitea -c "${GITEA_APP_INI}" web fi diff --git a/docker/rootless/usr/local/bin/docker-setup.sh b/docker/rootless/usr/local/bin/docker-setup.sh index feab02a379..3320d69dde 100755 --- a/docker/rootless/usr/local/bin/docker-setup.sh +++ b/docker/rootless/usr/local/bin/docker-setup.sh @@ -1,23 +1,23 @@ #!/bin/bash # Prepare git folder -mkdir -p ${HOME} && chmod 0700 ${HOME} -if [ ! -w ${HOME} ]; then echo "${HOME} is not writable"; exit 1; fi +mkdir -p "${HOME}" && chmod 0700 "${HOME}" +if [ ! -w "${HOME}" ]; then echo "${HOME} is not writable"; exit 1; fi # Prepare custom folder -mkdir -p ${GITEA_CUSTOM} && chmod 0700 ${GITEA_CUSTOM} +mkdir -p "${GITEA_CUSTOM}" && chmod 0700 "${GITEA_CUSTOM}" # Prepare temp folder -mkdir -p ${GITEA_TEMP} && chmod 0700 ${GITEA_TEMP} -if [ ! -w ${GITEA_TEMP} ]; then echo "${GITEA_TEMP} is not writable"; exit 1; fi +mkdir -p "${GITEA_TEMP}" && chmod 0700 "${GITEA_TEMP}" +if [ ! -w "${GITEA_TEMP}" ]; then echo "${GITEA_TEMP} is not writable"; exit 1; fi #Prepare config file -if [ ! -f ${GITEA_APP_INI} ]; then +if [ ! -f "${GITEA_APP_INI}" ]; then #Prepare config file folder - GITEA_APP_INI_DIR=$(dirname ${GITEA_APP_INI}) - mkdir -p ${GITEA_APP_INI_DIR} && chmod 0700 ${GITEA_APP_INI_DIR} - if [ ! -w ${GITEA_APP_INI_DIR} ]; then echo "${GITEA_APP_INI_DIR} is not writable"; exit 1; fi + GITEA_APP_INI_DIR=$(dirname "${GITEA_APP_INI}") + mkdir -p "${GITEA_APP_INI_DIR}" && chmod 0700 "${GITEA_APP_INI_DIR}" + if [ ! -w "${GITEA_APP_INI_DIR}" ]; then echo "${GITEA_APP_INI_DIR} is not writable"; exit 1; fi # Set INSTALL_LOCK to true only if SECRET_KEY is not empty and # INSTALL_LOCK is empty @@ -34,7 +34,7 @@ if [ ! -f ${GITEA_APP_INI} ]; then ROOT_URL=${ROOT_URL:-""} \ DISABLE_SSH=${DISABLE_SSH:-"false"} \ SSH_PORT=${SSH_PORT:-"2222"} \ - SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-$SSH_PORT} \ + SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-} \ DB_TYPE=${DB_TYPE:-"sqlite3"} \ DB_HOST=${DB_HOST:-"localhost:3306"} \ DB_NAME=${DB_NAME:-"gitea"} \ @@ -44,8 +44,8 @@ if [ ! -f ${GITEA_APP_INI} ]; then DISABLE_REGISTRATION=${DISABLE_REGISTRATION:-"false"} \ REQUIRE_SIGNIN_VIEW=${REQUIRE_SIGNIN_VIEW:-"false"} \ SECRET_KEY=${SECRET_KEY:-""} \ - envsubst < /etc/templates/app.ini > ${GITEA_APP_INI} + envsubst < /etc/templates/app.ini > "${GITEA_APP_INI}" fi # Replace app.ini settings with env variables in the form GITEA__SECTION_NAME__KEY_NAME -environment-to-ini --config ${GITEA_APP_INI} +environment-to-ini --config "${GITEA_APP_INI}" diff --git a/renovate.json5 b/renovate.json5 index c5d712dc0a..bf913011fb 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -18,6 +18,7 @@ "managerFilePatterns": ["/(^|/)Makefile$/"], "matchStrings": [ "[A-Z_]+_PACKAGE\\s*\\?=\\s*(?[^@\\s]+?)(?:/cmd/[^@/\\s]+)?@(?\\S+)\\s+# renovate: datasource=(?\\S+)", + "[A-Z_]+_IMAGE\\s*\\?=\\s*(?[^:\\s]+):(?[^@\\s]+)@(?sha256:[a-f0-9]+)\\s+# renovate: datasource=(?\\S+)", ], }, ], diff --git a/snap/part-gitea-build.sh b/snap/part-gitea-build.sh index 8388c16e06..005162488f 100755 --- a/snap/part-gitea-build.sh +++ b/snap/part-gitea-build.sh @@ -1,7 +1,7 @@ #!/bin/sh set -e -if [ ! -f go.mod -o ! -d snap ]; then +if [ ! -f go.mod ] || [ ! -d snap ]; then echo "This script should be run from the root of the gitea repository" exit 1 fi diff --git a/snap/part-gitea-pull.sh b/snap/part-gitea-pull.sh index 0d117bb4d1..83fde11a12 100755 --- a/snap/part-gitea-pull.sh +++ b/snap/part-gitea-pull.sh @@ -1,7 +1,7 @@ #!/bin/sh set -e -if [ ! -f go.mod -o ! -d snap ]; then +if [ ! -f go.mod ] || [ ! -d snap ]; then echo "This script should be run from the root of the gitea repository" exit 1 fi diff --git a/tools/lint-shell.sh b/tools/lint-shell.sh new file mode 100755 index 0000000000..dac838e833 --- /dev/null +++ b/tools/lint-shell.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail + +CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-docker}" +VERSION=$(echo "$SHELLCHECK_IMAGE" | sed -E 's/.*:v([0-9.]+)@.*/\1/') + +if hash shellcheck 2>/dev/null && shellcheck --version | grep -qx "version: $VERSION"; then + exec shellcheck --color=always "$@" +else + exec "$CONTAINER_RUNTIME" run --rm -v "$PWD":/mnt -w /mnt "$SHELLCHECK_IMAGE" --color=always "$@" +fi diff --git a/tools/test-e2e.sh b/tools/test-e2e.sh index d6c053ff1f..be3e848ee1 100755 --- a/tools/test-e2e.sh +++ b/tools/test-e2e.sh @@ -71,8 +71,10 @@ if [ "$CMD" = "install" ]; then if [ "$PLAYWRIGHT_MODE" = "local" ]; then # on GitHub Actions VMs, playwright's system deps are pre-installed if [ -z "${GITHUB_ACTIONS:-}" ]; then + # shellcheck disable=SC2086 # flag string pnpm exec playwright install --with-deps chromium firefox ${PLAYWRIGHT_FLAGS:-} else + # shellcheck disable=SC2086 # flag string pnpm exec playwright install chromium firefox ${PLAYWRIGHT_FLAGS:-} fi else From a290d81eabe60998ab84ac74a45489f8880b68d0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 22 May 2026 22:39:10 +0200 Subject: [PATCH 10/18] ci: cap Elasticsearch heap in db-tests (#37816) Elasticsearch JVM heap defaults to ~50% of detected host RAM, typically way too much for our little tests and it starved the other runner processes from memory. Co-authored-by: Claude (Opus 4.7) --- .github/workflows/pull-db-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index e4de01e711..1a3ce40035 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -119,6 +119,7 @@ jobs: env: discovery.type: single-node xpack.security.enabled: false + ES_JAVA_OPTS: "-Xms512m -Xmx512m" # reduce from ES default of 50% ports: - "9200:9200" meilisearch: @@ -192,6 +193,7 @@ jobs: env: discovery.type: single-node xpack.security.enabled: false + ES_JAVA_OPTS: "-Xms512m -Xmx512m" # reduce from ES default of 50% ports: - "9200:9200" smtpimap: From 502d3c1f9d1a15660818324c400b1a0fbb36e6a6 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 23 May 2026 03:09:18 +0200 Subject: [PATCH 11/18] ci: tweak `files-changed`, add `free-disk-space` (#37819) Reduces CI minutes consumption by narrowing the `files-changed` filters. - DB matrix (`pgsql`/`mysql`/`mssql`/`unit`) now runs only on real backend changes. `test-sqlite` stays gated on `actions`, so it remains the smoke check that validates CI-infra changes (composite-action edits, workflow edits, renovate action-pin bumps) without spinning up the full matrix. - Fix the `templates` filter: the SVG template linter is `tools/lint-templates-svg.ts`, so the `tools/lint-templates-*.js` glob matched nothing. - Add missed paths: `tsconfig.json` and `tools/generate-svg-vscode-extensions.json` to `frontend`, `eslint.json.config.ts` to `json`, and `.github/actions/docker-dryrun/**` to `docker`. --- This PR was written with the help of Claude Opus 4.7 --------- Signed-off-by: silverwind Co-authored-by: Claude (Opus 4.7) --- .github/actions/free-disk-space/action.yml | 9 +++++++++ .github/actions/go-setup/action.yml | 1 + .github/workflows/files-changed.yml | 6 +++++- .github/workflows/pull-db-tests.yml | 10 +++++----- 4 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 .github/actions/free-disk-space/action.yml diff --git a/.github/actions/free-disk-space/action.yml b/.github/actions/free-disk-space/action.yml new file mode 100644 index 0000000000..510b643a33 --- /dev/null +++ b/.github/actions/free-disk-space/action.yml @@ -0,0 +1,9 @@ +name: free-disk-space +description: Free space on / before large cache restores + +# Delete preinstalled toolchains which gitea doesn't use +runs: + using: composite + steps: + - shell: bash + run: sudo rm -rf /usr/local/lib/android /usr/local/.ghcup /opt/ghc /usr/share/dotnet diff --git a/.github/actions/go-setup/action.yml b/.github/actions/go-setup/action.yml index d6f88bbc6d..a99ef0629a 100644 --- a/.github/actions/go-setup/action.yml +++ b/.github/actions/go-setup/action.yml @@ -12,6 +12,7 @@ inputs: runs: using: composite steps: + - uses: ./.github/actions/free-disk-space - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index a7b116fec7..c17afbca97 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -69,6 +69,8 @@ jobs: - "*.ts" - "web_src/**" - "tools/generate-svg.ts" + - "tools/generate-svg-vscode-extensions.json" + - "tsconfig.json" - "assets/emoji.json" - "package.json" - "pnpm-lock.yaml" @@ -88,13 +90,14 @@ jobs: - "Makefile" templates: - - "tools/lint-templates-*.js" + - "tools/lint-templates-*.ts" - "templates/**/*.tmpl" - "pyproject.toml" - "uv.lock" docker: - ".github/workflows/pull-docker-dryrun.yml" + - ".github/actions/docker-dryrun/**" - "Dockerfile" - "Dockerfile.rootless" - "docker/**" @@ -122,6 +125,7 @@ jobs: json: - "**/*.json" - "**/*.json5" + - "eslint.json.config.ts" e2e: - "tests/e2e/**" diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 1a3ce40035..dd2c11a40b 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -15,7 +15,7 @@ jobs: uses: ./.github/workflows/files-changed.yml test-pgsql-shard-1: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' + if: needs.files-changed.outputs.backend == 'true' needs: files-changed runs-on: ubuntu-latest timeout-minutes: 50 @@ -51,7 +51,7 @@ jobs: run-migration: "true" test-pgsql-shard-2: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' + if: needs.files-changed.outputs.backend == 'true' needs: files-changed runs-on: ubuntu-latest timeout-minutes: 50 @@ -110,7 +110,7 @@ jobs: GOEXPERIMENT: test-unit: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' + if: needs.files-changed.outputs.backend == 'true' needs: files-changed runs-on: ubuntu-latest services: @@ -174,7 +174,7 @@ jobs: - run: make test-check test-mysql: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' + if: needs.files-changed.outputs.backend == 'true' needs: files-changed runs-on: ubuntu-latest services: @@ -220,7 +220,7 @@ jobs: TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" test-mssql: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' + if: needs.files-changed.outputs.backend == 'true' needs: files-changed runs-on: ubuntu-latest services: From d8a45db3dd053e738c9bafa4c9540fcac6ebd639 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 23 May 2026 01:10:13 +0000 Subject: [PATCH 12/18] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.json | 1 + 1 file changed, 1 insertion(+) diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 43f83c3423..3ef188568b 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -105,6 +105,7 @@ "copy_error": "Theip ar an gcóipeáil", "copy_type_unsupported": "Ní féidir an cineál comhaid seo a chóipeáil", "copy_filename": "Cóipeáil ainm comhaid", + "copy_output": "Cóipeáil aschur", "write": "Scríobh", "preview": "Réamhamharc", "loading": "Á lódáil...", From cdee9f5e10ef8c2c4448390828e22077bc67ebe8 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 23 May 2026 16:12:12 +0200 Subject: [PATCH 13/18] ci: sync PR labels from the conventional-commit title (#37784) Syncs `type/*` and `pr/breaking` labels from the PR title (Conventional Commits) and folds the existing title lint into the same workflow so labeling only runs once the title is valid. - `tools/pr-title.ts`: shared title parser and label mapping. - `tools/set-pr-labels.ts`: adds/removes labels via the GitHub API. `type/*` and `pr/breaking` are fully synced (added and removed); `skip-changelog` (chore/ci) and `topic/build` (build) are only added, never auto-removed, so manual labeling is preserved. - `pull-labeler.yml` now hosts `lint-pr-title` and `set-pr-labels` (`needs: lint-pr-title`) under `pull_request_target`, required so fork PRs get a writable token. Base-branch checkout only; no PR-head code runs in the elevated context. - Removes the superseded `pull-pr-title.yml` and the CI-only `lint-pr-title` Makefile target. --------- Signed-off-by: silverwind Co-authored-by: Claude Opus 4.7 Co-authored-by: silverwind Co-authored-by: TheFox0x7 --- .github/workflows/pull-labeler.yml | 31 ++++++- .github/workflows/pull-pr-title.yml | 31 ------- Makefile | 4 - tools/ci-tools.ts | 133 ++++++++++++++++++++++++++++ tools/lint-pr-title.ts | 19 ---- 5 files changed, 162 insertions(+), 56 deletions(-) delete mode 100644 .github/workflows/pull-pr-title.yml create mode 100644 tools/ci-tools.ts delete mode 100644 tools/lint-pr-title.ts diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml index b27fc32cdb..ff76285d97 100644 --- a/.github/workflows/pull-labeler.yml +++ b/.github/workflows/pull-labeler.yml @@ -1,8 +1,10 @@ name: labeler on: - pull_request_target: - types: [opened, synchronize, reopened] + # pull_request_target is required to label PRs from forks; jobs only use pinned + # actions or base-branch checkout, never PR-head code. + pull_request_target: # zizmor: ignore[dangerous-triggers] + types: [opened, synchronize, reopened, edited, ready_for_review] concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -18,3 +20,28 @@ jobs: - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 with: sync-labels: true + + pr-title: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + issues: write + steps: + # Base-branch checkout only: pull_request_target runs with elevated token; never run PR-head code here. + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.base.sha }} + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24 + # Labels are only synced after the title lints, so an invalid title never reaches the label diff. + - run: node ./tools/ci-tools.ts lint-pr-title + env: + PR_TITLE: ${{ github.event.pull_request.title }} + - run: node ./tools/ci-tools.ts set-pr-labels + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/pull-pr-title.yml b/.github/workflows/pull-pr-title.yml deleted file mode 100644 index 6dadcb7058..0000000000 --- a/.github/workflows/pull-pr-title.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: pr-title - -on: - pull_request: - types: - - opened - - edited - - reopened - - synchronize - - ready_for_review - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - lint-pr-title: - if: github.event.pull_request.draft == false - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - - run: make lint-pr-title - env: - PR_TITLE: ${{ github.event.pull_request.title }} diff --git a/Makefile b/Makefile index 52a266fa39..649465b2b4 100644 --- a/Makefile +++ b/Makefile @@ -321,10 +321,6 @@ lint-md: node_modules ## lint markdown files lint-md-fix: node_modules ## lint markdown files and fix issues pnpm exec markdownlint --fix *.md -.PHONY: lint-pr-title -lint-pr-title: ## lint PR title against Conventional Commits (set PR_TITLE=...) - @node ./tools/lint-pr-title.ts - .PHONY: lint-spell lint-spell: ## lint spelling @git ls-files $(SPELLCHECK_FILES) | xargs go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -error diff --git a/tools/ci-tools.ts b/tools/ci-tools.ts new file mode 100644 index 0000000000..4562643247 --- /dev/null +++ b/tools/ci-tools.ts @@ -0,0 +1,133 @@ +#!/usr/bin/env node +import {argv, env, exit} from 'node:process'; + +const allowedTypes = [ + 'build', + 'chore', + 'ci', + 'docs', + 'enhance', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test', +] as const; +type CommitType = typeof allowedTypes[number]; + +const allowedTypesList = allowedTypes.join(', '); +const titlePattern = new RegExp(`^(${allowedTypes.join('|')})(\\([\\w/.-]+\\))?(!)?: .+$`); + +function parsePrTitle(title: string): {type: CommitType; breaking: boolean} | null { + const match = titlePattern.exec(title); + return match ? {type: match[1] as CommitType, breaking: Boolean(match[3])} : null; +} + +const breakingLabel = 'pr/breaking'; + +// Mutually exclusive type labels, fully synced with the title type (added and removed). +const typeLabels: Partial> = { + feat: 'type/feature', + enhance: 'type/enhancement', + fix: 'type/bug', + docs: 'type/docs', + test: 'type/testing', +}; + +// Non-type labels, only added, never auto-removed, so manual labeling is not clobbered. +const extraLabels: Partial> = { + chore: 'skip-changelog', + ci: 'skip-changelog', + build: 'topic/build', +}; + +// Labels this tool may remove when the title no longer implies them. +const removableLabels = [...Object.values(typeLabels), breakingLabel]; + +function labelsForPrTitle(title: string): string[] { + const parsed = parsePrTitle(title); + if (!parsed) return []; + return [typeLabels[parsed.type], extraLabels[parsed.type], parsed.breaking ? breakingLabel : undefined] + .filter((label): label is string => label !== undefined); +} + +// Command: validate PR_TITLE against the allowed Conventional Commits format. +function lintPrTitle(): void { + if (!env.PR_TITLE) { + console.error('Missing PR_TITLE'); + exit(1); + } + if (!parsePrTitle(env.PR_TITLE)) { + console.error(`Invalid PR title: ${env.PR_TITLE}`); + console.error('Expected format: type(scope): subject (scope optional, append "!" for breaking changes)'); + console.error(`Allowed types: ${allowedTypesList}`); + exit(1); + } +} + +// Command: sync the title-derived labels onto the PR via the GitHub API. +async function setPrLabels(): Promise { + if (!env.PR_TITLE || !env.GITHUB_TOKEN || !env.GITHUB_REPOSITORY || !env.PR_NUMBER) { + console.error('set-pr-labels requires PR_TITLE, GITHUB_TOKEN, GITHUB_REPOSITORY and PR_NUMBER'); + exit(1); + } + + const labelsUrl = `https://api.github.com/repos/${env.GITHUB_REPOSITORY}/issues/${env.PR_NUMBER}/labels`; + + async function request(url: string, method = 'GET', body?: unknown): Promise { + const response = await fetch(url, { + method, + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${env.GITHUB_TOKEN}`, + 'X-GitHub-Api-Version': '2022-11-28', + ...(body ? {'Content-Type': 'application/json'} : {}), + }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!response.ok) { + throw new Error(`GitHub API ${method} ${url} failed (${response.status}): ${await response.text()}`); + } + return response; + } + + const desired = labelsForPrTitle(env.PR_TITLE); + const response = await request(`${labelsUrl}?per_page=100`); + const current = ((await response.json()) as Array<{name: string}>).map((label) => label.name); + + const toAdd = desired.filter((name) => !current.includes(name)); + const toRemove = removableLabels.filter((name) => current.includes(name) && !desired.includes(name)); + + if (toAdd.length) { + await request(labelsUrl, 'POST', {labels: toAdd}); + console.info(`Added labels: ${toAdd.join(', ')}`); + } + for (const name of toRemove) { + await request(`${labelsUrl}/${encodeURIComponent(name)}`, 'DELETE'); + console.info(`Removed label: ${name}`); + } + if (!toAdd.length && !toRemove.length) { + console.info('PR labels already in sync'); + } +} + +const commands: Record void | Promise> = { + 'lint-pr-title': lintPrTitle, + 'set-pr-labels': setPrLabels, +}; + +const command = argv[2]; +const handler = commands[command]; +if (!handler) { + console.error(`Usage: ci-tools.ts <${Object.keys(commands).join('|')}>`); + exit(1); +} + +try { + await handler(); +} catch (error) { + console.error(error instanceof Error ? error.message : error); + exit(1); +} diff --git a/tools/lint-pr-title.ts b/tools/lint-pr-title.ts deleted file mode 100644 index a63defe972..0000000000 --- a/tools/lint-pr-title.ts +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env node -import {env, exit} from 'node:process'; - -const allowedTypes = 'build, chore, ci, docs, enhance, feat, fix, perf, refactor, revert, style, test'; -const title = env.PR_TITLE; - -if (!title) { - console.error('Missing PR_TITLE'); - exit(1); -} - -const validTitlePattern = new RegExp(`^(${allowedTypes.replaceAll(', ', '|')})(\\([\\w.-]+\\))?(!)?: .+$`); - -if (!validTitlePattern.test(title)) { - console.error(`Invalid PR title: ${title}`); - console.error('Expected format: type(scope): subject'); - console.error(`Allowed types: ${allowedTypes}`); - exit(1); -} From c9ce7e447c0168a217b79b4c25d751a7317519ae Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 23 May 2026 20:51:03 +0200 Subject: [PATCH 14/18] feat(actions): add before/after to PR synchronize event payload (#37827) ## Summary - Add `before` and `after` fields to `PullRequestPayload` for `synchronize` events - Thread push old/new commit SHAs through the PR synchronize notifier path (regular and Agit flows) - Populate the fields in webhook and Actions event payloads so workflows can access them via `github.event.before` and `github.event.after` Fixes #33395 --------- Co-authored-by: Cursor --- modules/structs/hook.go | 4 +++ modules/structs/hook_test.go | 59 ++++++++++++++++++++++++++++++++++++ services/actions/notifier.go | 4 ++- services/agit/agit.go | 2 +- services/notify/notifier.go | 2 +- services/notify/notify.go | 4 +-- services/notify/null.go | 2 +- services/pull/pull.go | 2 +- services/webhook/notifier.go | 4 ++- 9 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 modules/structs/hook_test.go diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 99c1535155..176b913515 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -434,6 +434,10 @@ type ChangesPayload struct { type PullRequestPayload struct { // The action performed on the pull request Action HookIssueAction `json:"action"` + // The SHA of the most recent commit on the PR head branch before the push + Before string `json:"before,omitempty"` + // The SHA of the most recent commit on the PR head branch after the push + After string `json:"after,omitempty"` // The index number of the pull request Index int64 `json:"number"` // Changes made to the pull request (for edit actions) diff --git a/modules/structs/hook_test.go b/modules/structs/hook_test.go new file mode 100644 index 0000000000..a593b4eb98 --- /dev/null +++ b/modules/structs/hook_test.go @@ -0,0 +1,59 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +import ( + "testing" + + "code.gitea.io/gitea/modules/json" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPullRequestPayloadSynchronizeBeforeAfter(t *testing.T) { + payload := &PullRequestPayload{ + Action: HookIssueSynchronized, + Before: "1111111111111111111111111111111111111111", + After: "2222222222222222222222222222222222222222", + Index: 12, + } + + data, err := json.Marshal(payload) + require.NoError(t, err) + + assert.JSONEq(t, `{ + "action": "synchronized", + "before": "1111111111111111111111111111111111111111", + "after": "2222222222222222222222222222222222222222", + "number": 12, + "commit_id": "", + "pull_request": null, + "repository": null, + "requested_reviewer": null, + "review": null, + "sender": null + }`, string(data)) +} + +func TestPullRequestPayloadNonSynchronizeOmitsBeforeAfter(t *testing.T) { + payload := &PullRequestPayload{ + Action: HookIssueOpened, + Index: 12, + } + + data, err := json.Marshal(payload) + require.NoError(t, err) + + assert.JSONEq(t, `{ + "action": "opened", + "number": 12, + "commit_id": "", + "pull_request": null, + "repository": null, + "requested_reviewer": null, + "review": null, + "sender": null + }`, string(data)) +} diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 4b2e87afad..0069da8c6b 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -687,7 +687,7 @@ func (n *actionsNotifier) AutoMergePullRequest(ctx context.Context, doer *user_m n.MergePullRequest(ctx, doer, pr) } -func (n *actionsNotifier) PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { +func (n *actionsNotifier) PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, before, after string) { ctx = withMethod(ctx, "PullRequestSynchronized") if err := pr.LoadIssue(ctx); err != nil { @@ -703,6 +703,8 @@ func (n *actionsNotifier) PullRequestSynchronized(ctx context.Context, doer *use newNotifyInput(pr.Issue.Repo, doer, webhook_module.HookEventPullRequestSync). WithPayload(&api.PullRequestPayload{ Action: api.HookIssueSynchronized, + Before: before, + After: after, Index: pr.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), Repository: convert.ToRepo(ctx, pr.Issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}), diff --git a/services/agit/agit.go b/services/agit/agit.go index c7c46651c0..66f0d1c0db 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -286,7 +286,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. } else if commentCreated { notify_service.PullRequestPushCommits(ctx, pusher, pr, comment) } - notify_service.PullRequestSynchronized(ctx, pusher, pr) + notify_service.PullRequestSynchronized(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i]) results = append(results, private.HookProcReceiveRefResult{ OldOID: oldCommitID, diff --git a/services/notify/notifier.go b/services/notify/notifier.go index 875a70e564..bc990e9cb6 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -45,7 +45,7 @@ type Notifier interface { NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) - PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) + PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, before, after string) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) PullRequestCodeComment(ctx context.Context, pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User) PullRequestChangeTargetBranch(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) diff --git a/services/notify/notify.go b/services/notify/notify.go index 152d53b01c..aee2047e1c 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -120,9 +120,9 @@ func NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions } // PullRequestSynchronized notifies Synchronized pull request -func PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { +func PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, before, after string) { for _, notifier := range notifiers { - notifier.PullRequestSynchronized(ctx, doer, pr) + notifier.PullRequestSynchronized(ctx, doer, pr, before, after) } } diff --git a/services/notify/null.go b/services/notify/null.go index c3085d7c9e..f10a132da7 100644 --- a/services/notify/null.go +++ b/services/notify/null.go @@ -63,7 +63,7 @@ func (*NullNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model. } // PullRequestSynchronized places a place holder function -func (*NullNotifier) PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { +func (*NullNotifier) PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, before, after string) { } // PullRequestChangeTargetBranch places a place holder function diff --git a/services/pull/pull.go b/services/pull/pull.go index 4f76df31da..64d9f60dae 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -479,7 +479,7 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) { } } - notify_service.PullRequestSynchronized(ctx, opts.Doer, pr) + notify_service.PullRequestSynchronized(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID) } } } diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 7627935a32..d53dfaaa6a 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -818,7 +818,7 @@ func (m *webhookNotifier) CreateRef(ctx context.Context, pusher *user_model.User } } -func (m *webhookNotifier) PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { +func (m *webhookNotifier) PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, before, after string) { if err := pr.LoadIssue(ctx); err != nil { log.Error("LoadIssue: %v", err) return @@ -830,6 +830,8 @@ func (m *webhookNotifier) PullRequestSynchronized(ctx context.Context, doer *use if err := PrepareWebhooks(ctx, EventSource{Repository: pr.Issue.Repo}, webhook_module.HookEventPullRequestSync, &api.PullRequestPayload{ Action: api.HookIssueSynchronized, + Before: before, + After: after, Index: pr.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pr, doer), Repository: convert.ToRepo(ctx, pr.Issue.Repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), From 8d6124a68afa2a55f6c25ef37fa04bdf0f9b5329 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 23 May 2026 22:46:36 +0200 Subject: [PATCH 15/18] ci: FIX sync PR labels from the conventional-commit title (#37784) (#37825) If this also doesnt work we need to revert it ig --------- Co-authored-by: silverwind Co-authored-by: Claude (Opus 4.7) --- .github/workflows/pull-labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml index ff76285d97..34395c8d9e 100644 --- a/.github/workflows/pull-labeler.yml +++ b/.github/workflows/pull-labeler.yml @@ -27,7 +27,7 @@ jobs: timeout-minutes: 5 permissions: contents: read - issues: write + pull-requests: write steps: # Base-branch checkout only: pull_request_target runs with elevated token; never run PR-head code here. - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 From 7d8bfb8dc6d9dc266559775a882ff6f2d716e7bd Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 23 May 2026 23:04:54 +0200 Subject: [PATCH 16/18] test: run `TestAPIRepoMigrate` offline via a local clone source (#37817) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `TestAPIRepoMigrate` migrated from `https://github.com/go-gitea/test_repo.git`, so it required internet access, was slow, and could hit GitHub rate limits. It now clones a local fixture repo (`user2/repo1`) served by the `onGiteaRun` test server, split into two subtests: - `Permitted` (`AllowLocalNetworks=true`) — the success/permission cases, cloning the local repo. - `DisallowedHost` (`AllowLocalNetworks=false`) — the private-IP rejection cases. The split is needed because those two settings are mutually exclusive. The clone address is built from the live listener (`u`) so it can't drift from the bound host/port. The permission matrix and disallowed-host assertions are unchanged. Test is now roughly 2.5 times as fast with while asserting the same as before without a GitHub dependency. --- This PR was written with the help of Claude Opus 4.7 Co-authored-by: Claude (Opus 4.7) Co-authored-by: Nicolas --- tests/integration/api_repo_test.go | 84 ++++++++++++++++-------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index 2b19c3452d..406c82e475 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -351,48 +351,52 @@ func TestAPIGetRepoByIDUnauthorized(t *testing.T) { } func TestAPIRepoMigrate(t *testing.T) { - testCases := []struct { - ctxUserID, userID int64 - cloneURL, repoName string - expectedStatus int - }{ - {ctxUserID: 1, userID: 2, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-admin", expectedStatus: http.StatusCreated}, - {ctxUserID: 2, userID: 2, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-own", expectedStatus: http.StatusCreated}, - {ctxUserID: 2, userID: 1, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-bad", expectedStatus: http.StatusForbidden}, - {ctxUserID: 2, userID: 3, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-org", expectedStatus: http.StatusCreated}, - {ctxUserID: 2, userID: 6, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-bad-org", expectedStatus: http.StatusForbidden}, - {ctxUserID: 2, userID: 3, cloneURL: "https://localhost:3000/user/test_repo.git", repoName: "private-ip", expectedStatus: http.StatusUnprocessableEntity}, - {ctxUserID: 2, userID: 3, cloneURL: "https://10.0.0.1/user/test_repo.git", repoName: "private-ip", expectedStatus: http.StatusUnprocessableEntity}, - } + onGiteaRun(t, func(t *testing.T, u *url.URL) { + // migrate from a local fixture repo (user2/repo1) via the live listener so the test runs offline + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + cloneAddr := fmt.Sprintf("%s%s/%s.git", u.String(), repo1.OwnerName, repo1.Name) - defer tests.PrepareTestEnv(t)() - defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, false)() - require.NoError(t, migrations.Init()) - - for _, testCase := range testCases { - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID}) - session := loginUser(t, user.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", &api.MigrateRepoOptions{ - CloneAddr: testCase.cloneURL, - RepoOwnerID: testCase.userID, - RepoName: testCase.repoName, - }).AddTokenAuth(token) - resp := MakeRequest(t, req, NoExpectedStatus) - if resp.Code == http.StatusUnprocessableEntity { - respJSON := DecodeJSON(t, resp, map[string]string{}) - switch respJSON["message"] { - case "Remote visit addressed rate limitation.": - t.Log("test hit github rate limitation") - case "You can not import from disallowed hosts.": - assert.Equal(t, "private-ip", testCase.repoName) - default: - assert.FailNow(t, "unexpected error", "unexpected error '%v' on url '%s'", respJSON["message"], testCase.cloneURL) + t.Run("Permitted", func(t *testing.T) { + // migrations.Init builds the host allowlist from AllowLocalNetworks, so set it first + defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() + require.NoError(t, migrations.Init()) + for _, testCase := range []struct { + ctxUserID, ownerID int64 + repoName string + expectedStatus int + }{ + {ctxUserID: 1, ownerID: 2, repoName: "git-admin", expectedStatus: http.StatusCreated}, + {ctxUserID: 2, ownerID: 2, repoName: "git-own", expectedStatus: http.StatusCreated}, + {ctxUserID: 2, ownerID: 1, repoName: "git-bad", expectedStatus: http.StatusForbidden}, + {ctxUserID: 2, ownerID: 3, repoName: "git-org", expectedStatus: http.StatusCreated}, + {ctxUserID: 2, ownerID: 6, repoName: "git-bad-org", expectedStatus: http.StatusForbidden}, + } { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID}) + token := getTokenForLoggedInUser(t, loginUser(t, user.Name), auth_model.AccessTokenScopeWriteRepository) + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", &api.MigrateRepoOptions{ + CloneAddr: cloneAddr, + RepoOwnerID: testCase.ownerID, + RepoName: testCase.repoName, + }).AddTokenAuth(token) + MakeRequest(t, req, testCase.expectedStatus) } - } else { - assert.Equal(t, testCase.expectedStatus, resp.Code) - } - } + }) + + t.Run("DisallowedHost", func(t *testing.T) { + defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, false)() + require.NoError(t, migrations.Init()) + token := getTokenForLoggedInUser(t, loginUser(t, "user2"), auth_model.AccessTokenScopeWriteRepository) + for _, cloneURL := range []string{"https://localhost:3000/user/test_repo.git", "https://10.0.0.1/user/test_repo.git"} { + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", &api.MigrateRepoOptions{ + CloneAddr: cloneURL, + RepoOwnerID: 3, + RepoName: "private-ip", + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusUnprocessableEntity) + assert.Equal(t, "You can not import from disallowed hosts.", DecodeJSON(t, resp, map[string]string{})["message"]) + } + }) + }) } func TestAPIRepoMigrateConflict(t *testing.T) { From 748d4a80403cade23af7e60bf3af3824579796d5 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sun, 24 May 2026 01:15:54 +0000 Subject: [PATCH 17/18] [skip ci] Updated translations via Crowdin --- options/locale/locale_ja-JP.json | 210 ++++++++++++++++++++++++------- 1 file changed, 164 insertions(+), 46 deletions(-) diff --git a/options/locale/locale_ja-JP.json b/options/locale/locale_ja-JP.json index ffbd8c0d9e..4af9d945b2 100644 --- a/options/locale/locale_ja-JP.json +++ b/options/locale/locale_ja-JP.json @@ -81,9 +81,11 @@ "retry": "再試行", "rerun": "再実行", "rerun_all": "すべてのジョブを再実行", + "rerun_failed": "失敗したジョブを再実行", "save": "保存", "add": "追加", "add_all": "すべて追加", + "dismiss": "閉じる", "remove": "除去", "remove_all": "すべて除去", "remove_label_str": "アイテム「%s」を削除", @@ -103,6 +105,7 @@ "copy_error": "コピーに失敗しました", "copy_type_unsupported": "このファイルタイプはコピーできません", "copy_filename": "ファイル名をコピー", + "copy_output": "出力をコピー", "write": "書き込み", "preview": "プレビュー", "loading": "読み込み中…", @@ -120,6 +123,7 @@ "unpin": "ピン留め解除", "artifacts": "成果物", "expired": "期限切れ", + "artifact_expires_at": "保存期限 %s", "confirm_delete_artifact": "アーティファクト '%s' を削除してよろしいですか?", "archived": "アーカイブ", "concept_system_global": "グローバル", @@ -167,9 +171,12 @@ "search.exact_tooltip": "検索語と完全に一致する結果だけを含めます", "search.repo_kind": "リポジトリを検索…", "search.user_kind": "ユーザーを検索…", + "search.badge_kind": "バッジを検索…", "search.org_kind": "組織を検索…", "search.team_kind": "チームを検索…", "search.code_kind": "コードを検索…", + "search.code_empty": "コード検索を開始。", + "search.code_empty_description": "コード全体から検索するキーワードを入力してください。", "search.code_search_unavailable": "コード検索は現在利用できません。 サイト管理者にお問い合わせください。", "search.code_search_by_git_grep": "現在のコード検索は \"git grep\" によって行われています。 サイト管理者がリポジトリインデクサーを有効にすれば、より優れた結果が得られる可能性があります。", "search.package_kind": "パッケージを検索…", @@ -210,11 +217,15 @@ "editor.buttons.switch_to_legacy.tooltip": "レガシーエディタを使用する", "editor.buttons.enable_monospace_font": "等幅フォントを有効にする", "editor.buttons.disable_monospace_font": "等幅フォントを無効にする", + "editor.code_editor.command_palette": "コマンドパレット", + "editor.code_editor.find": "検索", + "editor.code_editor.placeholder": "ファイルの内容をここに入力", "filter.string.asc": "A–Z", "filter.string.desc": "Z–A", "error.occurred": "エラーが発生しました", "error.report_message": "Gitea のバグが疑われる場合は、GitHubでIssueを検索して、見つからなければ新しいIssueを作成してください。", "error.not_found": "ターゲットが見つかりませんでした。", + "error.permission_denied": "権限がありません。", "error.network_error": "ネットワークエラー", "startpage.app_desc": "自分で立てる、超簡単 Git サービス", "startpage.install": "簡単インストール", @@ -261,7 +272,7 @@ "install.lfs_path": "Git LFSルートパス", "install.lfs_path_helper": "Git LFSで管理するファイルが、このディレクトリに保存されます。 空欄にするとGit LFSを無効にします。", "install.run_user": "実行ユーザー名", - "install.run_user_helper": "オペレーティングシステム上のユーザー名です。 Giteaをこのユーザーとして実行します。 このユーザーはリポジトリルートパスへのアクセス権を持っている必要があります。", + "install.run_user_helper": "Giteaを実行しているオペレーティングシステム上のユーザー名です。 このユーザーにはデータパスへの書き込み権限が必要です。 この値は自動的に検出されたもので、ここでは変更できません。 別のユーザーを使用するには、そのアカウントからGiteaを再起動してください。", "install.domain": "サーバードメイン", "install.domain_helper": "サーバーのドメインまたはホストアドレス。", "install.ssh_port": "SSHサーバーのポート", @@ -284,12 +295,6 @@ "install.register_confirm": "登録にはメールによる確認が必要", "install.mail_notify": "メール通知を有効にする", "install.server_service_title": "サーバーと外部サービスの設定", - "install.offline_mode": "ローカルモードを有効にする", - "install.offline_mode_popup": "外のCDNサービスを使わず、すべてのリソースを自前で提供します。", - "install.disable_gravatar": "Gravatarを無効にする", - "install.disable_gravatar_popup": "Gravatarと外のアバターソースを無効にします。 アバターをローカルにアップロードしていないユーザーには、デフォルトのアバターが使用されます。", - "install.federated_avatar_lookup": "フェデレーテッド・アバターを有効にする", - "install.federated_avatar_lookup_popup": "Libravatarを使用したフェデレーテッド・アバター検索を有効にします。", "install.disable_registration": "セルフ登録を無効にする", "install.disable_registration_popup": "ユーザーのセルフ登録を無効にします。 新しいユーザーアカウントを作成できるのは管理者だけとなります。", "install.allow_only_external_registration_popup": "外部サービスを使用した登録のみを許可", @@ -309,12 +314,10 @@ "install.admin_email": "メールアドレス", "install.install_btn_confirm": "Giteaをインストール", "install.test_git_failed": "'git'コマンドが確認できません: %v", - "install.sqlite3_not_available": "GiteaのこのバージョンはSQLite3をサポートしていません。 公式のバイナリ版を %s からダウンロードしてください。 ('gobuild'版でないもの)", "install.invalid_db_setting": "データベース設定が無効です: %v", "install.invalid_db_table": "データベーステーブルの \"%s\" が無効です: %v", "install.invalid_repo_path": "リポジトリのルートパスが無効です: %v", "install.invalid_app_data_path": "アプリのデータパス (APP_DATA_PATH) が無効です: %v", - "install.run_user_not_match": "実行ユーザー名が、現在のユーザー名ではありません: %s -> %s", "install.internal_token_failed": "内部トークンの生成に失敗しました: %v", "install.secret_key_failed": "シークレットキーの生成に失敗しました: %v", "install.save_config_failed": "設定ファイルの保存に失敗しました: %v", @@ -547,6 +550,7 @@ "form.glob_pattern_error": "のglobパターンが不正です: %s.", "form.regex_pattern_error": "の正規表現パターンが不正です: %s.", "form.username_error": "は、英数字('0-9','a-z','A-Z')、ダッシュ('-')、アンダースコア('_')、ドット('.')だけを含めることができます。 先頭と末尾は英数字以外の文字にはできません。 また、連続した英数字以外の文字も許されません。", + "form.invalid_slug_error": " は無効です。", "form.invalid_group_team_map_error": "のマッピングが無効です: %s", "form.unknown_error": "不明なエラー:", "form.captcha_incorrect": "CAPTCHAコードが正しくありません。", @@ -635,14 +639,8 @@ "user.block.unblock.failure": "ユーザーのブロック解除に失敗しました: %s", "user.block.blocked": "あなたはこのユーザーをブロックしています。", "user.block.title": "ユーザーをブロックする", - "user.block.info": "ユーザーをブロックすると、そのユーザーは、プルリクエストやイシューの作成、コメントの投稿など、リポジトリに対する操作ができなくなります。 ユーザーのブロックについてはよく確認してください。", - "user.block.info_1": "ユーザーをブロックすることで、あなたのアカウントとあなたのリポジトリに対する以下の行為を阻止します:", - "user.block.info_2": "あなたのアカウントのフォロー", - "user.block.info_3": "あなたのユーザー名で@メンションして通知を送ること", - "user.block.info_4": "そのユーザーのリポジトリに、あなたを共同作業者として招待すること", - "user.block.info_5": "リポジトリへの、スター、フォーク、ウォッチ", - "user.block.info_6": "イシューやプルリクエストの作成、コメント投稿", - "user.block.info_7": "イシューやプルリクエストでの、あなたのコメントに対するリアクションの送信", + "user.block.info": "ユーザーをブロックすると、そのユーザーは、プルリクエストやイシューの作成、コメントの投稿など、リポジトリに対する操作ができなくなります。", + "user.block.info.docs": "ユーザーのブロックについて詳しくはこちらをご覧ください。", "user.block.user_to_block": "ブロックするユーザー", "user.block.note": "メモ", "user.block.note.title": "メモ(任意):", @@ -650,6 +648,7 @@ "user.block.note.edit": "メモを編集", "user.block.list": "ブロックしたユーザー", "user.block.list.none": "ブロックしているユーザーはいません。", + "settings.general": "一般", "settings.profile": "プロフィール", "settings.account": "アカウント", "settings.appearance": "外観", @@ -970,7 +969,6 @@ "repo.visibility_description": "オーナー、または権限を持つ組織のメンバーだけが、リポジトリを見ることができます。", "repo.visibility_helper": "リポジトリをプライベートにする", "repo.visibility_helper_forced": "サイト管理者の設定により、新しいリポジトリは強制的にプライベートになります。", - "repo.visibility_fork_helper": "(この変更はすべてのフォークに適用されます)", "repo.clone_helper": "クローンに関してお困りであればヘルプを参照しましょう。", "repo.fork_repo": "リポジトリをフォーク", "repo.fork_from": "フォーク元", @@ -1042,6 +1040,7 @@ "repo.forks": "フォーク", "repo.stars": "スター", "repo.reactions_more": "さらに %d 件", + "repo.reactions": "リアクション", "repo.unit_disabled": "サイト管理者がこのリポジトリセクションを無効にしています。", "repo.language_other": "その他", "repo.adopt_search": "未登録リポジトリを探すユーザー名を入力… (空ですべてを探索)", @@ -1062,8 +1061,8 @@ "repo.transfer.accept_desc": "\"%s\" に移転", "repo.transfer.reject": "移転を拒否", "repo.transfer.reject_desc": "\"%s\" への移転をキャンセル", - "repo.transfer.no_permission_to_accept": "この移転を承認する権限がありません。", - "repo.transfer.no_permission_to_reject": "この移転を拒否する権限がありません。", + "repo.transfer.is_transferring": "移転処理中…", + "repo.transfer.is_transferring_prompt": "リポジトリは %s に移転処理中です", "repo.desc.private": "プライベート", "repo.desc.public": "公開", "repo.desc.public_access": "公開アクセス", @@ -1214,7 +1213,7 @@ "repo.ambiguous_runes_description": "このファイルには、他の文字と見間違える可能性があるUnicode文字が含まれています。 それが意図的なものと考えられる場合は、この警告を無視して構いません。 それらの文字を表示するにはエスケープボタンを使用します。", "repo.invisible_runes_line": "この行には不可視のUnicode文字があります", "repo.ambiguous_runes_line": "この行には曖昧(ambiguous)なUnicode文字があります", - "repo.ambiguous_character": "%[1]c [U+%04[1]X] は %[2]c [U+%04[2]X] と混同するおそれがあります", + "repo.ambiguous_character": "%[1]s は %[2]s と混同するおそれがあります", "repo.escape_control_characters": "エスケープ", "repo.unescape_control_characters": "エスケープ解除", "repo.file_copy_permalink": "パーマリンクをコピー", @@ -1307,9 +1306,9 @@ "repo.editor.upload_file_is_locked": "ファイル \"%s\" は %s がロックしています。", "repo.editor.upload_files_to_dir": "\"%s\" にファイルをアップロード", "repo.editor.cannot_commit_to_protected_branch": "保護されたブランチ \"%s\" にコミットすることはできません。", - "repo.editor.no_commit_to_branch": "ブランチに直接コミットすることはできません、なぜなら:", + "repo.editor.no_commit_to_branch": "ブランチに直接コミットすることは許可されません、なぜなら:", "repo.editor.user_no_push_to_branch": "ユーザーはブランチにプッシュできません", - "repo.editor.require_signed_commit": "ブランチでは署名されたコミットが必須です", + "repo.editor.require_signed_commit": "ブランチには署名済みコミットが必要です", "repo.editor.cherry_pick": "チェリーピック %s:", "repo.editor.revert": "リバート %s:", "repo.editor.failed_to_commit": "変更のコミットに失敗しました。", @@ -1323,7 +1322,7 @@ "repo.commits.desc": "ソースコードの変更履歴を参照します。", "repo.commits.commits": "コミット", "repo.commits.no_commits": "共通のコミットはありません。 \"%s\" と \"%s\" の履歴はすべて異なっています。", - "repo.commits.nothing_to_compare": "二つのブランチは同じ内容です。", + "repo.commits.nothing_to_compare": "差分はありません。", "repo.commits.search.tooltip": "キーワード \"author:\"、\"committer:\"、\"after:\"、\"before:\" を付けて指定できます。 例 \"revert author:Alice before:2019-01-13\"", "repo.commits.search_branch": "このブランチ", "repo.commits.search_all": "すべてのブランチ", @@ -1355,10 +1354,13 @@ "repo.projects.desc": "プロジェクトでイシューとプルリクエストを管理します。", "repo.projects.description": "説明 (オプション)", "repo.projects.description_placeholder": "説明", + "repo.projects.empty": "プロジェクトはまだありません。", + "repo.projects.empty_description": "イシューとプルリクエストを整理するために、プロジェクトを作成してください。", "repo.projects.create": "プロジェクトを作成", "repo.projects.title": "タイトル", "repo.projects.new": "新しいプロジェクト", "repo.projects.new_subheader": "作業の調整・追跡・更新をひとつの場所で行い、プロジェクトの透明性と良好な進捗を維持します。", + "repo.projects.no_results": "検索条件に一致するプロジェクトはありません。", "repo.projects.create_success": "プロジェクト \"%s\" を作成しました。", "repo.projects.deletion": "プロジェクトの削除", "repo.projects.deletion_desc": "プロジェクトを削除し、関連するすべてのイシューから除去します。続行しますか?", @@ -1383,6 +1385,7 @@ "repo.projects.column.delete": "列を削除", "repo.projects.column.deletion_desc": "プロジェクト列を削除すると、関連するすべてのイシューがデフォルトの列に移動します。 続行しますか?", "repo.projects.column.color": "カラー", + "repo.projects.column": "列", "repo.projects.open": "オープン", "repo.projects.close": "クローズ", "repo.projects.column.assigned_to": "担当", @@ -1400,11 +1403,12 @@ "repo.issues.new": "新しいイシュー", "repo.issues.new.title_empty": "タイトルは空にできません", "repo.issues.new.labels": "ラベル", - "repo.issues.new.no_label": "ラベルなし", + "repo.issues.new.no_labels": "ラベルなし", "repo.issues.new.clear_labels": "ラベルをクリア", "repo.issues.new.projects": "プロジェクト", "repo.issues.new.clear_projects": "プロジェクトをクリア", "repo.issues.new.no_projects": "プロジェクトなし", + "repo.issues.new.no_column": "列なし", "repo.issues.new.open_projects": "オープン中のプロジェクト", "repo.issues.new.closed_projects": "クローズ済みプロジェクト", "repo.issues.new.no_items": "項目なし", @@ -1524,16 +1528,18 @@ "repo.issues.commented_at": "がコメント %s", "repo.issues.delete_comment_confirm": "このコメントを削除してよろしいですか?", "repo.issues.context.copy_link": "リンクをコピー", + "repo.issues.context.copy_source": "ソースをコピー", "repo.issues.context.quote_reply": "引用して返信", "repo.issues.context.reference_issue": "新しいイシューから参照", "repo.issues.context.edit": "編集", "repo.issues.context.delete": "削除", "repo.issues.no_content": "説明はありません。", + "repo.issues.comment_no_content": "コメントはありません。", "repo.issues.close": "イシューをクローズ", "repo.issues.comment_pull_merged_at": "がコミット %[1]s を %[2]s にマージ %[3]s", "repo.issues.comment_manually_pull_merged_at": "がコミット %[1]s を %[2]s に手動マージ %[3]s", "repo.issues.close_comment_issue": "コメントしてクローズ", - "repo.issues.reopen_issue": "再オープンする", + "repo.issues.reopen_issue": "イシューを再オープン", "repo.issues.reopen_comment_issue": "コメントして再オープン", "repo.issues.create_comment": "コメントする", "repo.issues.comment.blocked_user": "投稿者またはリポジトリのオーナーがあなたをブロックしているため、コメントの作成や編集はできません。", @@ -1778,8 +1784,9 @@ "repo.pulls.select_commit_hold_shift_for_range": "コミットを選択。シフトを押しながらクリックで範囲選択。", "repo.pulls.review_only_possible_for_full_diff": "すべての差分を表示しているときだけレビューが可能です", "repo.pulls.filter_changes_by_commit": "コミットで絞り込み", - "repo.pulls.nothing_to_compare": "同じブランチ同士のため、 プルリクエストを作成する必要がありません。", - "repo.pulls.nothing_to_compare_have_tag": "選択したブランチ/タグは同一のものです。", + "repo.pulls.nothing_to_compare": "差分がないため、 プルリクエストを作成する必要はありません。", + "repo.pulls.no_common_history": "これらのブランチには共通のマージベースがありません。 別のベースブランチまたは比較ブランチを選択してください。", + "repo.pulls.nothing_to_compare_have_tag": "選択したブランチ/タグの間で差分がありません。", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "これらのブランチは内容が同じです。 空のプルリクエストになります。", "repo.pulls.has_pull_request": "同じブランチのプルリクエストはすでに存在します: %[2]s#%[3]d", "repo.pulls.create": "プルリクエストを作成", @@ -1813,6 +1820,7 @@ "repo.pulls.required_status_check_failed": "いくつかの必要なステータスチェックが成功していません。", "repo.pulls.required_status_check_missing": "必要なチェックがいくつか抜けています。", "repo.pulls.required_status_check_administrator": "管理者であるため、このプルリクエストをマージすることは可能です。", + "repo.pulls.required_status_check_bypass_allowlist": "あなたには、このマージでブランチ保護ルールのバイパスが許可されています。", "repo.pulls.blocked_by_approvals": "このプルリクエストはまだ必要な承認数を満たしていません。 公式の承認を %[1]d / %[2]d 得ています。", "repo.pulls.blocked_by_approvals_whitelisted": "このプルリクエストはまだ必要な承認数を満たしていません。 許可リストのユーザーまたはチームからの承認を %[1]d / %[2]d 得ています。", "repo.pulls.blocked_by_rejection": "このプルリクエストは公式レビューアにより変更要請されています。", @@ -1844,7 +1852,8 @@ "repo.pulls.fast_forward_only_merge_pull_request": "ファストフォワードのみ", "repo.pulls.merge_manually": "手動マージ済みにする", "repo.pulls.merge_commit_id": "マージコミットID", - "repo.pulls.require_signed_wont_sign": "ブランチでは署名されたコミットが必須ですが、このマージでは署名がされません", + "repo.pulls.require_signed_wont_sign": "ブランチには署名済みコミットが必要ですが、このマージでは署名がされません", + "repo.pulls.require_signed_head_commits_unverified": "ブランチには署名済みコミットが必要ですが、このプルリクエストに対する 1つ以上のコミットは検証されていません", "repo.pulls.invalid_merge_option": "このプルリクエストでは、指定したマージ方法は使えません。", "repo.pulls.merge_conflict": "マージ失敗: マージ中にコンフリクトがありました。 ヒント: 別のストラテジーを試してみてください。", "repo.pulls.merge_conflict_summary": "エラーメッセージ", @@ -1877,6 +1886,7 @@ "repo.pulls.update_not_allowed": "ブランチを更新する権限がありません", "repo.pulls.outdated_with_base_branch": "このブランチはベースブランチに対して最新ではありません", "repo.pulls.close": "プルリクエストをクローズ", + "repo.pulls.reopen": "プルリクエストを再オープン", "repo.pulls.closed_at": "がプルリクエストをクローズ %[2]s", "repo.pulls.reopened_at": "がプルリクエストを再オープン %[2]s", "repo.pulls.cmd_instruction_hint": "コマンドラインの手順を表示", @@ -1949,7 +1959,6 @@ "repo.signing.wont_sign.headsigned": "HEADコミットが署名されていないため、マージは署名されません。", "repo.signing.wont_sign.commitssigned": "関連するコミットすべてが署名されていないため、マージは署名されません。", "repo.signing.wont_sign.approved": "PRが未承認のため、マージは署名されません。", - "repo.signing.wont_sign.not_signed_in": "サインインしていません。", "repo.ext_wiki": "外部Wikiへのアクセス", "repo.ext_wiki.desc": "外部Wikiへのリンク。", "repo.wiki": "Wiki", @@ -2131,7 +2140,9 @@ "repo.settings.pulls_desc": "プルリクエストを有効にする", "repo.settings.pulls.ignore_whitespace": "空白文字のコンフリクトを無視する", "repo.settings.pulls.enable_autodetect_manual_merge": "手動マージの自動検出を有効にする (注意: 特殊なケースでは判定ミスが発生する場合があります)", + "repo.settings.pulls.allow_merge_update": "マージでプルリクエストのブランチの更新を可能にする", "repo.settings.pulls.allow_rebase_update": "リベースでプルリクエストのブランチの更新を可能にする", + "repo.settings.pulls.default_update_style": "デフォルトのブランチ更新スタイル", "repo.settings.pulls.default_target_branch": "新しいプルリクエストのデフォルトのターゲットブランチ", "repo.settings.pulls.default_target_branch_default": "デフォルトブランチ (%s)", "repo.settings.pulls.default_delete_branch_after_merge": "デフォルトでプルリクエストのブランチをマージ後に削除する", @@ -2173,7 +2184,8 @@ "repo.settings.transfer_abort_invalid": "存在しないリポジトリの移転はキャンセルできません。", "repo.settings.transfer_abort_success": "%s へのリポジトリ移転は正常にキャンセルされました。", "repo.settings.transfer_desc": "別のユーザーやあなたが管理者権限を持っている組織にリポジトリを移転します。", - "repo.settings.transfer_form_title": "確認のためリポジトリ名を入力:", + "repo.settings.enter_repo_name_to_confirm": "確認のためリポジトリ名を入力:", + "repo.settings.enter_repo_full_name_to_confirm": "確認のため完全なリポジトリ名(owner/name形式)を入力:", "repo.settings.transfer_in_progress": "現在進行中の移転があります。このリポジトリを別のユーザーに移転したい場合はキャンセルしてください。", "repo.settings.transfer_notices_1": "- 個人ユーザーに移転すると、あなたはリポジトリへのアクセス権を失います。", "repo.settings.transfer_notices_2": "- あなたが所有(または共同で所有)している組織に移転すると、リポジトリへのアクセス権は維持されます。", @@ -2249,13 +2261,14 @@ "repo.settings.webhook.delivery.success": "イベントを配信キューに追加しました。 配信履歴に表示されるまで数秒かかります。", "repo.settings.githooks_desc": "GitのフックはGit自体が提供する仕組みです。 以下のフックファイルを編集するとカスタム処理を設定できます。", "repo.settings.githook_edit_desc": "もしフックがアクティブではない場合はサンプルの内容が表示されます。 内容を空にするとフックが無効になります。", - "repo.settings.githook_name": "フックの名称", - "repo.settings.githook_content": "フックの内容", "repo.settings.update_githook": "フックを更新", "repo.settings.add_webhook_desc": "GiteaはターゲットURLに、指定したContent TypeでPOSTリクエストを送ります。 詳細はWebhookガイドへ。", "repo.settings.payload_url": "ターゲットURL", "repo.settings.http_method": "HTTPメソッド", "repo.settings.content_type": "POST Content Type", + "repo.settings.webhook.name": "Webhook名", + "repo.settings.webhook.name_helper": "任意で、このWebhookにわかりやすい名前をつける", + "repo.settings.webhook.name_empty": "無名のWebhook", "repo.settings.secret": "シークレット", "repo.settings.webhook_secret_desc": "Webhookサーバーがsecretの使用をサポートしている場合は、webhookのマニュアルに従いここにsecretを入力できます。", "repo.settings.slack_username": "ユーザー名", @@ -2404,6 +2417,11 @@ "repo.settings.protect_merge_whitelist_committers_desc": "許可リストに登録したユーザーまたはチームにのみ、このブランチへのプルリクエストのマージを許可します。", "repo.settings.protect_merge_whitelist_users": "マージ許可リストに含むユーザー:", "repo.settings.protect_merge_whitelist_teams": "マージ許可リストに含むチーム:", + "repo.settings.protect_bypass_allowlist": "ブランチ保護のバイパス", + "repo.settings.protect_enable_bypass_allowlist": "選択されたユーザーまたはチームにブランチ保護のバイパスを許可", + "repo.settings.protect_enable_bypass_allowlist_desc": "許可リストに指定したユーザーまたはチームは、必要な承認、ステータスチェック、保護ファイルのルールにより通常であればブロックされる場合でも、マージやプッシュを行うことができます。", + "repo.settings.protect_bypass_allowlist_users": "保護のバイパスを許可するユーザー:", + "repo.settings.protect_bypass_allowlist_teams": "保護のバイパスを許可するチーム:", "repo.settings.protect_check_status_contexts": "ステータスチェックを有効にする", "repo.settings.protect_status_check_patterns": "ステータスチェック パターン:", "repo.settings.protect_status_check_patterns_desc": "このルールの対象ブランチがマージ可能になる前に、どのステータスチェックがパスしなければならないかを、パターンで入力します。 各行にパターンを指定します。 この設定は空にできません。", @@ -2445,7 +2463,7 @@ "repo.settings.block_outdated_branch": "遅れているプルリクエストのマージをブロック", "repo.settings.block_outdated_branch_desc": "baseブランチがheadブランチより進んでいる場合、マージできないようにします。", "repo.settings.block_admin_merge_override": "管理者もブランチ保護のルールに従う", - "repo.settings.block_admin_merge_override_desc": "管理者はブランチ保護のルールに従う必要があり、回避することはできません。", + "repo.settings.block_admin_merge_override_desc": "管理者はブランチ保護のルールに従う必要があり、回避することはできません。 バイパス許可リストを有効にしている場合、許可リストに含まれるユーザーやチームは、引き続きルールのバイパスが可能です。", "repo.settings.default_branch_desc": "コミット表示のデフォルトのブランチを選択します。", "repo.settings.default_target_branch_desc": "プルリクエストでは、リポジトリ拡張設定の「プルリクエスト」セクションで設定することで、別のデフォルトターゲットブランチを使用できます。", "repo.settings.merge_style_desc": "マージ スタイル", @@ -2476,7 +2494,10 @@ "repo.settings.visibility.private.text": "プライベートに変更することで、リポジトリは許可されたメンバーにのみ表示されるようになり、既存のフォーク、ウォッチャー、スターとの関係は解除される可能性があります。", "repo.settings.visibility.private.bullet_title": "プライベートに変更すると:", "repo.settings.visibility.private.bullet_one": "許可されたメンバーにのみリポジトリを表示します。", - "repo.settings.visibility.private.bullet_two": "フォークウォッチャースターとの関係を解除する可能性があります。", + "repo.settings.visibility.private.bullet_two": "非公開設定はフォークにも適用し、ウォッチャースターは削除します。", + "repo.settings.visibility.private.stats_stars": "このリポジトリには、失われるおそれのあるスターが %d 個あります。", + "repo.settings.visibility.private.stats_watchers": "このリポジトリには、失われるおそれのあるウォッチャーが %d 件あります。", + "repo.settings.visibility.private.stats_forks": "このリポジトリには、関連するフォークが %d 件あります。", "repo.settings.visibility.public.button": "公開する", "repo.settings.visibility.public.text": "公開に変更すると、リポジトリを誰でも閲覧できるようにします。", "repo.settings.visibility.public.bullet_title": "公開に変更すると:", @@ -2712,6 +2733,8 @@ "org.members": "メンバー", "org.teams": "チーム", "org.code": "コード", + "org.repos.empty": "リポジトリはまだありません。", + "org.repos.empty_description": "コードを組織内で共有するため、リポジトリを作成してください。", "org.lower_members": "メンバー", "org.lower_repositories": "リポジトリ", "org.create_new_team": "新しいチーム", @@ -2804,7 +2827,10 @@ "org.teams.no_desc": "このチームには説明がありません。", "org.teams.settings": "設定", "org.teams.owners_permission_desc": "オーナーはすべてのリポジトリへのフルアクセス権と、組織への管理者アクセス権を持ちます。", + "org.teams.owners_permission_suggestion": "メンバーのアクセス権限を細かく管理するため、新しくチームを作成することもできます。", "org.teams.members": "チームメンバー", + "org.teams.manage_team_member": "チームとメンバーを管理", + "org.teams.manage_team_member_prompt": "メンバーの管理はチームを使って行います。 ユーザーをこの組織に招待するには、チームに追加してください。", "org.teams.update_settings": "設定の更新", "org.teams.delete_team": "チームを削除", "org.teams.add_team_member": "チームメンバーを追加", @@ -2845,6 +2871,8 @@ "org.worktime.date_range_end": "期間 (至)", "org.worktime.query": "集計", "org.worktime.time": "時間", + "org.worktime.empty": "作業時間のデータはまだありません。", + "org.worktime.empty_description": "記録された作業時間を表示するには、日付範囲を調整してください。", "org.worktime.by_repositories": "リポジトリ別", "org.worktime.by_milestones": "マイルストーン別", "org.worktime.by_members": "メンバー別", @@ -2859,6 +2887,30 @@ "admin.hooks": "Webhook", "admin.integrations": "連携", "admin.authentication": "認証ソース", + "admin.badges": "バッジ", + "admin.badges.badges_manage_panel": "バッジ管理", + "admin.badges.details": "バッジ詳細", + "admin.badges.new_badge": "新しいバッジを作成", + "admin.badges.slug": "スラッグ", + "admin.badges.slug_been_taken": "スラッグが既に使用されています。", + "admin.badges.description": "説明", + "admin.badges.image_url": "画像URL", + "admin.badges.new_success": "バッジ \"%s\" を作成しました。", + "admin.badges.update_success": "バッジを更新しました。", + "admin.badges.deletion_success": "バッジを削除しました。", + "admin.badges.edit_badge": "バッジを編集", + "admin.badges.update_badge": "バッジを更新", + "admin.badges.delete_badge": "バッジを削除", + "admin.badges.delete_badge_desc": "このバッジを完全に削除してよろしいですか?", + "admin.badges.users_with_badge": "バッジ保持者: %s", + "admin.badges.not_found": "バッジが見つかりません。", + "admin.badges.user_already_has": "ユーザーは既にこのバッジを保有しています。", + "admin.badges.user_add_success": "ユーザーにバッジを付与しました。", + "admin.badges.user_remove_success": "ユーザーのバッジを取り消しました。", + "admin.badges.manage_users": "ユーザーを管理", + "admin.badges.add_user": "ユーザーを追加", + "admin.badges.remove_user": "ユーザーを削除", + "admin.badges.delete_user_desc": "このユーザーのバッジを取り消してもよろしいですか?", "admin.emails": "ユーザーメールアドレス", "admin.config": "設定", "admin.config_summary": "サマリー", @@ -3135,6 +3187,8 @@ "admin.auths.oauth2_required_claim_name_helper": "このClaim名を設定すると、このソースからのログインを、指定したClaim名を持つユーザーに限定します。", "admin.auths.oauth2_required_claim_value": "必須Claim値", "admin.auths.oauth2_required_claim_value_helper": "この値を設定すると、このソースからのログインを、指定したClaim名とClaim値を持つユーザーに限定します。", + "admin.auths.open_id_connect_external_id_claim": "外部ID Claim名 (オプション)", + "admin.auths.open_id_connect_external_id_claim_helper": "ユーザーの外部IDに使用するClaim名。 デフォルトは \"sub\" です。 Azure AD / Entra IDについては、Azure AD V2 プロバイダーから移行したときに継続性を維持するため、この値を \"oid\" にしてください。 注: \"oid\" Claimを使う場合は、上にあるスコープ設定に \"profile\" スコープを含める必要があります。", "admin.auths.oauth2_group_claim_name": "このソースでグループ名を提供するClaim名 (オプション)", "admin.auths.oauth2_full_name_claim_name": "フルネームのClaim名。 (オプション — 設定すると、ユーザーのフルネームは常にこのClaimに同期します)", "admin.auths.oauth2_ssh_public_key_claim_name": "SSH公開鍵のClaim名", @@ -3187,11 +3241,8 @@ "admin.config.server_config": "サーバー設定", "admin.config.app_name": "サイトのタイトル", "admin.config.app_ver": "Giteaのバージョン", - "admin.config.app_url": "GiteaのベースURL", "admin.config.custom_conf": "設定ファイルのパス", "admin.config.custom_file_root_path": "カスタムファイルのルートパス", - "admin.config.domain": "サーバードメイン", - "admin.config.offline_mode": "ローカルモード", "admin.config.disable_router_log": "ルーターのログが無効", "admin.config.run_user": "実行ユーザー名", "admin.config.run_mode": "実行モード", @@ -3271,15 +3322,20 @@ "admin.config.cache_config": "キャッシュ設定", "admin.config.cache_adapter": "キャッシュ アダプター", "admin.config.cache_interval": "キャッシュ間隔", - "admin.config.cache_conn": "キャッシュ接続", "admin.config.cache_item_ttl": "キャッシュアイテムのTTL", "admin.config.cache_test": "キャッシュのテスト", "admin.config.cache_test_failed": "キャッシュの調査に失敗しました: %v.", "admin.config.cache_test_slow": "キャッシュのテストは成功しましたが、応答が遅いです: %s", "admin.config.cache_test_succeeded": "キャッシュのテストは成功し、応答時間は %s でした。", + "admin.config.common.start_time": "開始時刻", + "admin.config.common.end_time": "終了時刻", + "admin.config.common.skip_time_check": "時刻を空に(入力欄をクリア)すると時刻チェックをスキップします", + "admin.config.instance_maintenance": "インスタンスのメンテナンス", + "admin.config.instance_maintenance_mode.admin_web_access_only": "管理者にのみWeb UIへのアクセスを許可", + "admin.config.instance_web_banner.enabled": "バナーを表示", + "admin.config.instance_web_banner.message_placeholder": "バナーメッセージ (Markdownをサポート)", "admin.config.session_config": "セッション設定", "admin.config.session_provider": "セッション プロバイダー", - "admin.config.provider_config": "プロバイダーの設定", "admin.config.cookie_name": "Cookieの名称", "admin.config.gc_interval_time": "GCの間隔", "admin.config.session_life_time": "セッションの有効期間", @@ -3287,7 +3343,7 @@ "admin.config.cookie_life_time": "Cookieの有効期間", "admin.config.picture_config": "画像とアバターの設定", "admin.config.picture_service": "画像サービス", - "admin.config.disable_gravatar": "Gravatarが無効", + "admin.config.enable_gravatar": "Gravatar有効", "admin.config.enable_federated_avatar": "フェデレーテッド・アバター有効", "admin.config.open_with_editor_app_help": "クローンメニューの「~で開く」に表示するエディタ。 空白のままにするとデフォルトが使用されます。 展開するとデフォルトを確認できます。", "admin.config.git_guide_remote_name": "操作説明内のgitコマンドで使うリポジトリリモート名", @@ -3465,6 +3521,7 @@ "packages.dependencies": "依存関係", "packages.keywords": "キーワード", "packages.details": "詳細", + "packages.name": "パッケージ名", "packages.details.author": "著作者", "packages.details.project_site": "プロジェクトサイト", "packages.details.repository_site": "リポジトリサイト", @@ -3560,9 +3617,27 @@ "packages.swift.registry": "このレジストリをコマンドラインからセットアップします:", "packages.swift.install": "あなたの Package.swift ファイルにパッケージを追加します:", "packages.swift.install2": "そして次のコマンドを実行します:", + "packages.terraform.install": "HTTPバックエンドを使用するよう状態を設定します", + "packages.terraform.install2": "そして次のコマンドを実行します:", + "packages.terraform.lock_status": "ロック状態", + "packages.terraform.locked_by": "%s がロックしています", + "packages.terraform.unlocked": "アンロックされています", + "packages.terraform.lock": "ロック", + "packages.terraform.unlock": "アンロック", + "packages.terraform.lock.success": "Terraform状態は正常にロックされました。", + "packages.terraform.unlock.success": "Terraform状態は正常にアンロックされました。", + "packages.terraform.lock.error.already_locked": "Terraform状態は既にロックされています。", + "packages.terraform.delete.locked": "Terraform状態はロックされているため削除できません。", + "packages.terraform.delete.latest": "Terraform状態の最新バージョンは削除できません。", "packages.vagrant.install": "Vagrant ボックスを追加するには、次のコマンドを実行します。", "packages.settings.link": "このパッケージをリポジトリにリンク", - "packages.settings.link.description": "パッケージをリポジトリにリンクすると、リポジトリのパッケージ一覧にそのパッケージが表示されます。 リンクできるのは、同じオーナーが所有するリポジトリのみです。 入力欄を空にするとリンクを削除します。", + "packages.settings.link.description": "パッケージをリポジトリにリンクすると、リポジトリのパッケージ一覧に表示されるようになります。", + "packages.settings.link.notice1": "同じオーナーが所有するリポジトリにだけリンクできます。", + "packages.settings.link.notice2": "リポジトリにリンクしてもパッケージの公開範囲は変わりません。", + "packages.settings.link.notice3": "入力欄を空にするとリンクを削除します。", + "packages.settings.visibility": "パッケージの公開範囲", + "packages.settings.visibility.inherit": "パッケージの公開範囲は所有者から継承されており、ここで個別に変更することはできません。 変更するには、このパッケージを所有するユーザーまたは組織の公開設定を変更してください。", + "packages.settings.visibility.button": "所有者の公開範囲を変更", "packages.settings.link.select": "リポジトリを選択", "packages.settings.link.button": "リポジトリのリンクを更新", "packages.settings.link.success": "リポジトリのリンクを更新しました。", @@ -3573,8 +3648,13 @@ "packages.settings.delete": "パッケージ削除", "packages.settings.delete.description": "パッケージの削除は恒久的で元に戻すことはできません。", "packages.settings.delete.notice": "%s (%s) を削除しようとしています。この操作は元に戻せません。よろしいですか?", + "packages.settings.delete.notice.package": "%s とその全てのバージョンを削除しようとしています。この操作は元に戻せません。よろしいですか?", "packages.settings.delete.success": "パッケージを削除しました。", + "packages.settings.delete.version.success": "パッケージバージョンを削除しました。", "packages.settings.delete.error": "パッケージの削除に失敗しました。", + "packages.settings.delete.version": "バージョンを削除", + "packages.settings.delete.confirm": "確認のためパッケージ名を入力してください", + "packages.settings.delete.invalid_package_name": "入力したパッケージ名が間違っています。", "packages.owner.settings.cargo.title": "Cargoレジストリ インデックス", "packages.owner.settings.cargo.initialize": "インデックスを初期化", "packages.owner.settings.cargo.initialize.description": "Cargoレジストリを使用するには、インデックス用の特別なgitリポジトリが必要です。 このオプションを使用するとそのリポジトリを(再)作成し、自動的に構成します。", @@ -3630,9 +3710,10 @@ "actions.status.running": "実行中", "actions.status.success": "成功", "actions.status.failure": "失敗", - "actions.status.cancelled": "キャンセル", - "actions.status.skipped": "スキップ", - "actions.status.blocked": "ブロックされた", + "actions.status.cancelled": "キャンセルされました", + "actions.status.cancelling": "キャンセル中", + "actions.status.skipped": "スキップされました", + "actions.status.blocked": "ブロックされました", "actions.runners": "ランナー", "actions.runners.runner_manage_panel": "ランナーの管理", "actions.runners.new": "新しいランナーを作成", @@ -3641,6 +3722,7 @@ "actions.runners.id": "ID", "actions.runners.name": "名称", "actions.runners.owner_type": "タイプ", + "actions.runners.availability": "利用可否", "actions.runners.description": "説明", "actions.runners.labels": "ラベル", "actions.runners.last_online": "最終オンライン時刻", @@ -3656,6 +3738,12 @@ "actions.runners.update_runner": "変更を保存", "actions.runners.update_runner_success": "ランナーを更新しました", "actions.runners.update_runner_failed": "ランナーの更新に失敗しました", + "actions.runners.enable_runner": "このランナーを有効化", + "actions.runners.enable_runner_success": "ランナーを有効化しました", + "actions.runners.enable_runner_failed": "ランナーの有効化に失敗しました", + "actions.runners.disable_runner": "このランナーを無効化", + "actions.runners.disable_runner_success": "ランナーを無効化しました", + "actions.runners.disable_runner_failed": "ランナーの無効化に失敗しました", "actions.runners.delete_runner": "このランナーを削除", "actions.runners.delete_runner_success": "ランナーを削除しました", "actions.runners.delete_runner_failed": "ランナーの削除に失敗しました", @@ -3671,7 +3759,11 @@ "actions.runners.reset_registration_token_confirm": "現在のトークンを無効にして、新しいトークンを生成しますか?", "actions.runners.reset_registration_token_success": "ランナー登録トークンをリセットしました", "actions.runs.all_workflows": "すべてのワークフロー", + "actions.runs.workflow_run_count_1": "%d 件のワークフロー実行", + "actions.runs.workflow_run_count_n": "%d 件のワークフロー実行", "actions.runs.commit": "コミット", + "actions.runs.run_details": "実行の詳細", + "actions.runs.workflow_file": "ワークフローのファイル", "actions.runs.scheduled": "スケジュール済み", "actions.runs.pushed_by": "pushed by", "actions.runs.invalid_workflow_helper": "ワークフロー設定ファイルは無効です。あなたの設定ファイルを確認してください: %s", @@ -3694,6 +3786,13 @@ "actions.runs.delete.description": "このワークフローを完全に削除してもよろしいですか?この操作は元に戻せません。", "actions.runs.not_done": "このワークフローの実行は完了していません。", "actions.runs.view_workflow_file": "ワークフローファイルを表示", + "actions.runs.summary": "サマリー", + "actions.runs.all_jobs": "すべてのジョブ", + "actions.runs.attempt": "試行", + "actions.runs.latest": "最新", + "actions.runs.latest_attempt": "最新 試行", + "actions.runs.triggered_via": "%s によりトリガー", + "actions.runs.total_duration": "総所要時間:", "actions.workflow.disable": "ワークフローを無効にする", "actions.workflow.disable_success": "ワークフロー '%s' が無効になりました。", "actions.workflow.enable": "ワークフローを有効にする", @@ -3743,5 +3842,24 @@ "git.filemode.normal_file": "ノーマルファイル", "git.filemode.executable_file": "実行可能ファイル", "git.filemode.symbolic_link": "シンボリックリンク", - "git.filemode.submodule": "サブモジュール" + "git.filemode.submodule": "サブモジュール", + "org.repos.none": "リポジトリはありません。", + "actions.general.permissions": "Actionsトークンの権限", + "actions.general.token_permissions.mode": "トークンのデフォルトの権限", + "actions.general.token_permissions.mode.desc": "ワークフローファイルでActionsジョブに権限(permissions)を指定していない場合、デフォルトの権限が使用されます。", + "actions.general.token_permissions.mode.permissive": "許可", + "actions.general.token_permissions.mode.permissive.desc": "ジョブの対象リポジトリについて、読み取りと書き込みを許可。", + "actions.general.token_permissions.mode.restricted": "制限あり", + "actions.general.token_permissions.mode.restricted.desc": "ジョブの対象リポジトリのコンテンツ・ユニット(コード、リリース)について、読み取りのみ許可。", + "actions.general.token_permissions.override_owner": "オーナーレベルの設定を上書きする", + "actions.general.token_permissions.override_owner_desc": "有効にすると、このリポジトリは(ユーザーまたは組織の)オーナーのActions設定に従うのではなく、独自の設定を使用します。", + "actions.general.token_permissions.maximum": "トークン権限の上限", + "actions.general.token_permissions.maximum.description": "Actionsジョブの実効権限はこの上限によって制限されます。", + "actions.general.token_permissions.fork_pr_note": "フォークからのプルリクエストによってジョブが開始された場合は、実効権限は読み取り専用までとなります。", + "actions.general.token_permissions.customize_max_permissions": "権限の上限をカスタマイズする", + "actions.general.cross_repo": "クロスリポジトリ アクセス", + "actions.general.cross_repo_desc": "Actionsジョブの実行時に、このオーナーのすべてのリポジトリから選択したリポジトリへ、GITEA_TOKENを使用して(読み取り専用で)アクセスすることを許可します。", + "actions.general.cross_repo_selected": "選択したリポジトリ", + "actions.general.cross_repo_target_repos": "対象リポジトリ", + "actions.general.cross_repo_add": "対象リポジトリの追加" } From 6f4027a6be28c876c0abaf37cc939658645b78a3 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Sun, 24 May 2026 04:13:49 -0500 Subject: [PATCH 18/18] fix(packages): render markdown links relative to linked repo (#37676) Package-page markdown (READMEs, descriptions, release notes) was rendered as a plain document, so relative links and images resolved against the site root and 404'd. This renders it in the context of the package's linked repository instead, falling back to plain rendering when the package has no linked repo. For a README link `[usage](docs/usage.md)` in a package linked to `user/repo` (default branch `main`): | | Resolved link | |---|---| | Before | `/docs/usage.md` | | After | `/user/repo/src/branch/main/docs/usage.md` | For an npm monorepo package with `repository.directory: packages/foo`, an image `![logo](logo.png)` resolves to `/user/repo/src/branch/main/packages/foo/logo.png`. Applied to every package content template that renders markdown: `cargo`, `chef`, `composer`, `npm`, `nuget`, `pub`, `pypi`. Links resolve against the repository default branch (metadata records no publish commit). Only the web package detail page is affected; registry API responses are unchanged. Note: as part of restructuring `npm.tmpl`, the package description and README now render as separate sections instead of the README replacing the description, matching the existing `cargo`/`composer`/`pub` layout. Signed-off-by: wxiaoguang Signed-off-by: silverwind Co-authored-by: silverwind Co-authored-by: Claude (Opus 4.7) Co-authored-by: wxiaoguang --- modules/packages/npm/creator.go | 7 +++-- modules/packages/npm/creator_test.go | 6 ++-- modules/templates/util_render.go | 20 ++++++++++++++ modules/templates/util_render_test.go | 32 ++++++++++++++++++++++ templates/package/content/cargo.tmpl | 2 +- templates/package/content/chef.tmpl | 2 +- templates/package/content/composer.tmpl | 2 +- templates/package/content/npm.tmpl | 11 ++------ templates/package/content/nuget.tmpl | 6 ++-- templates/package/content/pub.tmpl | 2 +- templates/package/content/pypi.tmpl | 4 +-- tests/integration/api_packages_npm_test.go | 29 +++++++++++++++++++- 12 files changed, 99 insertions(+), 24 deletions(-) diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index cc7695726b..c49e1267a7 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -181,10 +181,11 @@ func (u *User) UnmarshalJSON(data []byte) error { return nil } -// Repository https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version +// Repository https://docs.npmjs.com/cli/v11/configuring-npm/package-json#repository type Repository struct { - Type string `json:"type"` - URL string `json:"url"` + Type string `json:"type"` + URL string `json:"url"` + Directory string `json:"directory,omitempty"` } // PackageAttachment https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package diff --git a/modules/packages/npm/creator_test.go b/modules/packages/npm/creator_test.go index 40c50de91f..7474d6c9f4 100644 --- a/modules/packages/npm/creator_test.go +++ b/modules/packages/npm/creator_test.go @@ -28,8 +28,9 @@ func TestParsePackage(t *testing.T) { data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA" integrity := "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==" repository := Repository{ - Type: "gitea", - URL: "http://localhost:3000/gitea/test.git", + Type: "gitea", + URL: "http://localhost:3000/gitea/test.git", + Directory: "packages/test-package", } t.Run("InvalidUpload", func(t *testing.T) { @@ -298,6 +299,7 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"]) assert.Equal(t, repository.Type, p.Metadata.Repository.Type) assert.Equal(t, repository.URL, p.Metadata.Repository.URL) + assert.Equal(t, repository.Directory, p.Metadata.Repository.Directory) }) t.Run("ValidLicenseMap", func(t *testing.T) { diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 9d05bb2a0b..b78196bd5c 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/emoji" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -223,6 +224,25 @@ func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:rev return output } +// RenderPackageMarkdown renders package page Markdown so relative links resolve against the +// linked repository's default branch instead of the site root, falling back to plain rendering +// when there is no linked repository. pkgTreePath optionally roots links in a subdirectory +// (e.g. npm's repository.directory for monorepo packages). +func (ut *RenderUtils) RenderPackageMarkdown(input string, linkedRepo *repo.Repository, pkgTreePath ...string) template.HTML { + if linkedRepo == nil { + return `
` + ut.MarkdownToHtml(input) + `
` + } + rctx := renderhelper.NewRenderContextRepoFile(ut.ctx, linkedRepo, renderhelper.RepoFileOptions{ + CurrentRefSubURL: git.RefNameFromBranch(linkedRepo.DefaultBranch).RefWebLinkPath(), + CurrentTreePath: util.OptionalArg(pkgTreePath), + }) + output, err := markdown.RenderString(rctx, input) + if err != nil { + log.Error("RenderString: %v", err) + } + return `
` + output + `
` +} + func (ut *RenderUtils) RenderLabels(labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML { isPullRequest := issue != nil && issue.IsPull baseLink := fmt.Sprintf("%s/%s", repoLink, util.Iif(isPullRequest, "pulls", "issues")) diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index de7768e91c..61dcb4937f 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -194,6 +194,38 @@ space

assert.Equal(t, expected, string(newTestRenderUtils(t).MarkdownToHtml(testInput()))) } +func TestRenderPackageMarkdown(t *testing.T) { + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + mockRepo := &repo.Repository{ + ID: 1, OwnerName: "user13", Name: "repo11", DefaultBranch: "main", + Owner: &user_model.User{ID: 13, Name: "user13"}, + Units: []*repo.RepoUnit{}, + } + ut := newTestRenderUtils(t) + + t.Run("LinkedRepoWithDirectory", func(t *testing.T) { + rendered := ut.RenderPackageMarkdown("[docs](docs/getting-started.md)\n![logo](logo.png)", mockRepo, "pkg-subdir") + expected := `

docs +logo

+
` + assert.Equal(t, expected, strings.TrimSpace(string(rendered))) + }) + + t.Run("LinkedRepoWithEmptyDirectory", func(t *testing.T) { + rendered := ut.RenderPackageMarkdown("[docs](docs/getting-started.md)", mockRepo, "") + expected := `` + assert.Equal(t, expected, strings.TrimSpace(string(rendered))) + }) + + t.Run("UnlinkedRepo", func(t *testing.T) { + rendered := ut.RenderPackageMarkdown("[docs](docs/getting-started.md)", nil, "pkg-subdir") + expected := `` + assert.Equal(t, expected, strings.TrimSpace(string(rendered))) + }) +} + func TestRenderLabels(t *testing.T) { ut := newTestRenderUtils(t) label := &issues.Label{ID: 123, Name: "label-name", Color: "label-color"} diff --git a/templates/package/content/cargo.tmpl b/templates/package/content/cargo.tmpl index ebb23e8659..5c04ab8aae 100644 --- a/templates/package/content/cargo.tmpl +++ b/templates/package/content/cargo.tmpl @@ -27,7 +27,7 @@ git-fetch-with-cli = true {{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Readme}}

{{ctx.Locale.Tr "packages.about"}}

{{if .PackageDescriptor.Metadata.Description}}
{{.PackageDescriptor.Metadata.Description}}
{{end}} - {{if .PackageDescriptor.Metadata.Readme}}
{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Readme}}
{{end}} + {{if .PackageDescriptor.Metadata.Readme}}
{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Readme .PackageDescriptor.Repository}}
{{end}} {{end}} {{if .PackageDescriptor.Metadata.Dependencies}} diff --git a/templates/package/content/chef.tmpl b/templates/package/content/chef.tmpl index d1c17d36a4..0912aff792 100644 --- a/templates/package/content/chef.tmpl +++ b/templates/package/content/chef.tmpl @@ -20,7 +20,7 @@

{{ctx.Locale.Tr "packages.about"}}

{{if .PackageDescriptor.Metadata.Description}}

{{.PackageDescriptor.Metadata.Description}}

{{end}} - {{if .PackageDescriptor.Metadata.LongDescription}}{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.LongDescription}}{{end}} + {{if .PackageDescriptor.Metadata.LongDescription}}{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.LongDescription .PackageDescriptor.Repository}}{{end}}
{{end}} diff --git a/templates/package/content/composer.tmpl b/templates/package/content/composer.tmpl index 2e8cfb77eb..5bbc19e9f2 100644 --- a/templates/package/content/composer.tmpl +++ b/templates/package/content/composer.tmpl @@ -25,7 +25,7 @@ {{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Comments}}

{{ctx.Locale.Tr "packages.about"}}

{{if .PackageDescriptor.Metadata.Description}}
{{.PackageDescriptor.Metadata.Description}}
{{end}} - {{if .PackageDescriptor.Metadata.Readme}}
{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Readme}}
{{end}} + {{if .PackageDescriptor.Metadata.Readme}}
{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Readme .PackageDescriptor.Repository}}
{{end}} {{if .PackageDescriptor.Metadata.Comments}}
{{StringUtils.Join .PackageDescriptor.Metadata.Comments " "}}
{{end}} {{end}} diff --git a/templates/package/content/npm.tmpl b/templates/package/content/npm.tmpl index 28bcf45ee8..89f4c94008 100644 --- a/templates/package/content/npm.tmpl +++ b/templates/package/content/npm.tmpl @@ -22,15 +22,8 @@ {{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Readme}}

{{ctx.Locale.Tr "packages.about"}}

-
- {{if .PackageDescriptor.Metadata.Readme}} -
- {{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Readme}} -
- {{else if .PackageDescriptor.Metadata.Description}} - {{.PackageDescriptor.Metadata.Description}} - {{end}} -
+ {{if .PackageDescriptor.Metadata.Description}}
{{.PackageDescriptor.Metadata.Description}}
{{end}} + {{if .PackageDescriptor.Metadata.Readme}}
{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Readme .PackageDescriptor.Repository .PackageDescriptor.Metadata.Repository.Directory}}
{{end}} {{end}} {{if or .PackageDescriptor.Metadata.Dependencies .PackageDescriptor.Metadata.DevelopmentDependencies .PackageDescriptor.Metadata.PeerDependencies .PackageDescriptor.Metadata.OptionalDependencies}} diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl index 7f874044cc..fc9a715a28 100644 --- a/templates/package/content/nuget.tmpl +++ b/templates/package/content/nuget.tmpl @@ -18,9 +18,9 @@ {{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes .PackageDescriptor.Metadata.Readme}}

{{ctx.Locale.Tr "packages.about"}}

- {{if .PackageDescriptor.Metadata.Description}}
{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Description}}
{{end}} - {{if .PackageDescriptor.Metadata.Readme}}
{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Readme}}
{{end}} - {{if .PackageDescriptor.Metadata.ReleaseNotes}}
{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.ReleaseNotes}}
{{end}} + {{if .PackageDescriptor.Metadata.Description}}
{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Description .PackageDescriptor.Repository}}
{{end}} + {{if .PackageDescriptor.Metadata.Readme}}
{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Readme .PackageDescriptor.Repository}}
{{end}} + {{if .PackageDescriptor.Metadata.ReleaseNotes}}
{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.ReleaseNotes .PackageDescriptor.Repository}}
{{end}} {{end}} {{if .PackageDescriptor.Metadata.Dependencies}} diff --git a/templates/package/content/pub.tmpl b/templates/package/content/pub.tmpl index 9eefcf7162..5dafa2db47 100644 --- a/templates/package/content/pub.tmpl +++ b/templates/package/content/pub.tmpl @@ -14,6 +14,6 @@ {{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Readme}}

{{ctx.Locale.Tr "packages.about"}}

{{if .PackageDescriptor.Metadata.Description}}
{{.PackageDescriptor.Metadata.Description}}
{{end}} - {{if .PackageDescriptor.Metadata.Readme}}
{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Readme}}
{{end}} + {{if .PackageDescriptor.Metadata.Readme}}
{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Readme .PackageDescriptor.Repository}}
{{end}} {{end}} {{end}} diff --git a/templates/package/content/pypi.tmpl b/templates/package/content/pypi.tmpl index 4afdc1b72a..c570e60e6b 100644 --- a/templates/package/content/pypi.tmpl +++ b/templates/package/content/pypi.tmpl @@ -16,9 +16,9 @@

{{if .PackageDescriptor.Metadata.Summary}}{{.PackageDescriptor.Metadata.Summary}}{{end}}

{{if .PackageDescriptor.Metadata.LongDescription}} - {{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.LongDescription}} + {{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.LongDescription .PackageDescriptor.Repository}} {{else if .PackageDescriptor.Metadata.Description}} - {{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Description}} + {{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Description .PackageDescriptor.Repository}} {{end}}
{{end}} diff --git a/tests/integration/api_packages_npm_test.go b/tests/integration/api_packages_npm_test.go index 92f9b61081..8fb892cc86 100644 --- a/tests/integration/api_packages_npm_test.go +++ b/tests/integration/api_packages_npm_test.go @@ -13,6 +13,7 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/packages" + 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/packages/npm" @@ -20,6 +21,7 @@ import ( "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPackageNpm(t *testing.T) { @@ -39,6 +41,7 @@ func TestPackageNpm(t *testing.T) { packageBinPath := "./cli.sh" repoType := "gitea" repoURL := "http://localhost:3000/gitea/test.git" + repoDirectory := "package-subdir" data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA" @@ -67,8 +70,10 @@ func TestPackageNpm(t *testing.T) { }, "repository": { "type": "` + repoType + `", - "url": "` + repoURL + `" + "url": "` + repoURL + `", + "directory": "` + repoDirectory + `" }, + "readme": "[docs](docs/usage.md)\n![logo](logo.png)", "peerDependencies": { "tea": "2.x", "soy-milk": "1.2" @@ -282,6 +287,28 @@ func TestPackageNpm(t *testing.T) { } }) + t.Run("WebViewReadmeRepoLinks", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNpm) + assert.NoError(t, err) + require.Len(t, pvs, 1) + + // link the package to a repository so README relative links resolve against + // repository files instead of the site root + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.NoError(t, packages.SetRepositoryLink(t.Context(), pvs[0].PackageID, repo.ID)) + + req := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/npm/%s/%s", user.Name, url.PathEscape(packageName), packageVersion)). + AddBasicAuth(user.Name) + resp := MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + rendered, _ := doc.Find(".markup.markdown").Html() + assert.Equal(t, `

docs +logo

+`, rendered) + }) + t.Run("Delete", func(t *testing.T) { defer tests.PrintCurrentTest(t)()