Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package woodpecker for openSUSE:Factory checked in at 2026-10-30 18:15:04 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/woodpecker (Old) and /work/SRC/openSUSE:Factory/.woodpecker.new.1255 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "woodpecker" Fri Oct 30 18:15:04 2026 rev:34 rq:1393801 version:3.20.1 Changes: -------- --- /work/SRC/openSUSE:Factory/woodpecker/woodpecker.changes 2026-10-29 16:11:15.616441278 +0100 +++ /work/SRC/openSUSE:Factory/.woodpecker.new.1255/woodpecker.changes 2026-10-30 18:22:35.761127265 +0100 @@ -1,0 +2,10 @@ +Fri Oct 30 05:55:54 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 3.20.1: + * Bug Fixes + - Restore drone envsubst [#7433] + - Fix step lop group UX [#7431] + * Dependency + - Update golang-packages [#7426] + +------------------------------------------------------------------- Old: ---- web-3.20.0.tar.gz woodpecker-3.20.0.obscpio New: ---- web-3.20.1.tar.gz woodpecker-3.20.1.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ woodpecker.spec ++++++ --- /var/tmp/diff_new_pack.l5eNom/_old 2026-10-30 18:22:38.662248713 +0100 +++ /var/tmp/diff_new_pack.l5eNom/_new 2026-10-30 18:22:38.666248880 +0100 @@ -23,7 +23,7 @@ %define server_package_name woodpecker-server %define server_executable_name woodpecker-server Name: woodpecker -Version: 3.20.0 +Version: 3.20.1 Release: 0 Summary: Simple yet powerful CI/CD engine with great extensibility License: Apache-2.0 @@ -37,7 +37,7 @@ Source11: woodpecker-server.service Source12: woodpecker-agent.service Source21: system-user-woodpecker.conf -BuildRequires: golang(API) >= 1.26 +BuildRequires: golang(API) >= 1.27 %description Woodpecker is a simple yet powerful CI/CD engine with great extensibility ++++++ _service ++++++ --- /var/tmp/diff_new_pack.l5eNom/_old 2026-10-30 18:22:38.805254699 +0100 +++ /var/tmp/diff_new_pack.l5eNom/_new 2026-10-30 18:22:38.816255160 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/woodpecker-ci/woodpecker.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">refs/tags/v3.20.0</param> + <param name="revision">refs/tags/v3.20.1</param> <param name="versionformat">@PARENT_TAG@</param> <param name="changesgenerate">enable</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.l5eNom/_old 2026-10-30 18:22:38.868257337 +0100 +++ /var/tmp/diff_new_pack.l5eNom/_new 2026-10-30 18:22:38.874257588 +0100 @@ -3,6 +3,6 @@ <param name="url">https://github.com/woodpecker-ci/woodpecker</param> <param name="changesrevision">0812d5e790472290eae75a3f329c85af41d203b0</param></service><service name="tar_scm"> <param name="url">https://github.com/woodpecker-ci/woodpecker.git</param> - <param name="changesrevision">f10ce96d3c73ee8cd4608a3cc89b403cd0863d05</param></service></servicedata> + <param name="changesrevision">3f1bb0bd0db28e410ad797034d75780116122c91</param></service></servicedata> (No newline at EOF) ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/woodpecker/vendor.tar.gz /work/SRC/openSUSE:Factory/.woodpecker.new.1255/vendor.tar.gz differ: char 13, line 1 ++++++ web-3.20.0.tar.gz -> web-3.20.1.tar.gz ++++++ /work/SRC/openSUSE:Factory/woodpecker/web-3.20.0.tar.gz /work/SRC/openSUSE:Factory/.woodpecker.new.1255/web-3.20.1.tar.gz differ: char 30, line 1 ++++++ woodpecker-3.20.0.obscpio -> woodpecker-3.20.1.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/woodpecker-3.20.0/.golangci.yaml new/woodpecker-3.20.1/.golangci.yaml --- old/woodpecker-3.20.0/.golangci.yaml 2026-10-28 19:37:53.000000000 +0100 +++ new/woodpecker-3.20.1/.golangci.yaml 2026-10-29 20:38:42.000000000 +0100 @@ -55,8 +55,6 @@ deny: - pkg: golang.org/x/crypto/openpgp desc: deprecated and frozen, use github.com/ProtonMail/go-crypto/openpgp instead - - pkg: github.com/drone/envsubst - desc: unmaintained, use github.com/woodpecker-ci/envsubst instead agent: list-mode: lax files: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/woodpecker-3.20.0/CHANGELOG.md new/woodpecker-3.20.1/CHANGELOG.md --- old/woodpecker-3.20.0/CHANGELOG.md 2026-10-28 19:37:53.000000000 +0100 +++ new/woodpecker-3.20.1/CHANGELOG.md 2026-10-29 20:38:42.000000000 +0100 @@ -1,5 +1,20 @@ # Changelog +## [3.20.1](https://github.com/woodpecker-ci/woodpecker/releases/tag/v3.20.1) - 2026-10-29 + +### ❤️ Thanks to all contributors! ❤️ + +@6543, @lafriks + +### 🐛 Bug Fixes + +- Restore drone envsubst [[#7433](https://github.com/woodpecker-ci/woodpecker/pull/7433)] +- Fix step lop group UX [[#7431](https://github.com/woodpecker-ci/woodpecker/pull/7431)] + +### 📦️ Dependency + +- Update golang-packages [[#7426](https://github.com/woodpecker-ci/woodpecker/pull/7426)] + ## [3.20.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/v3.20.0) - 2026-10-28 ### ❤️ Thanks to all contributors! ❤️ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/woodpecker-3.20.0/go.mod new/woodpecker-3.20.1/go.mod --- old/woodpecker-3.20.0/go.mod 2026-10-28 19:37:53.000000000 +0100 +++ new/woodpecker-3.20.1/go.mod 2026-10-29 20:38:42.000000000 +0100 @@ -18,6 +18,7 @@ github.com/docker/cli v29.9.2+incompatible github.com/docker/go-connections v0.8.2 github.com/docker/go-units v0.5.0 + github.com/drone/envsubst v1.0.3 github.com/expr-lang/expr v1.17.8 github.com/fsnotify/fsnotify v1.10.1 github.com/gdgvda/cron v0.7.0 @@ -51,11 +52,10 @@ github.com/tink-crypto/tink-go/v2 v2.9.0 github.com/urfave/cli-docs/v3 v3.1.1-0.20251022123016-72b87d11c482 github.com/urfave/cli/v3 v3.14.0 - github.com/woodpecker-ci/envsubst v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0 github.com/yaronf/httpsign v0.6.2 github.com/zalando/go-keyring v0.2.8 - gitlab.com/gitlab-org/api/client-go/v3 v3.39.1 + gitlab.com/gitlab-org/api/client-go/v3 v3.39.2 go.uber.org/multierr v1.11.0 go.yaml.in/yaml/v4 v4.0.0-rc.6 golang.org/x/image v0.47.0 @@ -67,9 +67,9 @@ google.golang.org/grpc v1.85.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.37.1 - k8s.io/apimachinery v0.37.1 - k8s.io/client-go v0.37.1 + k8s.io/api v0.37.2 + k8s.io/apimachinery v0.37.2 + k8s.io/client-go v0.37.2 sigs.k8s.io/yaml v1.6.0 src.techknowlogick.com/xormigrate v1.7.1 xorm.io/builder v0.3.13 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/woodpecker-3.20.0/go.sum new/woodpecker-3.20.1/go.sum --- old/woodpecker-3.20.0/go.sum 2026-10-28 19:37:53.000000000 +0100 +++ new/woodpecker-3.20.1/go.sum 2026-10-29 20:38:42.000000000 +0100 @@ -136,6 +136,8 @@ github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= +github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= github.com/dunglas/httpsfv v1.1.2 h1:432E+FKy2DLMbVcZeAnOuM7nhyJ4vTiH7b9iXJ/Dkmc= github.com/dunglas/httpsfv v1.1.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -267,6 +269,7 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -560,8 +563,6 @@ github.com/urfave/cli/v3 v3.14.0/go.mod h1:vXn6HxPNccJSzQr2QvwVncOKrgYGIHU0HY5h8B2nQj4= github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= -github.com/woodpecker-ci/envsubst v1.2.0 h1:40afOEAI/jGBp1SmOenbAGbuqTBl3rWReqjqLrZw5sM= -github.com/woodpecker-ci/envsubst v1.2.0/go.mod h1:FJXaRFaV047oN78B17EP39v+sn0+SYKfv+JZfNvcGek= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -583,8 +584,8 @@ github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= -gitlab.com/gitlab-org/api/client-go/v3 v3.39.1 h1:yBmlSUi5P6L354Aq3PJCWvW24zz9Ru3WnAXgNMwA9g8= -gitlab.com/gitlab-org/api/client-go/v3 v3.39.1/go.mod h1:jBFhGkTHLKkgeHEdS/A8yA0tQ6UHq96QVsS5muu6flM= +gitlab.com/gitlab-org/api/client-go/v3 v3.39.2 h1:Pvtob9Mnkp8N4W5uhLrUGtIVW3xp9mCtFgmedRfUZ6E= +gitlab.com/gitlab-org/api/client-go/v3 v3.39.2/go.mod h1:jBFhGkTHLKkgeHEdS/A8yA0tQ6UHq96QVsS5muu6flM= go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU= go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= @@ -788,12 +789,12 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.37.1 h1:l6N77U7tjwB5L056bgrBTJIEdevac/naBZ3iSvDNfpM= -k8s.io/api v0.37.1/go.mod h1:zSlbB1YpJ1YQlFVQy20UYll81UJSJJUMLhkhvg6Z78M= -k8s.io/apimachinery v0.37.1 h1:hGCYyvKHCwtwMitj2vU4vYx0Z16N9GyZk9BBnz0wDAE= -k8s.io/apimachinery v0.37.1/go.mod h1:jF84AyUi/IRIXRot5f+lm6MpxoWI+F1XgjaMmwCdTFw= -k8s.io/client-go v0.37.1 h1:QTv/5ha4jAHtW9qxxVBkQVFBRDb4jHfFopQqqMdc+wM= -k8s.io/client-go v0.37.1/go.mod h1:dnAPtTnCNY38Ho04D2KdY1F4IKausa9UbqaAZKl60SY= +k8s.io/api v0.37.2 h1:PnxqnUFy93jr2GcpWDkKOm8wfy25kT9wa572MuQS8oc= +k8s.io/api v0.37.2/go.mod h1:+NVRLft99kx+Jg9v34GmHw867Dapim89TSyCsn2hK+M= +k8s.io/apimachinery v0.37.2 h1:G4t9RGoPDbQML38hz/kQQiltttgd3egg2ODCR8aIc2o= +k8s.io/apimachinery v0.37.2/go.mod h1:qaAvSNvgU2x07wRM95MSHSJTPQ/ob/LeNAGbeDgqQJg= +k8s.io/client-go v0.37.2 h1:sgFtyRZukGkOrsu+QgS7LYnbFFJ5ut3MchdBNduD//E= +k8s.io/client-go v0.37.2/go.mod h1:bo3LFC8YbCtKFPX21Yj8puMsoJLAv/+SvuOODma0s54= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/woodpecker-3.20.0/pipeline/frontend/metadata/substitution.go new/woodpecker-3.20.1/pipeline/frontend/metadata/substitution.go --- old/woodpecker-3.20.0/pipeline/frontend/metadata/substitution.go 2026-10-28 19:37:53.000000000 +0100 +++ new/woodpecker-3.20.1/pipeline/frontend/metadata/substitution.go 2026-10-29 20:38:42.000000000 +0100 @@ -18,7 +18,7 @@ "fmt" "strings" - "github.com/woodpecker-ci/envsubst" + "github.com/drone/envsubst" ) func EnvVarSubst(yaml string, environ map[string]string) (string, error) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/woodpecker-3.20.0/pipeline/frontend/metadata/substitution_test.go new/woodpecker-3.20.1/pipeline/frontend/metadata/substitution_test.go --- old/woodpecker-3.20.0/pipeline/frontend/metadata/substitution_test.go 2026-10-28 19:37:53.000000000 +0100 +++ new/woodpecker-3.20.1/pipeline/frontend/metadata/substitution_test.go 2026-10-29 20:38:42.000000000 +0100 @@ -34,6 +34,52 @@ command: echo "some env\nwith newline"`, result) } +// TestEnvVarSubstEscapes pins the escape rules of the config pre-processor. +// Outside of ${...}, "\\" and "\/" are unescaped like "$$", so configs that +// double backslashes (evaluate regexes, branch patterns) keep working. +func TestEnvVarSubstEscapes(t *testing.T) { + environ := map[string]string{"X": "x", "A": "a/b", "B": `a\b`} + tests := []struct { + in string + want string + }{ + // backslash escapes outside of ${...} + {in: `\\`, want: `\`}, + {in: `\/`, want: `/`}, + {in: `a\\b`, want: `a\b`}, + {in: `a\/b`, want: `a/b`}, + {in: `a\\\\b`, want: `a\\b`}, + {in: `a\\/b`, want: `a\/b`}, + {in: `$$\\`, want: `$\`}, + {in: `\\${X}\\`, want: `\x\`}, + {in: `${X}\\`, want: `x\`}, + {in: `${X}\/`, want: `x/`}, + // other backslashes are kept + {in: `a\.b`, want: `a\.b`}, + {in: `a\nb`, want: `a\nb`}, + {in: `a\`, want: `a\`}, + {in: `\$`, want: `\$`}, + {in: `\${X}`, want: `\x`}, + // dollar escapes + {in: `$$`, want: `$`}, + {in: `$$$$`, want: `$$`}, + {in: `$${X}`, want: `${X}`}, + // inside of ${...} + {in: `${A/\//_}`, want: `a_b`}, + {in: `${B/\\/_}`, want: `a_b`}, + {in: `${UNSET=x\\y}`, want: `x\\y`}, + {in: `${UNSET=x\/y}`, want: `x\/y`}, + {in: `${UNSET=x\.y}`, want: `x\.y`}, + } + for _, tt := range tests { + t.Run(tt.in, func(t *testing.T) { + got, err := EnvVarSubst(tt.in, environ) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + // TestEnvVarSubstNoStructureInjection guards against a user-controlled value // (commit message, branch name, PR title, …) injecting new YAML structure // through a line break. YAML 1.1 treats LF, CR, NEL, LS and PS as line breaks, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/woodpecker-3.20.0/pipeline/frontend/yaml/constraint/constraint_test.go new/woodpecker-3.20.1/pipeline/frontend/yaml/constraint/constraint_test.go --- old/woodpecker-3.20.0/pipeline/frontend/yaml/constraint/constraint_test.go 2026-10-28 19:37:53.000000000 +0100 +++ new/woodpecker-3.20.1/pipeline/frontend/yaml/constraint/constraint_test.go 2026-10-29 20:38:42.000000000 +0100 @@ -185,6 +185,36 @@ env: map[string]string{"TESTVAR": "qwe"}, want: false, }, + { + desc: "filter by eval with escaped regex matches release tag", + conf: `{ evaluate: 'CI_COMMIT_TAG matches "^v?[0-9]+\\\\.[0-9]+\\\\.[0-9]+$"' }`, + with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventTag, Commit: metadata.Commit{Ref: "refs/tags/v3.20.0"}}}, + want: true, + }, + { + desc: "filter by eval with escaped regex skips pre-release tag", + conf: `{ evaluate: 'CI_COMMIT_TAG matches "^v?[0-9]+\\\\.[0-9]+\\\\.[0-9]+$"' }`, + with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventTag, Commit: metadata.Commit{Ref: "refs/tags/v3.20.0-rc.1"}}}, + want: false, + }, + { + desc: "filter by branch with escaped slash matches one slash", + conf: `{ branch: '*\\/*' }`, + with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush, Commit: metadata.Commit{Branch: "feat/x"}}}, + want: true, + }, + { + desc: "filter by branch with escaped slash skips two slashes", + conf: `{ branch: '*\\/*' }`, + with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush, Commit: metadata.Commit{Branch: "feat/x/y"}}}, + want: false, + }, + { + desc: "filter by branch with escaped slash and globstar matches two slashes", + conf: `{ branch: '*\\/**' }`, + with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush, Commit: metadata.Commit{Branch: "feat/x/y"}}}, + want: true, + }, } for _, tt := range testdata { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/woodpecker-3.20.0/web/src/assets/locales/uk.json new/woodpecker-3.20.1/web/src/assets/locales/uk.json --- old/woodpecker-3.20.0/web/src/assets/locales/uk.json 2026-10-28 19:37:53.000000000 +0100 +++ new/woodpecker-3.20.1/web/src/assets/locales/uk.json 2026-10-29 20:38:42.000000000 +0100 @@ -73,6 +73,13 @@ }, "org": { "badge": "організація" + }, + "filters": { + "name": "Фільтри", + "desc": "Підписи агентів, за якими сервер фільтрує робочі процеси", + "key": "Назва підпису", + "value": "Значення підпису", + "delete": "Видалити фільтр" } }, "queue": { @@ -114,7 +121,11 @@ "admin": { "admin": "Адміністрація", "placeholder": "Користувач належить до адміністрації", - "admin_warning": "Користувач має адміністраторські доступи відповідно до вашої конфігурації середовища. Якщо забрати доступ тут, користувач знову його отримає, коли ввійде наступного разу." + "admin_warning": "Користувача перелічено в змінній середовища WOODPECKER_ADMIN. Відкликання адміністратора тут чинне лише до перезапуску сервера. Щоб відкликати назовсім, вилучіть користувача зі змінних середовища.", + "admin_env": "Адміністрація (середовище)", + "admin_env_desc": "Адміністратор, оскільки користувача перелічено в WOODPECKER_ADMIN.", + "admin_revoked": "Адміністратора тимчасово відкликано", + "admin_revoked_desc": "Користувача перелічено в змінній середовища WOODPECKER_ADMIN, але відкликано з адміністрації тут. Щоб адміністратор не відновлювався після перезапуску сервера, вилучіть його з WOODPECKER_ADMIN." }, "delete_user": "Видалити користувача", "edit_user": "Редагувати користувача", @@ -190,7 +201,12 @@ "desc": "Щоб використовувати приватні образи в усіх контейнерах організації, додайте доступи до реєстрів організації." }, "agents": { - "desc": "Агенти, зареєстровані для цієї організації." + "desc": "Агенти, зареєстровані для цієї організації.", + "require_org_agents": { + "require": "Запускати конвеєри тільки на агентах організації", + "desc": "Робочі процеси репозиторіїв організації підбиратимуть лише агенти організації, не всього сервера.", + "saved": "Параметри організації збережено" + } } } }, @@ -240,7 +256,17 @@ "delete": "Видалити змінну" }, "show_pipelines": "Показати конвеєри", - "no_manual_workflows": "Підходящих робочих процесів не знайдено. Переконайтесь, що принаймні один робочий процес реагує на подію ручного запуску (manual)." + "no_manual_workflows": "Підходящих робочих процесів не знайдено. Переконайтесь, що принаймні один робочий процес реагує на подію ручного запуску (manual).", + "message": { + "title": "Повідомлення", + "desc": "Цей текст доповнює повідомлення конвеєра." + }, + "workflow": { + "title": "Робочий процес", + "desc": "Запустити лише вибраний робочий процес і його обов'язкові залежності.", + "all": "Усі робочі процеси", + "load_error": "Не вдалось завантажити робочих процесів гілки. Переконайтесь, що гілка існує й для неї правильно налаштовано конвеєр." + } }, "not_allowed": "Ви не маєте права доступу до цього репозиторію", "open_in_forge": "Відкрити репозиторій на платформі", @@ -534,7 +560,8 @@ "internal": "Внутрішньо", "desc": "Проєкт видно лише після входу на сервер Woodpecker." } - } + }, + "perm_sync_failed": "Не вдалось синхронізувати дозволів репозиторію з платформою. Ви тимчасово можете лише читати." }, "repos": "Репозиторії", "repositories": { @@ -601,7 +628,12 @@ "swagger_ui": "Swagger-інтерфейс" }, "agents": { - "desc": "Агенти, зареєстровані у репозиторіях вашого облікового запису." + "desc": "Агенти, зареєстровані у репозиторіях вашого облікового запису.", + "require_user_agents": { + "require": "Запускати конвеєри тільки на своїх агентах", + "desc": "Робочі процеси ваших особистих репозиторіїв підбиратимуть лише агенти вашого облікового запису, не всього сервера.", + "saved": "Користувацькі параметри збережено" + } } } }, @@ -753,5 +785,8 @@ "forge_saved": "Платформу збережено", "merge_ref": "Посилання для злиття", "merge_ref_desc": "Посилання (ref) на основу злиття. Використовується для визначення різниці в запитах на злиття.", - "delete": "Видалити" + "delete": "Видалити", + "allowed_orgs": "Дозволені організації", + "allowed_orgs_desc": "Організації, членство яких може входити за допомогою цієї платформи, додатково до глобального параметра WOODPECKER_ORGS.", + "allowed_orgs_placeholder": "Назва організації" } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/woodpecker-3.20.0/web/src/assets/locales/zh-Hans.json new/woodpecker-3.20.1/web/src/assets/locales/zh-Hans.json --- old/woodpecker-3.20.0/web/src/assets/locales/zh-Hans.json 2026-10-28 19:37:53.000000000 +0100 +++ new/woodpecker-3.20.1/web/src/assets/locales/zh-Hans.json 2026-10-29 20:38:42.000000000 +0100 @@ -131,7 +131,11 @@ "admin": { "admin": "管理员", "placeholder": "此用户是管理员", - "admin_warning": "由于您的环境配置,该用户拥有管理员权限。如果你在这里撤销权限,该用户下次登录时仍会重新获得管理员权限。" + "admin_warning": "该用户已列在 WOODPECKER_ADMIN 环境配置中。在此处撤销的管理员权限仅在下次服务器重启前有效。如需永久撤销,还需将该用户从环境变量中移除。", + "admin_env": "管理员 (环境变量)", + "admin_env_desc": "该用户已列在 WOODPECKER_ADMIN 中,因此拥有管理员权限。", + "admin_revoked": "管理员权限已临时撤销", + "admin_revoked_desc": "该用户已列在 WOODPECKER_ADMIN 环境配置中,但其管理员权限已在此处撤销。除非将其从 WOODPECKER_ADMIN 中移除,否则下次服务器重启后,该用户首次登录时将重新获得管理员权限。" }, "avatar_url": "头像链接", "cancel": "取消", @@ -261,6 +265,12 @@ "message": { "title": "消息", "desc": "该消息将作为附加说明添加到流水线消息中。" + }, + "workflow": { + "title": "工作流", + "desc": "仅运行所选工作流及其硬依赖。", + "all": "所有工作流", + "load_error": "无法加载此分支的工作流。请检查该分支是否存在,以及其流水线配置是否有效。" } }, "not_allowed": "你没有权限访问这个仓库", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/woodpecker-3.20.0/web/src/components/repo/pipeline/PipelineLog.test.ts new/woodpecker-3.20.1/web/src/components/repo/pipeline/PipelineLog.test.ts --- old/woodpecker-3.20.0/web/src/components/repo/pipeline/PipelineLog.test.ts 2026-10-28 19:37:53.000000000 +0100 +++ new/woodpecker-3.20.1/web/src/components/repo/pipeline/PipelineLog.test.ts 2026-10-29 20:38:42.000000000 +0100 @@ -203,7 +203,10 @@ const wrapper = await loadLog(['initializing', '▶ make build', 'built', '▶ echo linux', 'linux']); - expect(wrapper.findAll('.sticky span[id]').map((heading) => heading.text())).toEqual(['make build', 'echo linux']); + expect(wrapper.findAll('.sticky .truncate > :last-child').map((heading) => heading.text())).toEqual([ + 'make build', + 'echo linux', + ]); await wrapper.get('button[title="repo.pipeline.actions.collapse_all"]').trigger('click'); expect(wrapper.get('a#L1').element.nextElementSibling?.textContent).toBe('initializing\n'); expect(wrapper.find('a#L3').exists()).toBe(false); @@ -219,14 +222,14 @@ const wrapper = await loadLog(['Preparing pipeline', ' ▶ npm test ', 'existing command output']); expect(wrapper.findAll('.sticky')).toHaveLength(1); - expect(wrapper.get('.sticky span#L2').element.textContent).toBe('▶ npm test \n'); - expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1']); + expect(wrapper.get('.sticky a#L2').text()).toBe('2'); + expect(wrapper.get('.sticky .truncate > :last-child').element.textContent).toBe(' npm test \n'); + expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '2']); await wrapper.get('button[title="repo.pipeline.actions.expand_all"]').trigger('click'); expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '2', '3']); - expect(wrapper.get('a#L2').element.nextElementSibling?.textContent).toBe(' ▶ npm test \n'); expect(wrapper.get('a#L3').element.nextElementSibling?.textContent).toBe('existing command output\n'); await wrapper.get('.sticky').trigger('click'); - expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1']); + expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '2']); }); describe('arbitrary log groups', () => { @@ -245,13 +248,14 @@ const wrapper = await loadLog(lines); expect(getLogs).toHaveBeenCalledWith(1, 1, 7); - expect(wrapper.findAll('.sticky span[id]').map((heading) => heading.text())).toEqual([ + expect(wrapper.findAll('.sticky .truncate > :last-child').map((heading) => heading.text())).toEqual([ 'Build <assets> & bundle', 'Test suite', ]); - expect(wrapper.findAll('.sticky span[id]').map((heading) => heading.attributes('id'))).toEqual(['L2', 'L4']); + expect(wrapper.findAll('.sticky a[id]').map((heading) => heading.attributes('id'))).toEqual(['L2', 'L4']); expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '2', '3', '4', '5']); for (const [index, text] of lines.entries()) { + if (text.startsWith('▶')) continue; expect(wrapper.get(`a#L${index + 1}`).element.nextElementSibling?.textContent).toBe(`${text}\n`); } }); @@ -259,10 +263,10 @@ it('starts only at a nonempty heading and ends at the next heading or end of log', async () => { const wrapper = await loadLog(['▶ ', '▶ \t', '▶ B', 'build output', '▶ T', 'test output', 'last output']); - expect(wrapper.findAll('.sticky span[id]').map((heading) => heading.text())).toEqual(['B', 'T']); + expect(wrapper.findAll('.sticky .truncate > :last-child').map((heading) => heading.text())).toEqual(['B', 'T']); await wrapper.get('.sticky').trigger('click'); - expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '2', '5', '6', '7']); + expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '2', '3', '5', '6', '7']); }); it('collapses and expands an individual group without hiding the next group or preamble', async () => { @@ -270,18 +274,38 @@ await wrapper.get('.sticky').trigger('click'); - expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '4', '5']); - expect(wrapper.get('.sticky span').text()).toBe('Build'); + expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '2', '4', '5']); + expect(wrapper.get('.sticky .truncate > :last-child').text()).toBe('Build'); await wrapper.get('.sticky').trigger('click'); expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '2', '3', '4', '5']); }); + it('numbers the group header in place of a duplicated command line', async () => { + const wrapper = await loadLog(['Preparing', '▶ Build', 'built']); + + expect(wrapper.findAll('.sticky a[href^="#L"]').map((line) => line.text())).toEqual(['2']); + expect(wrapper.get('a#L2').attributes('href')).toBe('#L2'); + expect(wrapper.findAll('a#L2')).toHaveLength(1); + expect(wrapper.get('a#L3').element.nextElementSibling?.textContent).toBe('built\n'); + expect(wrapper.get('.sticky').element.lastElementChild?.textContent?.trim()).toBe('1s'); + // the marker stays in the header (invisible) so copied text matches the raw log + expect(wrapper.get('.sticky .truncate').element.textContent).toBe('▶ Build\n'); + await wrapper.get('a#L2').trigger('click'); + expect(wrapper.find('a#L3').exists()).toBe(true); + }); + + it('shows the full plain heading as hover title of the truncated header', async () => { + const wrapper = await loadLog(['Preparing', '▶ Build <assets> & bundle ', 'built']); + + expect(wrapper.get('.sticky .truncate').attributes('title')).toBe('Build <assets> & bundle'); + }); + it('collapses and expands all groups while retaining initialization', async () => { const wrapper = await loadLog(['Preparing', '▶ Build', 'built', '▶ Test', 'passed']); await wrapper.get('button[title="repo.pipeline.actions.collapse_all"]').trigger('click'); - expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1']); + expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '2', '4']); expect(wrapper.findAll('.sticky')).toHaveLength(2); await wrapper.get('button[title="repo.pipeline.actions.expand_all"]').trigger('click'); expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '2', '3', '4', '5']); @@ -341,8 +365,11 @@ } await flushLogs(); - expect(wrapper.findAll('.sticky span[id]').map((heading) => heading.text())).toEqual(['Build', 'Test suite']); - expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '5', '6']); + expect(wrapper.findAll('.sticky .truncate > :last-child').map((heading) => heading.text())).toEqual([ + 'Build', + 'Test suite', + ]); + expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '2', '5', '6']); await wrapper.get('.sticky').trigger('click'); expect(wrapper.get('a#L4').element.nextElementSibling?.textContent).toBe('later output\n'); expect(wrapper.findAll('a[href^="#L"]').map((line) => line.text())).toEqual(['1', '2', '3', '4', '5', '6']); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/woodpecker-3.20.0/web/src/components/repo/pipeline/PipelineLog.vue new/woodpecker-3.20.1/web/src/components/repo/pipeline/PipelineLog.vue --- old/woodpecker-3.20.0/web/src/components/repo/pipeline/PipelineLog.vue 2026-10-28 19:37:53.000000000 +0100 +++ new/woodpecker-3.20.1/web/src/components/repo/pipeline/PipelineLog.vue 2026-10-29 20:38:42.000000000 +0100 @@ -78,26 +78,38 @@ > <div v-for="group in groupedLogs" :key="group.id" class="contents"> <div - v-if="group.isActualCommand" - class="sticky -top-4 z-10 col-span-3 my-1 flex cursor-pointer items-center rounded-sm px-2 py-1 font-mono text-sm shadow-xs" - :class="[group.command && isSelected(group.command) ? 'bg-blue-900 dark:bg-blue-800' : 'bg-wp-code-100']" + v-if="group.isActualCommand && group.command" + class="sticky -top-4 z-10 col-span-3 grid cursor-pointer grid-cols-subgrid rounded-sm font-mono shadow-xs" + :class="[isSelected(group.command) ? 'bg-blue-900 dark:bg-blue-800' : 'bg-wp-code-100']" @click="toggleGroup(group.id)" > - <Icon - name="chevron-right" - class="mr-2 transition-transform" - :class="{ - 'rotate-90': !collapsedCommands.has(group.id), - invisible: group.lines.length === 0, - }" - /> - <!-- eslint-disable vue/no-v-html --> - <span - v-if="group.command" - :id="`L${group.command?.number}`" - class="flex-1 truncate" - v-html="group.command.text?.substring(2)" - /> + <a + :id="`L${group.command.number}`" + :href="`#L${group.command.number}`" + class="text-wp-code-text-alt-100 pr-6 pl-2 text-right whitespace-nowrap select-none" + :class="{ underline: isSelected(group.command) }" + @click.stop + > + {{ group.command.number }} + </a> + <div class="flex min-w-0 items-center"> + <Icon + name="chevron-right" + class="mr-1 size-4 shrink-0 transition-transform" + :class="{ + 'rotate-90': !collapsedCommands.has(group.id), + invisible: group.lines.length === 0, + }" + /> + <span class="flex-1 truncate" :title="group.command.rawText?.trim().slice(commandMarker.length).trim()"> + <span class="text-[0px] whitespace-pre" aria-hidden="true" v-text="commandMarker" /> + <!-- eslint-disable vue/no-v-html --> + <span v-html="group.command.text?.substring(3)" /> + </span> + </div> + <span class="text-wp-code-text-alt-100 pr-1 text-right whitespace-nowrap select-none"> + {{ formatTime(group.command.time) }} + </span> </div> <template v-if="!collapsedCommands.has(group.id)"> @@ -247,6 +259,8 @@ const collapsedCommands = ref(new Set<number>()); +const commandMarker = '▶ '; + const groupedLogs = computed(() => { if (!log.value) return []; @@ -255,12 +269,12 @@ log.value.forEach((line) => { const trimmedText = (line.rawText || '').trim(); - const isCommand = trimmedText.startsWith('▶ ') && trimmedText.slice(3).trim().length > 0; + const isCommand = trimmedText.startsWith(commandMarker) && trimmedText.slice(3).trim().length > 0; if (isCommand) { currentBlock = { command: line, - lines: [line], + lines: [], id: line.number, isActualCommand: true, }; ++++++ woodpecker.obsinfo ++++++ --- /var/tmp/diff_new_pack.l5eNom/_old 2026-10-30 18:22:40.852340394 +0100 +++ /var/tmp/diff_new_pack.l5eNom/_new 2026-10-30 18:22:40.858340646 +0100 @@ -1,5 +1,5 @@ name: woodpecker -version: 3.20.0 -mtime: 1793212673 -commit: f10ce96d3c73ee8cd4608a3cc89b403cd0863d05 +version: 3.20.1 +mtime: 1793302722 +commit: 3f1bb0bd0db28e410ad797034d75780116122c91
