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
 

Reply via email to