diff --git a/api/models/project_v1_alpha.go b/api/models/project_v1_alpha.go index 85f678a..134e85d 100644 --- a/api/models/project_v1_alpha.go +++ b/api/models/project_v1_alpha.go @@ -81,16 +81,18 @@ func (s *Scheduler) MarshalJSON() ([]byte, error) { } type Task struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - Scheduled bool `json:"scheduled"` - Id string `json:"id,omitempty"` - Branch string `json:"branch,omitempty" yaml:"branch,omitempty"` // Deprecated: Use Reference field instead. - Reference *Reference `json:"reference,omitempty" yaml:"reference,omitempty"` - At string `json:"at,omitempty"` - PipelineFile string `json:"pipeline_file" yaml:"pipeline_file,omitempty"` - Status string `json:"status,omitempty" yaml:"status,omitempty"` - Parameters []TaskParameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Scheduled bool `json:"scheduled"` + Id string `json:"id,omitempty"` + Branch string `json:"branch,omitempty" yaml:"branch,omitempty"` // Deprecated: Use Reference field instead. + Reference *Reference `json:"reference,omitempty" yaml:"reference,omitempty"` + At string `json:"at,omitempty"` + PipelineFile string `json:"pipeline_file" yaml:"pipeline_file,omitempty"` + Status string `json:"status,omitempty" yaml:"status,omitempty"` + SkipScheduledRunNotifications bool `json:"skip_scheduled_run_notifications,omitempty" yaml:"skip_scheduled_run_notifications,omitempty"` + SkipManualRunNotifications bool `json:"skip_manual_run_notifications,omitempty" yaml:"skip_manual_run_notifications,omitempty"` + Parameters []TaskParameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` } // UnmarshalJSON implements custom JSON unmarshaling for backward compatibility diff --git a/api/models/tasks_v1_alpha.go b/api/models/tasks_v1_alpha.go index 4313677..ffd5588 100644 --- a/api/models/tasks_v1_alpha.go +++ b/api/models/tasks_v1_alpha.go @@ -20,19 +20,21 @@ type TaskParameterV1Alpha struct { } type TaskV1Alpha struct { - ID string `json:"id" yaml:"id"` - Name string `json:"name" yaml:"name"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - ProjectID string `json:"project_id" yaml:"project_id"` - Branch string `json:"branch,omitempty" yaml:"branch,omitempty"` - At string `json:"at,omitempty" yaml:"at,omitempty"` - PipelineFile string `json:"pipeline_file" yaml:"pipeline_file"` - RequesterID string `json:"requester_id,omitempty" yaml:"requester_id,omitempty"` - UpdatedAt string `json:"updated_at,omitempty" yaml:"updated_at,omitempty"` - Paused bool `json:"paused" yaml:"paused"` - Suspended bool `json:"suspended" yaml:"suspended"` - Recurring bool `json:"recurring" yaml:"recurring"` - Parameters []TaskParameterV1Alpha `json:"parameters,omitempty" yaml:"parameters,omitempty"` + ID string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + ProjectID string `json:"project_id" yaml:"project_id"` + Branch string `json:"branch,omitempty" yaml:"branch,omitempty"` + At string `json:"at,omitempty" yaml:"at,omitempty"` + PipelineFile string `json:"pipeline_file" yaml:"pipeline_file"` + RequesterID string `json:"requester_id,omitempty" yaml:"requester_id,omitempty"` + UpdatedAt string `json:"updated_at,omitempty" yaml:"updated_at,omitempty"` + Paused bool `json:"paused" yaml:"paused"` + Suspended bool `json:"suspended" yaml:"suspended"` + Recurring bool `json:"recurring" yaml:"recurring"` + SkipScheduledRunNotifications bool `json:"skip_scheduled_run_notifications" yaml:"skip_scheduled_run_notifications"` + SkipManualRunNotifications bool `json:"skip_manual_run_notifications" yaml:"skip_manual_run_notifications"` + Parameters []TaskParameterV1Alpha `json:"parameters,omitempty" yaml:"parameters,omitempty"` } type TriggerV1Alpha struct { diff --git a/cmd/apply_test.go b/cmd/apply_test.go index 7dcad7a..82ff8ad 100644 --- a/cmd/apply_test.go +++ b/cmd/apply_test.go @@ -246,6 +246,70 @@ spec: assert.Equal(t, "", envParam.RegexPattern, "absent regex pattern should default to empty") } +func Test__ApplyProject__FromYaml_TaskNotificationSkipFlags_Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + yaml_file := ` +apiVersion: v1alpha +kind: Project +metadata: + name: Test + id: a13949b7-b2f6-4286-8f26-3962d7e97828 +spec: + visibility: public + repository: + url: "git@github.com:/semaphoreci/cli.git" + integration_type: github_token + pipeline_file: ".semaphore/semaphore.yml" + run_on: + - branches + tasks: + - name: nightly + scheduled: true + branch: main + at: "0 3 * * *" + pipeline_file: ".semaphore/cron.yml" + skip_scheduled_run_notifications: true + skip_manual_run_notifications: false + - name: release + scheduled: false + branch: main + pipeline_file: ".semaphore/release.yml" + skip_manual_run_notifications: true +` + + yaml_file_path := "/tmp/project_task_skip_flags.yaml" + ioutil.WriteFile(yaml_file_path, []byte(yaml_file), 0644) + + var received *models.ProjectV1Alpha + + httpmock.RegisterResponder("PATCH", "https://org.semaphoretext.xyz/api/v1alpha/projects/a13949b7-b2f6-4286-8f26-3962d7e97828", + func(req *http.Request) (*http.Response, error) { + body, _ := ioutil.ReadAll(req.Body) + received, _ = models.NewProjectV1AlphaFromJson(body) + + return httpmock.NewStringResponse(200, string(body)), nil + }, + ) + + RootCmd.SetArgs([]string{"apply", "-f", yaml_file_path}) + RootCmd.Execute() + + assert.NotNil(t, received) + assert.Len(t, received.Spec.Tasks, 2) + + nightly := received.Spec.Tasks[0] + assert.Equal(t, "nightly", nightly.Name) + assert.Equal(t, true, nightly.SkipScheduledRunNotifications) + assert.Equal(t, false, nightly.SkipManualRunNotifications, "explicit false should stay false") + + release := received.Spec.Tasks[1] + assert.Equal(t, "release", release.Name) + assert.Equal(t, false, release.SkipScheduledRunNotifications, "absent scheduled flag should default to false") + assert.Equal(t, true, release.SkipManualRunNotifications) +} + func Test__ApplyDeploymentTarget__FromYaml_Response200(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() diff --git a/cmd/create_test.go b/cmd/create_test.go index c9ba51e..e959296 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -117,6 +117,67 @@ spec: assert.Equal(t, `^[0-9]+\.[0-9]+\.[0-9]+$`, param.RegexPattern) } +func Test__CreateProject__FromYaml_TaskNotificationSkipFlags_Response200(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + yaml_file := ` +apiVersion: v1alpha +kind: Project +metadata: + name: Test +spec: + visibility: public + repository: + url: "git@github.com:/semaphoreci/cli.git" + integration_type: github_token + pipeline_file: ".semaphore/semaphore.yml" + run_on: + - branches + tasks: + - name: nightly + scheduled: true + branch: main + at: "0 3 * * *" + pipeline_file: ".semaphore/cron.yml" + skip_scheduled_run_notifications: true + - name: release + scheduled: false + branch: main + pipeline_file: ".semaphore/release.yml" +` + + yaml_file_path := "/tmp/project_task_skip_flags_create.yaml" + ioutil.WriteFile(yaml_file_path, []byte(yaml_file), 0644) + + var received *models.ProjectV1Alpha + + httpmock.RegisterResponder("POST", "https://org.semaphoretext.xyz/api/v1alpha/projects", + func(req *http.Request) (*http.Response, error) { + body, _ := ioutil.ReadAll(req.Body) + received, _ = models.NewProjectV1AlphaFromJson(body) + + return httpmock.NewStringResponse(200, string(body)), nil + }, + ) + + RootCmd.SetArgs([]string{"create", "-f", yaml_file_path}) + RootCmd.Execute() + + assert.NotNil(t, received) + assert.Len(t, received.Spec.Tasks, 2) + + nightly := received.Spec.Tasks[0] + assert.Equal(t, "nightly", nightly.Name) + assert.Equal(t, true, nightly.SkipScheduledRunNotifications) + assert.Equal(t, false, nightly.SkipManualRunNotifications, "absent manual flag should default to false") + + release := received.Spec.Tasks[1] + assert.Equal(t, "release", release.Name) + assert.Equal(t, false, release.SkipScheduledRunNotifications, "absent scheduled flag should default to false") + assert.Equal(t, false, release.SkipManualRunNotifications, "absent manual flag should default to false") +} + func Test__CreateNotification__FromYaml__Response200(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() diff --git a/cmd/edit_test.go b/cmd/edit_test.go index 888a71f..8d9fb78 100644 --- a/cmd/edit_test.go +++ b/cmd/edit_test.go @@ -310,6 +310,8 @@ func Test__EditProject__WithTasks__Response200(t *testing.T) { "scheduled":false, "branch":"master", "pipeline_file":".semaphore/cron.yml", + "skip_scheduled_run_notifications":true, + "skip_manual_run_notifications":false, "parameters":[ { "name":"param1", @@ -365,6 +367,8 @@ func Test__EditProject__WithTasks__Response200(t *testing.T) { assert.Equal(t, "", task.Branch) // Branch is cleared when auto-creating Reference assert.Equal(t, task.Scheduled, false) assert.Equal(t, task.PipelineFile, ".semaphore/cron.yml") + assert.Equal(t, true, task.SkipScheduledRunNotifications, "flag set on the server must survive the edit round-trip") + assert.Equal(t, false, task.SkipManualRunNotifications) task_parameter := task.Parameters[0] diff --git a/cmd/get_tasks_test.go b/cmd/get_tasks_test.go index 222120c..954f573 100644 --- a/cmd/get_tasks_test.go +++ b/cmd/get_tasks_test.go @@ -357,3 +357,80 @@ func Test__ListTasks__WithParameterRegexValidation(t *testing.T) { assert.True(t, param.ValidateInputFormat) assert.Equal(t, "^(prod|staging|dev)$", param.RegexPattern) } + +func Test__DescribeTask__WithNotificationSkipFlags(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + taskID := "bb2ba294-d4b3-48bc-90a7-12dd56e9424c" + + httpmock.RegisterResponder("GET", "https://org.semaphoretext.xyz/api/v1alpha/tasks/"+taskID, + func(req *http.Request) (*http.Response, error) { + body := `{ + "schedule": { + "id": "bb2ba294-d4b3-48bc-90a7-12dd56e9424c", + "name": "nightly", + "project_id": "aa1ba294-d4b3-48bc-90a7-12dd56e9424a", + "branch": "main", + "pipeline_file": ".semaphore/cron.yml", + "recurring": true, + "skip_scheduled_run_notifications": true, + "skip_manual_run_notifications": false + } + }` + return httpmock.NewStringResponse(200, body), nil + }, + ) + + c := client.NewTasksV1AlphaApi() + task, err := c.DescribeTask(taskID) + + assert.NoError(t, err) + assert.True(t, task.Schedule.SkipScheduledRunNotifications, "expected skip_scheduled_run_notifications=true to be parsed") + assert.False(t, task.Schedule.SkipManualRunNotifications) +} + +func Test__ListTasks__WithNotificationSkipFlags(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + projectID := "758cb945-7495-4e40-a9a1-4b3991c6a8fe" + + httpmock.RegisterRegexpResponder("GET", + regexp.MustCompile(`https://org\.semaphoretext\.xyz/api/v1alpha/tasks\?.*project_id=`+projectID), + func(req *http.Request) (*http.Response, error) { + body := `[ + { + "id": "bb2ba294-d4b3-48bc-90a7-12dd56e9424c", + "name": "nightly", + "project_id": "758cb945-7495-4e40-a9a1-4b3991c6a8fe", + "branch": "main", + "pipeline_file": ".semaphore/cron.yml", + "recurring": true, + "skip_manual_run_notifications": true + }, + { + "id": "cc3ba294-d4b3-48bc-90a7-12dd56e9424d", + "name": "deploy", + "project_id": "758cb945-7495-4e40-a9a1-4b3991c6a8fe", + "branch": "main", + "pipeline_file": ".semaphore/deploy.yml", + "recurring": false + } + ]` + return httpmock.NewStringResponse(200, body), nil + }, + ) + + c := client.NewTasksV1AlphaApi() + tasks, err := c.ListTasks(projectID) + + assert.NoError(t, err) + assert.Len(t, tasks, 2) + + assert.False(t, tasks[0].SkipScheduledRunNotifications, "absent scheduled flag should default to false") + assert.True(t, tasks[0].SkipManualRunNotifications) + + assert.False(t, tasks[1].SkipScheduledRunNotifications) + assert.False(t, tasks[1].SkipManualRunNotifications) +}