Skip to content

Commit 0fe6675

Browse files
committed
[#74416] add work package description support
Add explicit `--description` support to create and update work package commands. This extends create and the core update patch path, includes JSON dry-run coverage for descriptions, documents the new flag, and keeps validation strict around unsupported flag combinations. https://community.openproject.org/wp/74416
1 parent a7ea4fa commit 0fe6675

12 files changed

Lines changed: 465 additions & 18 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ op update workpackge 42 -
130130
--action -a -- Executes a custom action on a work package
131131
--assignee -- Assign a user to the work package
132132
--attach -- Attach a file to the work package
133+
--description -- Change the raw work package description
133134
--help -h -- help for workpackage
134135
--subject -- Change the subject of the work package
135136
--type -t -- Change the work package type
@@ -154,6 +155,11 @@ op create workpackge -p11 'Document new CLI tool' -o
154155
# Validating the creation of a child work package without persisting it.
155156
# The parent determines the project automatically.
156157
op create workpackage --parent 74316 --type Implementation 'Build reusable skill' --dry-run --json
158+
159+
# Creating a child work package with an explicit raw description.
160+
op create workpackage --parent 74316 --type Implementation \
161+
--description 'Use openproject-cli JSON workflows from a reusable agent skill.' \
162+
'Build reusable skill'
157163
```
158164

159165
#### Listing
@@ -182,6 +188,10 @@ op update workpackage 42 --attach ./Downloads/Report.pdf
182188
# Resolving and validating field updates as JSON before applying them
183189
# This first slice supports schema-resolved custom fields via --set.
184190
op update workpackage 74316 --set 'Votes=3' --dry-run --json
191+
192+
# Updating core fields, including the raw description, in one PATCH request.
193+
op update workpackage 74416 --subject 'Add explicit description support' \
194+
--description 'Allow create and update to write the raw ticket body.'
185195
```
186196

187197
#### Inspecting

cmd/create/create.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ func init() {
4040
"Create the work package as a child of an existing work package",
4141
)
4242

43+
createWorkPackageCmd.Flags().StringVar(
44+
&descriptionFlag,
45+
"description",
46+
"",
47+
"Set the raw work package description",
48+
)
49+
4350
createWorkPackageCmd.Flags().BoolVar(
4451
&printCreatedWorkPackageAsJSON,
4552
"json",

cmd/create/work_package.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var shouldOpenWorkPackageInBrowser bool
1818
var printCreatedWorkPackageAsJSON bool
1919
var dryRunCreateWorkPackage bool
2020
var typeFlag string
21+
var descriptionFlag string
2122

2223
var createWorkPackageCmd = &cobra.Command{
2324
Use: "workpackage [subject]",
@@ -103,6 +104,10 @@ func createOptions(subject string) map[work_packages.CreateOption]string {
103104
options[work_packages.CreateParent] = fmt.Sprintf("%d", parentWorkPackageID)
104105
}
105106

107+
if len(descriptionFlag) > 0 {
108+
options[work_packages.CreateDescription] = descriptionFlag
109+
}
110+
106111
return options
107112
}
108113

cmd/create/work_package_test.go

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,85 @@ func TestCreateWorkPackagePrintsDryRunJSONWithParent(t *testing.T) {
6060

6161
createWorkPackage(nil, []string{"Build reusable skill"})
6262

63-
expected := "{\"valid\":true,\"operation\":\"create\",\"project_id\":1482,\"parent_id\":74316,\"work_package\":{\"subject\":\"Build reusable skill\",\"type\":\"\"}}\n"
63+
expected := "{\"valid\":true,\"operation\":\"create\",\"project_id\":1482,\"parent_id\":74316,\"work_package\":{\"subject\":\"Build reusable skill\",\"type\":\"\",\"description\":\"\"}}\n"
64+
if activePrinter.Result != expected {
65+
t.Fatalf("expected %s, got %s", expected, activePrinter.Result)
66+
}
67+
}
68+
69+
func TestCreateWorkPackagePrintsDryRunJSONWithDescription(t *testing.T) {
70+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
71+
switch r.URL.Path {
72+
case "/api/v3/work_packages/74316":
73+
_, _ = io.WriteString(w, `{
74+
"id": 74316,
75+
"subject": "Expand op CLI to support scripted work package workflows",
76+
"_embedded": {
77+
"project": {
78+
"id": 1482,
79+
"identifier": "cli",
80+
"name": "CLI"
81+
}
82+
},
83+
"_links": {
84+
"self": {"href": "/api/v3/work_packages/74316"},
85+
"project": {"href": "/api/v3/projects/1482", "title": "CLI"},
86+
"status": {"href": "/api/v3/statuses/1", "title": "new"},
87+
"type": {"href": "/api/v3/types/6", "title": "Feature"},
88+
"assignee": {"href": null, "title": ""}
89+
}
90+
}`)
91+
case "/api/v3/projects/1482":
92+
_, _ = io.WriteString(w, `{
93+
"id": 1482,
94+
"identifier": "cli",
95+
"name": "CLI",
96+
"_links": {
97+
"types": {"href": "/api/v3/projects/1482/types/available"}
98+
}
99+
}`)
100+
case "/api/v3/projects/1482/types/available":
101+
_, _ = io.WriteString(w, `{
102+
"_embedded": {
103+
"elements": [
104+
{
105+
"id": 7,
106+
"name": "Implementation",
107+
"_links": {
108+
"self": {"href": "/api/v3/types/7"}
109+
}
110+
}
111+
]
112+
}
113+
}`)
114+
default:
115+
t.Fatalf("unexpected path %s", r.URL.Path)
116+
}
117+
}))
118+
defer server.Close()
119+
120+
host, err := url.Parse(server.URL)
121+
if err != nil {
122+
t.Fatal(err)
123+
}
124+
125+
requests.Init(host, "token", false)
126+
routes.Init(host)
127+
128+
activePrinter := &printer.TestingPrinter{}
129+
printer.Init(activePrinter)
130+
131+
projectId = 0
132+
parentWorkPackageID = 74316
133+
shouldOpenWorkPackageInBrowser = false
134+
printCreatedWorkPackageAsJSON = true
135+
dryRunCreateWorkPackage = true
136+
typeFlag = "Implementation"
137+
descriptionFlag = "Body"
138+
139+
createWorkPackage(nil, []string{"Add explicit work package description support"})
140+
141+
expected := "{\"valid\":true,\"operation\":\"create\",\"project_id\":1482,\"parent_id\":74316,\"work_package\":{\"subject\":\"Add explicit work package description support\",\"type\":\"Implementation\",\"description\":\"Body\"}}\n"
64142
if activePrinter.Result != expected {
65143
t.Fatalf("expected %s, got %s", expected, activePrinter.Result)
66144
}

cmd/update/update.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ func addWorkPackageFlags() {
4343
"",
4444
"Change the subject of the work package",
4545
)
46+
workPackageCmd.Flags().StringVar(
47+
&descriptionFlag,
48+
"description",
49+
"",
50+
"Change the raw work package description",
51+
)
4652
workPackageCmd.Flags().StringVarP(
4753
&typeFlag,
4854
"type",

cmd/update/work_package.go

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var (
1717
actionFlag string
1818
assigneeFlag uint64
1919
attachFlag string
20+
descriptionFlag string
2021
dryRunUpdateWorkPackage bool
2122
printUpdatedWorkPackageAsJSON bool
2223
setFlags []string
@@ -100,17 +101,55 @@ func updateWorkPackage(_ *cobra.Command, args []string) {
100101
return
101102
}
102103

103-
if printUpdatedWorkPackageAsJSON || dryRunUpdateWorkPackage {
104+
options := updateOptions()
105+
if len(options) == 0 {
104106
printUpdateError("invalid_argument", "--json and --dry-run currently require at least one --set value")
105107
return
106108
}
107109

108-
if workPackage, err := work_packages.Update(id, updateOptions()); err == nil {
109-
printer.Info("-- ")
110-
printer.WorkPackage(workPackage)
111-
} else {
110+
if dryRunUpdateWorkPackage {
111+
plan := work_packages.DryRunUpdate(id, options)
112+
113+
data, err := presenter.MarshalJSON(plan)
114+
if err != nil {
115+
printer.Error(err)
116+
return
117+
}
118+
119+
printer.Info(string(data))
120+
return
121+
}
122+
123+
workPackage, err := work_packages.Update(id, options)
124+
if err != nil {
125+
if printUpdatedWorkPackageAsJSON {
126+
printUpdateError("api_error", err.Error())
127+
return
128+
}
129+
112130
printer.Error(err)
131+
return
113132
}
133+
134+
if printUpdatedWorkPackageAsJSON {
135+
payload, err := work_packages.Inspect(id)
136+
if err != nil {
137+
printUpdateError("api_error", err.Error())
138+
return
139+
}
140+
141+
data, err := presenter.MarshalJSON(payload)
142+
if err != nil {
143+
printer.Error(err)
144+
return
145+
}
146+
147+
printer.Info(string(data))
148+
return
149+
}
150+
151+
printer.Info("-- ")
152+
printer.WorkPackage(workPackage)
114153
}
115154

116155
func updateOptions() map[work_packages.UpdateOption]string {
@@ -127,6 +166,9 @@ func updateOptions() map[work_packages.UpdateOption]string {
127166
if len(subjectFlag) > 0 {
128167
options[work_packages.UpdateSubject] = subjectFlag
129168
}
169+
if len(descriptionFlag) > 0 {
170+
options[work_packages.UpdateDescription] = descriptionFlag
171+
}
130172
if len(typeFlag) > 0 {
131173
options[work_packages.UpdateType] = typeFlag
132174
}
@@ -139,6 +181,13 @@ func validateUpdateWorkPackageFlags() error {
139181
return fmt.Errorf("cannot use --dry-run without --json")
140182
}
141183

184+
if len(descriptionFlag) > 0 {
185+
conflicts := activeDescriptionConflicts()
186+
if len(conflicts) > 0 {
187+
return fmt.Errorf("cannot combine --description with %s", strings.Join(conflicts, ", "))
188+
}
189+
}
190+
142191
if len(setFlags) > 0 && hasLegacyUpdateFlags() {
143192
return fmt.Errorf("cannot combine --set with %s", strings.Join(activeLegacyUpdateFlags(), ", "))
144193
}
@@ -165,13 +214,29 @@ func activeLegacyUpdateFlags() []string {
165214
if len(subjectFlag) > 0 {
166215
flags = append(flags, "--subject")
167216
}
217+
if len(descriptionFlag) > 0 {
218+
flags = append(flags, "--description")
219+
}
168220
if len(typeFlag) > 0 {
169221
flags = append(flags, "--type")
170222
}
171223

172224
return flags
173225
}
174226

227+
func activeDescriptionConflicts() []string {
228+
flags := []string{}
229+
230+
if len(actionFlag) > 0 {
231+
flags = append(flags, "--action")
232+
}
233+
if len(attachFlag) > 0 {
234+
flags = append(flags, "--attach")
235+
}
236+
237+
return flags
238+
}
239+
175240
func printUpdateError(code, message string) {
176241
if !printUpdatedWorkPackageAsJSON {
177242
printer.ErrorText(message)

0 commit comments

Comments
 (0)