Skip to content

Commit 9da2361

Browse files
authored
Merge branch 'semaphoreui:develop' into develop
2 parents 0b4542b + 20a99a2 commit 9da2361

12 files changed

Lines changed: 398 additions & 108 deletions

api/system_info.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ func (c *SystemInfoController) GetSystemInfo(w http.ResponseWriter, r *http.Requ
6060

6161
if err != nil {
6262
log.WithError(err).Error("Failed to get subscription plan")
63-
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
64-
return
63+
err = nil
64+
//http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
65+
//return
6566
}
6667

6768
switch {
@@ -70,7 +71,9 @@ func (c *SystemInfoController) GetSystemInfo(w http.ResponseWriter, r *http.Requ
7071
plan = ""
7172
case err != nil:
7273
log.WithError(err).Error("Failed to get subscription plan")
73-
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
74+
err = nil
75+
plan = ""
76+
//http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
7477
return
7578
default:
7679
plan = token.Plan

cli/cmd/project_export.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
projectService "github.com/semaphoreui/semaphore/services/project"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
type projectExportArgs struct {
13+
projectID int
14+
projectName string
15+
file string
16+
}
17+
18+
var targetProjectExportArgs projectExportArgs
19+
20+
func init() {
21+
projectExportCmd.PersistentFlags().IntVar(&targetProjectExportArgs.projectID, "project-id", 0, "Project ID to export")
22+
projectExportCmd.PersistentFlags().StringVar(&targetProjectExportArgs.projectName, "project-name", "", "Project name to export")
23+
projectExportCmd.PersistentFlags().StringVar(&targetProjectExportArgs.file, "file", "", "Output file path (default: stdout)")
24+
projectCmd.AddCommand(projectExportCmd)
25+
}
26+
27+
var projectExportCmd = &cobra.Command{
28+
Use: "export",
29+
Short: "Export project backup",
30+
Run: func(cmd *cobra.Command, args []string) {
31+
32+
ok := true
33+
if targetProjectExportArgs.projectID == 0 && targetProjectExportArgs.projectName == "" {
34+
fmt.Println("Argument --project-id or --project-name required")
35+
ok = false
36+
}
37+
38+
if targetProjectExportArgs.projectID != 0 && targetProjectExportArgs.projectName != "" {
39+
fmt.Println("Only one of --project-id or --project-name can be specified")
40+
ok = false
41+
}
42+
43+
if !ok {
44+
fmt.Println("Use command `semaphore project export --help` for details.")
45+
os.Exit(1)
46+
}
47+
48+
store := createStore("")
49+
defer store.Close("")
50+
51+
projectID := targetProjectExportArgs.projectID
52+
53+
if targetProjectExportArgs.projectName != "" {
54+
projects, err := store.GetAllProjects()
55+
if err != nil {
56+
fmt.Printf("Failed to get projects: %v\n", err)
57+
os.Exit(1)
58+
}
59+
60+
found := false
61+
searchName := strings.ToLower(targetProjectExportArgs.projectName)
62+
for _, p := range projects {
63+
if strings.ToLower(p.Name) == searchName {
64+
projectID = p.ID
65+
found = true
66+
break
67+
}
68+
}
69+
70+
if !found {
71+
fmt.Printf("Project with name '%s' not found\n", targetProjectExportArgs.projectName)
72+
os.Exit(1)
73+
}
74+
}
75+
76+
backup, err := projectService.GetBackup(projectID, store)
77+
if err != nil {
78+
fmt.Printf("Failed to create backup: %v\n", err)
79+
os.Exit(1)
80+
}
81+
82+
data, err := backup.Marshal()
83+
if err != nil {
84+
fmt.Printf("Failed to marshal backup: %v\n", err)
85+
os.Exit(1)
86+
}
87+
88+
if targetProjectExportArgs.file == "" {
89+
fmt.Println(data)
90+
} else {
91+
if err := os.WriteFile(targetProjectExportArgs.file, []byte(data), 0644); err != nil {
92+
fmt.Printf("Failed to write file: %v\n", err)
93+
os.Exit(1)
94+
}
95+
fmt.Printf("Project exported to %s\n", targetProjectExportArgs.file)
96+
}
97+
},
98+
}

cli/cmd/project_import.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,17 @@ import (
1616
)
1717

1818
type projectImportArgs struct {
19-
dir string
20-
file string
19+
dir string
20+
file string
21+
projectName string
2122
}
2223

2324
var targetProjectImportArgs projectImportArgs
2425

2526
func init() {
2627
projectImportCmd.PersistentFlags().StringVar(&targetProjectImportArgs.dir, "dir", "", "Directory path with project backups to import")
2728
projectImportCmd.PersistentFlags().StringVar(&targetProjectImportArgs.file, "file", "", "Backup file path to import")
29+
projectImportCmd.PersistentFlags().StringVar(&targetProjectImportArgs.projectName, "project-name", "", "Override project name (only valid with --file)")
2830
projectCmd.AddCommand(projectImportCmd)
2931
}
3032

@@ -44,6 +46,11 @@ var projectImportCmd = &cobra.Command{
4446
ok = false
4547
}
4648

49+
if targetProjectImportArgs.projectName != "" && targetProjectImportArgs.dir != "" {
50+
fmt.Println("Option --project-name can only be used with --file, not --dir")
51+
ok = false
52+
}
53+
4754
if !ok {
4855
fmt.Println("Use command `semaphore project import --help` for details.")
4956
os.Exit(1)
@@ -94,7 +101,7 @@ var projectImportCmd = &cobra.Command{
94101

95102
okCount := 0
96103
for _, f := range files {
97-
if err := importProjectFromFile(f, user, store); err != nil {
104+
if err := importProjectFromFile(f, targetProjectImportArgs.projectName, user, store); err != nil {
98105
log.Errorf("failed to import %s: %v", f, err)
99106
continue
100107
}
@@ -134,7 +141,7 @@ func resolveImportUser(store db.Store) (res db.User, err error) {
134141
return
135142
}
136143

137-
func importProjectFromFile(path string, user db.User, store db.Store) error {
144+
func importProjectFromFile(path string, projectName string, user db.User, store db.Store) error {
138145
data, err := os.ReadFile(path)
139146
if err != nil {
140147
return err
@@ -146,6 +153,9 @@ func importProjectFromFile(path string, user db.User, store db.Store) error {
146153
if err := backup.Verify(); err != nil {
147154
return err
148155
}
156+
if projectName != "" {
157+
backup.Meta.Name = projectName
158+
}
149159
_, err = backup.Restore(user, store)
150160
return err
151161
}

util/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,10 @@ type ConfigType struct {
331331
Debugging *DebuggingConfig `json:"debugging,omitempty"`
332332

333333
HA *HAConfig `json:"ha,omitempty"`
334+
335+
// SubscriptionKey is a subscription key or token that can be set via config.
336+
// When this is set, subscription activation from the web interface is disabled.
337+
SubscriptionKey string `json:"subscription_key,omitempty" db:"-" env:"SEMAPHORE_SUBSCRIPTION_KEY"`
334338
}
335339

336340
func NewConfigType() *ConfigType {
@@ -594,6 +598,13 @@ func assignMapToStructRecursive(m map[string]any, structValue reflect.Value) err
594598

595599
for i := 0; i < structType.NumField(); i++ {
596600
field := structType.Field(i)
601+
602+
// Skip fields with db:"-" tag
603+
dbTag := field.Tag.Get("db")
604+
if dbTag == "-" {
605+
continue
606+
}
607+
597608
jsonTag := field.Tag.Get("json")
598609
if jsonTag == "" {
599610
jsonTag = field.Name

util/config_assign_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,86 @@ func TestAssignMapToStruct_MapPrimitiveConversions(t *testing.T) {
162162
}
163163
}
164164

165+
func TestAssignMapToStruct_SkipsDbMinusTag(t *testing.T) {
166+
type Sample struct {
167+
Name string `json:"name"`
168+
Password string `json:"password" db:"-"`
169+
Age int `json:"age"`
170+
Secret string `json:"secret" db:"-"`
171+
}
172+
173+
t.Run("fields with db:- tag should not be assigned", func(t *testing.T) {
174+
s := Sample{
175+
Name: "original",
176+
Password: "original_password",
177+
Age: 25,
178+
Secret: "original_secret",
179+
}
180+
181+
m := map[string]any{
182+
"name": "updated",
183+
"password": "new_password",
184+
"age": 30,
185+
"secret": "new_secret",
186+
}
187+
188+
if err := AssignMapToStruct(m, &s); err != nil {
189+
t.Fatalf("unexpected error: %v", err)
190+
}
191+
192+
// Fields without db:"-" should be updated
193+
if s.Name != "updated" {
194+
t.Errorf("expected Name to be 'updated', got '%s'", s.Name)
195+
}
196+
if s.Age != 30 {
197+
t.Errorf("expected Age to be 30, got %d", s.Age)
198+
}
199+
200+
// Fields with db:"-" should retain original values
201+
if s.Password != "original_password" {
202+
t.Errorf("expected Password to remain 'original_password', got '%s'", s.Password)
203+
}
204+
if s.Secret != "original_secret" {
205+
t.Errorf("expected Secret to remain 'original_secret', got '%s'", s.Secret)
206+
}
207+
})
208+
209+
t.Run("nested struct with db:- tag fields", func(t *testing.T) {
210+
type Inner struct {
211+
Public string `json:"public"`
212+
Private string `json:"private" db:"-"`
213+
}
214+
type Outer struct {
215+
Inner Inner `json:"inner"`
216+
}
217+
218+
o := Outer{
219+
Inner: Inner{
220+
Public: "original_public",
221+
Private: "original_private",
222+
},
223+
}
224+
225+
m := map[string]any{
226+
"inner": map[string]any{
227+
"public": "updated_public",
228+
"private": "updated_private",
229+
},
230+
}
231+
232+
if err := AssignMapToStruct(m, &o); err != nil {
233+
t.Fatalf("unexpected error: %v", err)
234+
}
235+
236+
if o.Inner.Public != "updated_public" {
237+
t.Errorf("expected Inner.Public to be 'updated_public', got '%s'", o.Inner.Public)
238+
}
239+
if o.Inner.Private != "original_private" {
240+
t.Errorf("expected Inner.Private to remain 'original_private', got '%s'", o.Inner.Private)
241+
}
242+
})
243+
}
244+
165245
func TestSetConfigValue_SliceAndMap(t *testing.T) {
166246
// This ensures setConfigValue (used by defaults/env) is compatible with slice/map JSON
167247
type X struct {

web/src/components/ArgsPicker.vue

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,30 @@
6666
>
6767
<legend style="padding: 0 3px;">{{ title || $t('Args') }}</legend>
6868
<v-chip-group column style="margin-top: -4px;">
69-
<v-chip
70-
v-for="(v, i) in modifiedVars"
71-
close
72-
@click:close="deleteVar(i)"
73-
:key="i"
74-
@click="editVar(i)"
69+
<draggable
70+
v-model="modifiedVars"
71+
@end="onDragEnd"
72+
:animation="200"
73+
class="d-flex flex-wrap"
74+
ghost-class="chip-ghost"
7575
>
76-
<div
77-
style="
78-
max-width: 200px;
79-
overflow: hidden;
80-
text-overflow: ellipsis;
81-
"
82-
>{{ v.name }}</div>
83-
</v-chip>
76+
<v-chip
77+
v-for="(v, i) in modifiedVars"
78+
close
79+
@click:close="deleteVar(i)"
80+
:key="i"
81+
@click="editVar(i)"
82+
class="draggable-chip"
83+
>
84+
<div
85+
style="
86+
max-width: 200px;
87+
overflow: hidden;
88+
text-overflow: ellipsis;
89+
"
90+
>{{ v.name }}</div>
91+
</v-chip>
92+
</draggable>
8493
<v-chip @click="editVar(null)">
8594
+ <span
8695
class="ml-1"
@@ -92,10 +101,25 @@
92101
</div>
93102
</template>
94103
<style lang="scss">
104+
.draggable-chip {
105+
cursor: grab;
95106
107+
&:active {
108+
cursor: grabbing;
109+
}
110+
}
111+
112+
.chip-ghost {
113+
opacity: 0.5;
114+
}
96115
</style>
97116
<script>
117+
import draggable from 'vuedraggable';
118+
98119
export default {
120+
components: {
121+
draggable,
122+
},
99123
props: {
100124
vars: Array,
101125
title: String,
@@ -175,6 +199,10 @@ export default {
175199
this.modifiedVars.splice(index, 1);
176200
this.$emit('change', this.modifiedVars.map((x) => x.name));
177201
},
202+
203+
onDragEnd() {
204+
this.$emit('change', this.modifiedVars.map((x) => x.name));
205+
},
178206
},
179207
};
180208
</script>

0 commit comments

Comments
 (0)