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-07-28 18:19:55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/semaphore (Old) and /work/SRC/openSUSE:Factory/.semaphore.new.2004 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "semaphore" Tue Jul 28 18:19:55 2026 rev:54 rq:1368196 version:2.18.29 Changes: -------- --- /work/SRC/openSUSE:Factory/semaphore/semaphore.changes 2026-07-23 23:14:18.368627877 +0200 +++ /work/SRC/openSUSE:Factory/.semaphore.new.2004/semaphore.changes 2026-07-28 18:21:44.694566787 +0200 @@ -1,0 +2,7 @@ +Tue Jul 28 11:25:29 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 2.18.29: + * fix(secrets): do not remove synchronized fields and readoly + fields + +------------------------------------------------------------------- Old: ---- semaphore-2.18.28.obscpio web-2.18.28.tar.gz New: ---- semaphore-2.18.29.obscpio web-2.18.29.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ semaphore.spec ++++++ --- /var/tmp/diff_new_pack.H4ct5J/_old 2026-07-28 18:21:50.814780340 +0200 +++ /var/tmp/diff_new_pack.H4ct5J/_new 2026-07-28 18:21:50.822780619 +0200 @@ -17,7 +17,7 @@ Name: semaphore -Version: 2.18.28 +Version: 2.18.29 Release: 0 Summary: Modern UI for Ansible License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.H4ct5J/_old 2026-07-28 18:21:50.906783551 +0200 +++ /var/tmp/diff_new_pack.H4ct5J/_new 2026-07-28 18:21:50.910783690 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/ansible-semaphore/semaphore.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">refs/tags/v2.18.28</param> + <param name="revision">refs/tags/v2.18.29</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.H4ct5J/_old 2026-07-28 18:21:50.930784388 +0200 +++ /var/tmp/diff_new_pack.H4ct5J/_new 2026-07-28 18:21:50.934784528 +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">33874e063dd9d7dc441e11f7fbfc24c0c28b270f</param></service></servicedata> + <param name="changesrevision">91719b944ccf17e23cbf5e1955a1caea32a58c5c</param></service></servicedata> (No newline at EOF) ++++++ semaphore-2.18.28.obscpio -> semaphore-2.18.29.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/semaphore-2.18.28/api/projects/environment.go new/semaphore-2.18.29/api/projects/environment.go --- old/semaphore-2.18.28/api/projects/environment.go 2026-07-22 14:24:43.000000000 +0200 +++ new/semaphore-2.18.29/api/projects/environment.go 2026-07-28 07:08:59.000000000 +0200 @@ -296,7 +296,6 @@ env := helpers.GetFromContext(r, "environment").(db.Environment) err := c.environmentService.Delete(env.ProjectID, env.ID) - //err := helpers.Store(r).DeleteEnvironment(env.ProjectID, env.ID) if errors.Is(err, db.ErrInvalidOperation) { helpers.WriteJSON(w, http.StatusBadRequest, map[string]any{ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/semaphore-2.18.28/cli/cmd/root.go new/semaphore-2.18.29/cli/cmd/root.go --- old/semaphore-2.18.28/cli/cmd/root.go 2026-07-22 14:24:43.000000000 +0200 +++ new/semaphore-2.18.29/cli/cmd/root.go 2026-07-28 07:08:59.000000000 +0200 @@ -97,7 +97,7 @@ accessKeyService := server.NewAccessKeyService(store, encryptionService, store) secretStorageService := server.NewSecretStorageService(store, store, accessKeyService, encryptionService) secretStorageSyncScheduler := server.NewSecretStorageSyncScheduler(store, secretStorageService) - environmentService := server.NewEnvironmentService(store, encryptionService) + environmentService := server.NewEnvironmentService(store, encryptionService, store) runnerService := server.NewRunnerService(store) subscriptionService := proServer.NewSubscriptionService(store, store, store, terraformStore) logWriteService := proServer.NewLogWriteService() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/semaphore-2.18.28/services/server/environment_svc.go new/semaphore-2.18.29/services/server/environment_svc.go --- old/semaphore-2.18.28/services/server/environment_svc.go 2026-07-22 14:24:43.000000000 +0200 +++ new/semaphore-2.18.29/services/server/environment_svc.go 2026-07-28 07:08:59.000000000 +0200 @@ -13,16 +13,19 @@ func NewEnvironmentService( environmentRepo db.EnvironmentManager, encryptionService AccessKeyEncryptionService, + secretStorageRepo db.SecretStorageRepository, ) EnvironmentService { return &EnvironmentServiceImpl{ environmentRepo: environmentRepo, encryptionService: encryptionService, + secretStorageRepo: secretStorageRepo, } } type EnvironmentServiceImpl struct { environmentRepo db.EnvironmentManager encryptionService AccessKeyEncryptionService + secretStorageRepo db.SecretStorageRepository } func (s *EnvironmentServiceImpl) Delete(projectID int, environmentID int) (err error) { @@ -30,6 +33,11 @@ return fmt.Errorf("invalid project or environment ID") } + env, err := s.environmentRepo.GetEnvironment(projectID, environmentID) + if err != nil { + return + } + secrets, err := s.environmentRepo.GetEnvironmentSecrets(projectID, environmentID) if err != nil { return @@ -43,10 +51,23 @@ var errors []error - for _, secret := range secrets { - err = s.encryptionService.DeleteSecret(&secret) + if env.SecretStorageID != nil { + var storage db.SecretStorage + storage, err = s.secretStorageRepo.GetSecretStorage(projectID, *env.SecretStorageID) if err != nil { - errors = append(errors, err) + return + } + + if !storage.ReadOnly { + for _, secret := range secrets { + if secret.Synchronized { + continue + } + err = s.encryptionService.DeleteSecret(&secret) + if err != nil { + errors = append(errors, err) + } + } } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/semaphore-2.18.28/services/server/environment_svc_test.go new/semaphore-2.18.29/services/server/environment_svc_test.go --- old/semaphore-2.18.28/services/server/environment_svc_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/semaphore-2.18.29/services/server/environment_svc_test.go 2026-07-28 07:08:59.000000000 +0200 @@ -0,0 +1,337 @@ +package server + +import ( + "errors" + "testing" + + "github.com/semaphoreui/semaphore/db" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockEnvironmentManager struct { + GetEnvironmentFn func(projectID int, environmentID int) (db.Environment, error) + GetEnvironmentSecretsFn func(projectID int, environmentID int) ([]db.AccessKey, error) + DeleteEnvironmentFn func(projectID int, environmentID int) error + + DeleteEnvironmentCalls int +} + +func (m *mockEnvironmentManager) GetEnvironment(projectID int, environmentID int) (db.Environment, error) { + if m.GetEnvironmentFn != nil { + return m.GetEnvironmentFn(projectID, environmentID) + } + return db.Environment{}, nil +} + +func (m *mockEnvironmentManager) GetEnvironmentSecrets(projectID int, environmentID int) ([]db.AccessKey, error) { + if m.GetEnvironmentSecretsFn != nil { + return m.GetEnvironmentSecretsFn(projectID, environmentID) + } + return nil, nil +} + +func (m *mockEnvironmentManager) DeleteEnvironment(projectID int, environmentID int) error { + m.DeleteEnvironmentCalls++ + if m.DeleteEnvironmentFn != nil { + return m.DeleteEnvironmentFn(projectID, environmentID) + } + return nil +} + +// Stub methods to satisfy db.EnvironmentManager +func (m *mockEnvironmentManager) GetEnvironmentRefs(projectID int, environmentID int) (db.ObjectReferrers, error) { + return db.ObjectReferrers{}, nil +} +func (m *mockEnvironmentManager) GetEnvironments(projectID int, params db.RetrieveQueryParams) ([]db.Environment, error) { + return nil, nil +} +func (m *mockEnvironmentManager) UpdateEnvironment(env db.Environment) error { return nil } +func (m *mockEnvironmentManager) CreateEnvironment(env db.Environment) (db.Environment, error) { + return db.Environment{}, nil +} + +type mockSecretStorageRepository struct { + GetSecretStorageFn func(projectID int, storageID int) (db.SecretStorage, error) +} + +func (m *mockSecretStorageRepository) GetSecretStorage(projectID int, storageID int) (db.SecretStorage, error) { + if m.GetSecretStorageFn != nil { + return m.GetSecretStorageFn(projectID, storageID) + } + return db.SecretStorage{}, nil +} + +// Stub methods to satisfy db.SecretStorageRepository +func (m *mockSecretStorageRepository) GetSecretStorages(projectID int) ([]db.SecretStorage, error) { + return nil, nil +} +func (m *mockSecretStorageRepository) CreateSecretStorage(storage db.SecretStorage) (db.SecretStorage, error) { + return db.SecretStorage{}, nil +} +func (m *mockSecretStorageRepository) UpdateSecretStorage(storage db.SecretStorage) error { return nil } +func (m *mockSecretStorageRepository) GetSecretStorageRefs(projectID int, storageID int) (db.ObjectReferrers, error) { + return db.ObjectReferrers{}, nil +} +func (m *mockSecretStorageRepository) DeleteSecretStorage(projectID int, storageID int) error { + return nil +} + +type mockAccessKeyEncryptionService struct { + DeleteSecretFn func(key *db.AccessKey) error + + DeletedSecretIDs []int +} + +func (m *mockAccessKeyEncryptionService) DeleteSecret(key *db.AccessKey) error { + m.DeletedSecretIDs = append(m.DeletedSecretIDs, key.ID) + if m.DeleteSecretFn != nil { + return m.DeleteSecretFn(key) + } + return nil +} + +// Stub methods to satisfy AccessKeyEncryptionService +func (m *mockAccessKeyEncryptionService) SerializeSecret(key *db.AccessKey) error { return nil } +func (m *mockAccessKeyEncryptionService) DeserializeSecret(key *db.AccessKey) error { return nil } +func (m *mockAccessKeyEncryptionService) FillEnvironmentSecrets(env *db.Environment, deserializeSecret bool) error { + return nil +} +func (m *mockAccessKeyEncryptionService) RekeyAccessKeys(oldKey string) error { return nil } + +func TestEnvironmentServiceImpl_Delete_InvalidIDs(t *testing.T) { + tests := []struct { + name string + projectID int + environmentID int + }{ + {"zero project ID", 0, 1}, + {"negative project ID", -1, 1}, + {"zero environment ID", 1, 0}, + {"negative environment ID", 1, -1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + envRepo := &mockEnvironmentManager{} + service := &EnvironmentServiceImpl{ + environmentRepo: envRepo, + encryptionService: &mockAccessKeyEncryptionService{}, + secretStorageRepo: &mockSecretStorageRepository{}, + } + + err := service.Delete(tt.projectID, tt.environmentID) + + assert.ErrorContains(t, err, "invalid project or environment ID") + assert.Equal(t, 0, envRepo.DeleteEnvironmentCalls) + }) + } +} + +func TestEnvironmentServiceImpl_Delete_NoSecretStorage(t *testing.T) { + envRepo := &mockEnvironmentManager{ + GetEnvironmentFn: func(projectID int, environmentID int) (db.Environment, error) { + assert.Equal(t, 1, projectID) + assert.Equal(t, 2, environmentID) + return db.Environment{ID: environmentID, ProjectID: projectID}, nil + }, + GetEnvironmentSecretsFn: func(projectID int, environmentID int) ([]db.AccessKey, error) { + return []db.AccessKey{{ID: 10}}, nil + }, + } + encryption := &mockAccessKeyEncryptionService{} + service := &EnvironmentServiceImpl{ + environmentRepo: envRepo, + encryptionService: encryption, + secretStorageRepo: &mockSecretStorageRepository{}, + } + + err := service.Delete(1, 2) + + require.NoError(t, err) + assert.Equal(t, 1, envRepo.DeleteEnvironmentCalls) + assert.Empty(t, encryption.DeletedSecretIDs) +} + +func TestEnvironmentServiceImpl_Delete_DeletesUnsynchronizedSecrets(t *testing.T) { + storageID := 7 + envRepo := &mockEnvironmentManager{ + GetEnvironmentFn: func(projectID int, environmentID int) (db.Environment, error) { + return db.Environment{ID: environmentID, ProjectID: projectID, SecretStorageID: &storageID}, nil + }, + GetEnvironmentSecretsFn: func(projectID int, environmentID int) ([]db.AccessKey, error) { + return []db.AccessKey{ + {ID: 10}, + {ID: 11, Synchronized: true}, + {ID: 12}, + }, nil + }, + } + storageRepo := &mockSecretStorageRepository{ + GetSecretStorageFn: func(projectID int, id int) (db.SecretStorage, error) { + assert.Equal(t, storageID, id) + return db.SecretStorage{ID: id, ReadOnly: false}, nil + }, + } + encryption := &mockAccessKeyEncryptionService{} + service := &EnvironmentServiceImpl{ + environmentRepo: envRepo, + encryptionService: encryption, + secretStorageRepo: storageRepo, + } + + err := service.Delete(1, 2) + + require.NoError(t, err) + assert.Equal(t, 1, envRepo.DeleteEnvironmentCalls) + assert.Equal(t, []int{10, 12}, encryption.DeletedSecretIDs) +} + +func TestEnvironmentServiceImpl_Delete_ReadOnlyStorageKeepsSecrets(t *testing.T) { + storageID := 7 + envRepo := &mockEnvironmentManager{ + GetEnvironmentFn: func(projectID int, environmentID int) (db.Environment, error) { + return db.Environment{ID: environmentID, ProjectID: projectID, SecretStorageID: &storageID}, nil + }, + GetEnvironmentSecretsFn: func(projectID int, environmentID int) ([]db.AccessKey, error) { + return []db.AccessKey{{ID: 10}}, nil + }, + } + storageRepo := &mockSecretStorageRepository{ + GetSecretStorageFn: func(projectID int, id int) (db.SecretStorage, error) { + return db.SecretStorage{ID: id, ReadOnly: true}, nil + }, + } + encryption := &mockAccessKeyEncryptionService{} + service := &EnvironmentServiceImpl{ + environmentRepo: envRepo, + encryptionService: encryption, + secretStorageRepo: storageRepo, + } + + err := service.Delete(1, 2) + + require.NoError(t, err) + assert.Equal(t, 1, envRepo.DeleteEnvironmentCalls) + assert.Empty(t, encryption.DeletedSecretIDs) +} + +func TestEnvironmentServiceImpl_Delete_AggregatesSecretDeletionErrors(t *testing.T) { + storageID := 7 + envRepo := &mockEnvironmentManager{ + GetEnvironmentFn: func(projectID int, environmentID int) (db.Environment, error) { + return db.Environment{ID: environmentID, ProjectID: projectID, SecretStorageID: &storageID}, nil + }, + GetEnvironmentSecretsFn: func(projectID int, environmentID int) ([]db.AccessKey, error) { + return []db.AccessKey{{ID: 10}, {ID: 11}}, nil + }, + } + encryption := &mockAccessKeyEncryptionService{ + DeleteSecretFn: func(key *db.AccessKey) error { + if key.ID == 10 { + return errors.New("vault unreachable") + } + return nil + }, + } + service := &EnvironmentServiceImpl{ + environmentRepo: envRepo, + encryptionService: encryption, + secretStorageRepo: &mockSecretStorageRepository{}, + } + + err := service.Delete(1, 2) + + assert.ErrorContains(t, err, "failed to delete some secrets") + assert.ErrorContains(t, err, "vault unreachable") + // The environment itself is still deleted, and all secrets are attempted. + assert.Equal(t, 1, envRepo.DeleteEnvironmentCalls) + assert.Equal(t, []int{10, 11}, encryption.DeletedSecretIDs) +} + +func TestEnvironmentServiceImpl_Delete_RepositoryErrors(t *testing.T) { + storageID := 7 + + t.Run("GetEnvironment fails", func(t *testing.T) { + envRepo := &mockEnvironmentManager{ + GetEnvironmentFn: func(projectID int, environmentID int) (db.Environment, error) { + return db.Environment{}, db.ErrNotFound + }, + } + service := &EnvironmentServiceImpl{ + environmentRepo: envRepo, + encryptionService: &mockAccessKeyEncryptionService{}, + secretStorageRepo: &mockSecretStorageRepository{}, + } + + err := service.Delete(1, 2) + + assert.ErrorIs(t, err, db.ErrNotFound) + assert.Equal(t, 0, envRepo.DeleteEnvironmentCalls) + }) + + t.Run("GetEnvironmentSecrets fails", func(t *testing.T) { + envRepo := &mockEnvironmentManager{ + GetEnvironmentSecretsFn: func(projectID int, environmentID int) ([]db.AccessKey, error) { + return nil, errors.New("secrets query failed") + }, + } + service := &EnvironmentServiceImpl{ + environmentRepo: envRepo, + encryptionService: &mockAccessKeyEncryptionService{}, + secretStorageRepo: &mockSecretStorageRepository{}, + } + + err := service.Delete(1, 2) + + assert.ErrorContains(t, err, "secrets query failed") + assert.Equal(t, 0, envRepo.DeleteEnvironmentCalls) + }) + + t.Run("DeleteEnvironment fails", func(t *testing.T) { + envRepo := &mockEnvironmentManager{ + DeleteEnvironmentFn: func(projectID int, environmentID int) error { + return errors.New("delete failed") + }, + } + encryption := &mockAccessKeyEncryptionService{} + service := &EnvironmentServiceImpl{ + environmentRepo: envRepo, + encryptionService: encryption, + secretStorageRepo: &mockSecretStorageRepository{}, + } + + err := service.Delete(1, 2) + + assert.ErrorContains(t, err, "delete failed") + assert.Empty(t, encryption.DeletedSecretIDs) + }) + + t.Run("GetSecretStorage fails", func(t *testing.T) { + envRepo := &mockEnvironmentManager{ + GetEnvironmentFn: func(projectID int, environmentID int) (db.Environment, error) { + return db.Environment{ID: environmentID, ProjectID: projectID, SecretStorageID: &storageID}, nil + }, + GetEnvironmentSecretsFn: func(projectID int, environmentID int) ([]db.AccessKey, error) { + return []db.AccessKey{{ID: 10}}, nil + }, + } + storageRepo := &mockSecretStorageRepository{ + GetSecretStorageFn: func(projectID int, id int) (db.SecretStorage, error) { + return db.SecretStorage{}, errors.New("storage not found") + }, + } + encryption := &mockAccessKeyEncryptionService{} + service := &EnvironmentServiceImpl{ + environmentRepo: envRepo, + encryptionService: encryption, + secretStorageRepo: storageRepo, + } + + err := service.Delete(1, 2) + + assert.ErrorContains(t, err, "storage not found") + assert.Equal(t, 1, envRepo.DeleteEnvironmentCalls) + assert.Empty(t, encryption.DeletedSecretIDs) + }) +} ++++++ semaphore.obsinfo ++++++ --- /var/tmp/diff_new_pack.H4ct5J/_old 2026-07-28 18:21:52.538840498 +0200 +++ /var/tmp/diff_new_pack.H4ct5J/_new 2026-07-28 18:21:52.542840638 +0200 @@ -1,5 +1,5 @@ name: semaphore -version: 2.18.28 -mtime: 1784723083 -commit: 33874e063dd9d7dc441e11f7fbfc24c0c28b270f +version: 2.18.29 +mtime: 1785215339 +commit: 91719b944ccf17e23cbf5e1955a1caea32a58c5c ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/semaphore/vendor.tar.gz /work/SRC/openSUSE:Factory/.semaphore.new.2004/vendor.tar.gz differ: char 151, line 2 ++++++ web-2.18.28.tar.gz -> web-2.18.29.tar.gz ++++++ /work/SRC/openSUSE:Factory/semaphore/web-2.18.28.tar.gz /work/SRC/openSUSE:Factory/.semaphore.new.2004/web-2.18.29.tar.gz differ: char 13, line 1
