Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package semaphore for openSUSE:Factory 
checked in at 2026-10-09 21:45:13
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/semaphore (Old)
 and      /work/SRC/openSUSE:Factory/.semaphore.new.3698736 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "semaphore"

Fri Oct  9 21:45:13 2026 rev:58 rq:1383678 version:2.19.16

Changes:
--------
--- /work/SRC/openSUSE:Factory/semaphore/semaphore.changes      2026-09-18 
22:08:11.756761147 +0200
+++ /work/SRC/openSUSE:Factory/.semaphore.new.3698736/semaphore.changes 
2026-10-09 21:45:19.892187084 +0200
@@ -1,0 +2,39 @@
+Fri Oct 09 05:17:38 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 2.19.16 (no releases between .12 and this one):
+  * Configurable runner check interval
+    Runners can now be told how often to ask the server for new
+    jobs. Until now the interval was hard-coded to 1 second, which
+    on installations with many runners produced a noticeable amount
+    of request traffic.
+    New option in the runner section of the runner config:
+
+      {
+        "runner": {
+          "check_interval_seconds": 5
+        }
+      }
+
+    Or via environment variable:
+
+      SEMAPHORE_RUNNER_CHECK_INTERVAL_SECONDS=5
+
+    Default: 1 second, so existing installations behave exactly as
+    before. Values of 0, negative values, or values too large to
+    represent as a duration fall back to the default.
+
+    What changes and what does not. Only the check for new jobs
+    honours the configured interval. The runner's progress report,
+    which also serves as its heartbeat, keeps running once per
+    second. Raising the check interval therefore does not affect
+    when the server marks a runner offline. Jobs will start up to
+    check_interval_seconds later than before, so pick a value that
+    matches how quickly you need new tasks to be picked up.
+    Affected components: runner (semaphore runner start), config
+    schema. No database migration, no changes to the server or the
+    web interface.
+  * refactor(config): remove redundant conf.Dirs nil checks in
+    GetSecretsPath
+  * fix: old secrets path in config
+
+-------------------------------------------------------------------

Old:
----
  semaphore-2.19.12.obscpio
  web-2.19.12.tar.gz

New:
----
  semaphore-2.19.16.obscpio
  web-2.19.16.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ semaphore.spec ++++++
--- /var/tmp/diff_new_pack.JM6mFS/_old  2026-10-09 21:45:27.096490237 +0200
+++ /var/tmp/diff_new_pack.JM6mFS/_new  2026-10-09 21:45:27.097490279 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           semaphore
-Version:        2.19.12
+Version:        2.19.16
 Release:        0
 Summary:        Modern UI for Ansible
 License:        MIT

++++++ _service ++++++
--- /var/tmp/diff_new_pack.JM6mFS/_old  2026-10-09 21:45:27.170493351 +0200
+++ /var/tmp/diff_new_pack.JM6mFS/_new  2026-10-09 21:45:27.176493603 +0200
@@ -4,8 +4,8 @@
     <param name="scm">git</param>
     <param name="submodules">disable</param>
     <param name="exclude">.git</param>
-    <param name="revision">refs/tags/v2.19.12</param>
-    <param name="match-tag">v2.19.12</param>
+    <param name="revision">refs/tags/v2.19.16</param>
+    <param name="match-tag">v2.19.16</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.JM6mFS/_old  2026-10-09 21:45:27.221495497 +0200
+++ /var/tmp/diff_new_pack.JM6mFS/_new  2026-10-09 21:45:27.226495707 +0200
@@ -3,6 +3,6 @@
                 <param 
name="url">https://github.com/ansible-semaphore/semaphore</param>
               <param 
name="changesrevision">8a4dcf0868af718aaa5871368a3247dd622521f4</param></service><service
 name="tar_scm">
                 <param 
name="url">https://github.com/ansible-semaphore/semaphore.git</param>
-              <param 
name="changesrevision">012ed06d3eccadaed594c73b93b3d8a2459b576f</param></service></servicedata>
+              <param 
name="changesrevision">f8cefe62da4d40e536323e3dd936eb2e04e82b1a</param></service></servicedata>
 (No newline at EOF)
 

++++++ semaphore-2.19.12.obscpio -> semaphore-2.19.16.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/semaphore-2.19.12/config.schema.yaml 
new/semaphore-2.19.16/config.schema.yaml
--- old/semaphore-2.19.12/config.schema.yaml    2026-08-30 12:30:17.000000000 
+0200
+++ new/semaphore-2.19.16/config.schema.yaml    2026-10-07 07:29:04.000000000 
+0200
@@ -572,6 +572,14 @@
       enabled:            { type: boolean }
       project_id:
         type: [integer, "null"]
+      check_interval_seconds:
+        type: integer
+        minimum: 1
+        default: 1
+        description: >
+          How often the runner polls the server for new jobs. Raising it 
reduces
+          request volume on a server with many runners, at the cost of jobs
+          starting slightly later.
       connection: { $ref: "#/$defs/RunnerConnectionConfig" }
       executor:   { $ref: "#/$defs/ExecutorConfig" }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/semaphore-2.19.12/services/runners/job_pool.go 
new/semaphore-2.19.16/services/runners/job_pool.go
--- old/semaphore-2.19.12/services/runners/job_pool.go  2026-08-30 
12:30:17.000000000 +0200
+++ new/semaphore-2.19.16/services/runners/job_pool.go  2026-10-07 
07:29:04.000000000 +0200
@@ -256,6 +256,11 @@
        return
 }
 
+// runnerProgressInterval is how often the runner reports progress. It is not
+// configurable: the report is also the heartbeat the server uses to decide a
+// runner is still alive.
+const runnerProgressInterval = time.Second
+
 func (p *JobPool) Run() {
        launched := false
 
@@ -265,8 +270,20 @@
                }).Panic("runner token is empty, cannot start the runner")
        }
 
+       checkInterval := util.Config.RunnerCheckInterval()
+
+       log.WithFields(log.Fields{
+               "context":        "job_running",
+               "check_interval": checkInterval,
+       }).Debug("Runner poll interval")
+
+       // The progress report doubles as the runner's heartbeat, and the 
server marks
+       // a runner offline after a fixed timeout, so its cadence is fixed. 
Only the
+       // job check honours the configured interval.
+       lastJobCheck := time.Time{}
+
        queueTicker := time.NewTicker(5 * time.Second)
-       requestTimer := time.NewTicker(1 * time.Second)
+       requestTimer := time.NewTicker(runnerProgressInterval)
        p.resetRunningJobs()
 
        defer func() {
@@ -402,7 +419,10 @@
                                        os.Exit(0)
                                }
 
-                               p.checkNewJobs()
+                               if time.Since(lastJobCheck) >= checkInterval {
+                                       lastJobCheck = time.Now()
+                                       p.checkNewJobs()
+                               }
                        }()
 
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/semaphore-2.19.12/services/server/access_key_serializer_local.go 
new/semaphore-2.19.16/services/server/access_key_serializer_local.go
--- old/semaphore-2.19.12/services/server/access_key_serializer_local.go        
2026-08-30 12:30:17.000000000 +0200
+++ new/semaphore-2.19.16/services/server/access_key_serializer_local.go        
2026-10-07 07:29:04.000000000 +0200
@@ -119,7 +119,7 @@
                                }
                        }
 
-                       secretsBasePath := 
filepath.Clean(util.Config.Dirs.Secrets)
+                       secretsBasePath := 
filepath.Clean(util.Config.GetSecretsPath())
                        if !filepath.IsAbs(secretsBasePath) {
                                err = common_errors.NewUserErrorS("secrets path 
must be absolute")
                                return
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/semaphore-2.19.12/util/config.go 
new/semaphore-2.19.16/util/config.go
--- old/semaphore-2.19.12/util/config.go        2026-08-30 12:30:17.000000000 
+0200
+++ new/semaphore-2.19.16/util/config.go        2026-10-07 07:29:04.000000000 
+0200
@@ -9,6 +9,7 @@
        "errors"
        "fmt"
        "io"
+       "math"
        "net/url"
        "os"
        "os/exec"
@@ -167,6 +168,10 @@
        MaxParallelTasks int      `json:"max_parallel_tasks,omitempty" 
default:"9999" env:"SEMAPHORE_RUNNER_MAX_PARALLEL_TASKS"`
        ProjectID        *int     `json:"project_id,omitempty" 
env:"SEMAPHORE_RUNNER_PROJECT_ID"`
 
+       // CheckIntervalSeconds is how often the runner polls the server for 
new jobs.
+       // Plain int, not time.Duration, for env-binding simplicity.
+       CheckIntervalSeconds int `json:"check_interval_seconds,omitempty" 
default:"1" env:"SEMAPHORE_RUNNER_CHECK_INTERVAL_SECONDS"`
+
        Connection *RunnerConnectionConfig `json:"connection,omitempty"`
 
        Executor *ExecutorConfig `json:"executor,omitempty" 
env:"SEMAPHORE_RUNNER_EXECUTOR"`
@@ -487,6 +492,10 @@
        // semaphore stores ephemeral projects here
        TmpPath string `json:"tmp_path,omitempty" default:"/tmp/semaphore" 
env:"SEMAPHORE_TMP_PATH"`
 
+       // SecretsPath is a legacy top-level setting for backwards 
compatibility.
+       // Users should prefer configuring dirs.secrets instead.
+       SecretsPath string `json:"secrets_path,omitempty" 
env:"SEMAPHORE_SECRETS_PATH"`
+
        // HomeDirMode controls how the HOME environment variable is set for 
tasks.
        //   "template_home" (default) — HOME is set to a per-template 
directory,
        //       isolating .ansible/ across parallel tasks. Repo is cloned into 
a
@@ -628,6 +637,21 @@
        defaultRunnersReconcileIntervalSec = 30
 )
 
+// GetSecretsPath returns the secrets path from configuration.
+// Used for backward compatibility with legacy top-level secrets_path.
+func (conf *ConfigType) GetSecretsPath() string {
+       if conf.Dirs.Secrets != "" && conf.Dirs.Secrets != "/tmp/semaphore" {
+               return conf.Dirs.Secrets
+       }
+       if conf.SecretsPath != "" {
+               return conf.SecretsPath
+       }
+       if conf.Dirs.Secrets != "" {
+               return conf.Dirs.Secrets
+       }
+       return "/tmp/semaphore"
+}
+
 // RunnersOfflineTimeout returns the heartbeat staleness after which a runner
 // is considered offline (no new tasks; its "starting" tasks are reassigned).
 func (conf *ConfigType) RunnersOfflineTimeout() time.Duration {
@@ -652,6 +676,25 @@
        return res
 }
 
+// Default poll interval for a runner asking the server for work.
+const defaultRunnerCheckIntervalSec = 1
+
+// Larger overflows time.Duration and panics time.NewTicker. Kept as int64: the
+// value exceeds int on 32-bit release targets (386, arm).
+const maxRunnerCheckIntervalSec int64 = int64(math.MaxInt64) / 
int64(time.Second)
+
+// RunnerCheckInterval returns how often this runner polls the server for new
+// jobs. Out-of-range values fall back to the default: 0 is indistinguishable
+// from "unset" after defaults are applied, and oversized values overflow.
+func (conf *ConfigType) RunnerCheckInterval() time.Duration {
+       sec := int64(defaultRunnerCheckIntervalSec)
+       if configured := int64(conf.Runner.CheckIntervalSeconds); configured > 
0 &&
+               configured <= maxRunnerCheckIntervalSec {
+               sec = configured
+       }
+       return time.Duration(sec) * time.Second
+}
+
 // RunnersReconcileInterval returns how often the server reconciles dispatched
 // tasks against runner liveness.
 func (conf *ConfigType) RunnersReconcileInterval() time.Duration {
@@ -927,10 +970,18 @@
 }
 
 func loadConfigDefaults() {
+       legacySecretsPath := Config.SecretsPath
+       if Config.Dirs == nil {
+               Config.Dirs = &ConfigDirs{}
+       }
        err := loadDefaultsToObject(Config)
        if err != nil {
                panic(err)
        }
+
+       if legacySecretsPath != "" && (Config.Dirs.Secrets == "/tmp/semaphore" 
|| Config.Dirs.Secrets == "") {
+               Config.Dirs.Secrets = legacySecretsPath
+       }
 }
 
 func castStringToInt(value string) int {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/semaphore-2.19.12/util/config_test.go 
new/semaphore-2.19.16/util/config_test.go
--- old/semaphore-2.19.12/util/config_test.go   2026-08-30 12:30:17.000000000 
+0200
+++ new/semaphore-2.19.16/util/config_test.go   2026-10-07 07:29:04.000000000 
+0200
@@ -141,6 +141,36 @@
        }
 }
 
+func TestLoadEnvironmentToObject_RunnerExecutor(t *testing.T) {
+       var val RunnerConfig
+
+       origExecutor, hadExecutor := os.LookupEnv("SEMAPHORE_RUNNER_EXECUTOR")
+       origNetwork, hadNetwork := 
os.LookupEnv("SEMAPHORE_RUNNER_DOCKER_NETWORK")
+       t.Cleanup(func() {
+               if hadExecutor {
+                       _ = os.Setenv("SEMAPHORE_RUNNER_EXECUTOR", origExecutor)
+               } else {
+                       _ = os.Unsetenv("SEMAPHORE_RUNNER_EXECUTOR")
+               }
+               if hadNetwork {
+                       _ = os.Setenv("SEMAPHORE_RUNNER_DOCKER_NETWORK", 
origNetwork)
+               } else {
+                       _ = os.Unsetenv("SEMAPHORE_RUNNER_DOCKER_NETWORK")
+               }
+       })
+       require.NoError(t, os.Setenv("SEMAPHORE_RUNNER_EXECUTOR", 
`{"type":"docker","docker":{"image":"example.com/job:1"}}`))
+       require.NoError(t, os.Setenv("SEMAPHORE_RUNNER_DOCKER_NETWORK", "host"))
+
+       _, err := loadEnvironmentToObject(&val)
+       require.NoError(t, err)
+
+       require.NotNil(t, val.Executor)
+       assert.Equal(t, ExecutorTypeDocker, val.Executor.Type)
+       assert.Equal(t, "example.com/job:1", val.Executor.Docker.Image)
+       // individual executor env vars still override fields of the JSON object
+       assert.Equal(t, "host", val.Executor.Docker.Network)
+}
+
 func TestLoadEnvironmentToObject_SensitiveEnvs(t *testing.T) {
        type sub struct {
                Field string `json:"field"`
@@ -585,3 +615,53 @@
        ensureConfigValidationFailure(t, "AccessKeyEncryption", 
Config.AccessKeyEncryption)
        Config.AccessKeyEncryption = testCookieHash
 }
+
+func TestGetSecretsPath_DirsSecrets(t *testing.T) {
+       Config = NewConfigType()
+       Config.Dirs = &ConfigDirs{Secrets: "/custom/dirs/secrets"}
+       loadConfigDefaults()
+       assert.Equal(t, "/custom/dirs/secrets", Config.GetSecretsPath())
+}
+
+func TestGetSecretsPath_LegacySecretsPath(t *testing.T) {
+       Config = NewConfigType()
+       Config.SecretsPath = "/legacy/secrets/path"
+       loadConfigDefaults()
+       assert.Equal(t, "/legacy/secrets/path", Config.GetSecretsPath())
+       require.NotNil(t, Config.Dirs)
+       assert.Equal(t, "/legacy/secrets/path", Config.Dirs.Secrets)
+}
+
+func TestGetSecretsPath_Precedence(t *testing.T) {
+       Config = NewConfigType()
+       Config.Dirs = &ConfigDirs{Secrets: "/custom/dirs/secrets"}
+       Config.SecretsPath = "/legacy/secrets/path"
+       loadConfigDefaults()
+       assert.Equal(t, "/custom/dirs/secrets", Config.GetSecretsPath())
+}
+
+func TestGetSecretsPath_Default(t *testing.T) {
+       Config = NewConfigType()
+       loadConfigDefaults()
+       assert.Equal(t, "/tmp/semaphore", Config.GetSecretsPath())
+       require.NotNil(t, Config.Dirs)
+       assert.Equal(t, "/tmp/semaphore", Config.Dirs.SSHAgentSockets)
+}
+
+func TestGetSecretsPath_Env(t *testing.T) {
+       Config = NewConfigType()
+       orig, existed := os.LookupEnv("SEMAPHORE_SECRETS_PATH")
+       t.Cleanup(func() {
+               if existed {
+                       _ = os.Setenv("SEMAPHORE_SECRETS_PATH", orig)
+               } else {
+                       _ = os.Unsetenv("SEMAPHORE_SECRETS_PATH")
+               }
+       })
+       _ = os.Setenv("SEMAPHORE_SECRETS_PATH", "/env/secrets/path")
+
+       loadConfigEnvironment()
+       loadConfigDefaults()
+
+       assert.Equal(t, "/env/secrets/path", Config.GetSecretsPath())
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/semaphore-2.19.12/util/runner_check_interval_test.go 
new/semaphore-2.19.16/util/runner_check_interval_test.go
--- old/semaphore-2.19.12/util/runner_check_interval_test.go    1970-01-01 
01:00:00.000000000 +0100
+++ new/semaphore-2.19.16/util/runner_check_interval_test.go    2026-10-07 
07:29:04.000000000 +0200
@@ -0,0 +1,94 @@
+package util
+
+import (
+       "math"
+       "testing"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+// intHoldsOverflowingInterval reports whether an int is wide enough to 
express a
+// value past the duration limit. On 32-bit targets (386, arm) it is not, so
+// those cases cannot occur there and are skipped.
+const intHoldsOverflowingInterval = int64(math.MaxInt) > 
maxRunnerCheckIntervalSec
+
+// A variable, not a constant: int(maxIntervalSec) would be a constant
+// conversion and fail to compile on 32-bit even inside a guarded branch.
+var maxIntervalSec = maxRunnerCheckIntervalSec
+
+func TestRunnerCheckInterval(t *testing.T) {
+       tests := []struct {
+               name     string
+               runner   *RunnerConfig
+               expected time.Duration
+       }{
+               {"unset field", &RunnerConfig{}, time.Second},
+               {"zero falls back", &RunnerConfig{CheckIntervalSeconds: 0}, 
time.Second},
+               {"negative falls back", &RunnerConfig{CheckIntervalSeconds: 
-5}, time.Second},
+               {"configured", &RunnerConfig{CheckIntervalSeconds: 10}, 10 * 
time.Second},
+               {"large", &RunnerConfig{CheckIntervalSeconds: 300}, 5 * 
time.Minute},
+               {"largest int", &RunnerConfig{CheckIntervalSeconds: 
math.MaxInt},
+                       time.Duration(clampInterval(math.MaxInt)) * 
time.Second},
+       }
+
+       if intHoldsOverflowingInterval {
+               tests = append(tests,
+                       struct {
+                               name     string
+                               runner   *RunnerConfig
+                               expected time.Duration
+                       }{"max representable", 
&RunnerConfig{CheckIntervalSeconds: int(maxIntervalSec)},
+                               time.Duration(maxRunnerCheckIntervalSec) * 
time.Second},
+                       struct {
+                               name     string
+                               runner   *RunnerConfig
+                               expected time.Duration
+                       }{"overflows falls back", 
&RunnerConfig{CheckIntervalSeconds: int(maxIntervalSec) + 1},
+                               time.Second},
+               )
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       conf := &ConfigType{Runner: tt.runner}
+                       assert.Equal(t, tt.expected, conf.RunnerCheckInterval())
+               })
+       }
+}
+
+func clampInterval(sec int) int64 {
+       v := int64(sec)
+       if v > 0 && v <= maxRunnerCheckIntervalSec {
+               return v
+       }
+       return int64(defaultRunnerCheckIntervalSec)
+}
+
+// The result must always be safe for time.NewTicker.
+func TestRunnerCheckIntervalNeverPanicsTicker(t *testing.T) {
+       secs := []int{0, -1, 1, 300, math.MaxInt}
+       if intHoldsOverflowingInterval {
+               secs = append(secs, int(maxIntervalSec), int(maxIntervalSec)+1)
+       }
+
+       for _, sec := range secs {
+               conf := &ConfigType{Runner: &RunnerConfig{CheckIntervalSeconds: 
sec}}
+               d := conf.RunnerCheckInterval()
+
+               assert.Positive(t, d, "interval must stay positive for 
CheckIntervalSeconds=%d", sec)
+               assert.NotPanics(t, func() {
+                       time.NewTicker(d).Stop()
+               }, "time.NewTicker must accept the interval for 
CheckIntervalSeconds=%d", sec)
+       }
+}
+
+// The default tag must match the constant.
+func TestRunnerCheckIntervalDefaultMatchesTag(t *testing.T) {
+       conf := &ConfigType{Runner: &RunnerConfig{}}
+       require.NoError(t, loadDefaultsToObject(conf))
+
+       assert.Equal(t, defaultRunnerCheckIntervalSec, 
conf.Runner.CheckIntervalSeconds)
+       assert.Equal(t, time.Second, conf.RunnerCheckInterval())
+}

++++++ semaphore.obsinfo ++++++
--- /var/tmp/diff_new_pack.JM6mFS/_old  2026-10-09 21:45:28.413545658 +0200
+++ /var/tmp/diff_new_pack.JM6mFS/_new  2026-10-09 21:45:28.420545952 +0200
@@ -1,5 +1,5 @@
 name: semaphore
-version: 2.19.12
-mtime: 1788085817
-commit: 012ed06d3eccadaed594c73b93b3d8a2459b576f
+version: 2.19.16
+mtime: 1791350944
+commit: f8cefe62da4d40e536323e3dd936eb2e04e82b1a
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/semaphore/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.semaphore.new.3698736/vendor.tar.gz differ: char 
151, line 1

++++++ web-2.19.12.tar.gz -> web-2.19.16.tar.gz ++++++
/work/SRC/openSUSE:Factory/semaphore/web-2.19.12.tar.gz 
/work/SRC/openSUSE:Factory/.semaphore.new.3698736/web-2.19.16.tar.gz differ: 
char 13, line 1

Reply via email to