Skip to content

Commit 5f0bd49

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 149a355 commit 5f0bd49

12 files changed

Lines changed: 826 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: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ var shouldOpenWorkPackageInBrowser bool
1919
var printCreatedWorkPackageAsJSON bool
2020
var dryRunCreateWorkPackage bool
2121
var typeFlag string
22+
var descriptionFlag string
23+
var descriptionFlagChanged bool
2224

2325
var createWorkPackageCmd = &cobra.Command{
2426
Use: "workpackage [subject]",
@@ -27,7 +29,11 @@ var createWorkPackageCmd = &cobra.Command{
2729
Run: createWorkPackage,
2830
}
2931

30-
func createWorkPackage(_ *cobra.Command, args []string) {
32+
func createWorkPackage(cmd *cobra.Command, args []string) {
33+
if cmd != nil {
34+
descriptionFlagChanged = cmd.Flags().Changed("description")
35+
}
36+
3137
if len(args) != 1 {
3238
printCreateError("invalid_argument", fmt.Sprintf("Expected 1 argument [subject], but got %d", len(args)))
3339
return
@@ -104,6 +110,10 @@ func createOptions(subject string) map[work_packages.CreateOption]string {
104110
options[work_packages.CreateParent] = fmt.Sprintf("%d", parentWorkPackageID)
105111
}
106112

113+
if descriptionFlagChanged {
114+
options[work_packages.CreateDescription] = descriptionFlag
115+
}
116+
107117
return options
108118
}
109119

cmd/create/work_package_test.go

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,91 @@ func TestCreateWorkPackagePrintsDryRunJSONWithParent(t *testing.T) {
5757
printCreatedWorkPackageAsJSON = true
5858
dryRunCreateWorkPackage = true
5959
typeFlag = ""
60+
descriptionFlag = ""
61+
descriptionFlagChanged = false
6062

6163
createWorkPackage(nil, []string{"Build reusable skill"})
6264

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

80163
createWorkPackage(nil, []string{"Build reusable skill"})
81164

@@ -92,6 +175,8 @@ func TestValidateCreateWorkPackageFlagsRejectsDryRunWithoutJSON(t *testing.T) {
92175
printCreatedWorkPackageAsJSON = false
93176
dryRunCreateWorkPackage = true
94177
typeFlag = ""
178+
descriptionFlag = ""
179+
descriptionFlagChanged = false
95180

96181
err := validateCreateWorkPackageFlags()
97182
if err == nil {
@@ -207,6 +292,8 @@ func TestCreateWorkPackagePrintsInvalidArgumentForValidationError(t *testing.T)
207292
printCreatedWorkPackageAsJSON = true
208293
dryRunCreateWorkPackage = false
209294
typeFlag = ""
295+
descriptionFlag = ""
296+
descriptionFlagChanged = false
210297

211298
createWorkPackage(nil, []string{"Build reusable skill"})
212299

@@ -245,6 +332,8 @@ func TestCreateWorkPackagePrintsAPIErrorForDryRunParentLookupFailure(t *testing.
245332
printCreatedWorkPackageAsJSON = true
246333
dryRunCreateWorkPackage = true
247334
typeFlag = ""
335+
descriptionFlag = ""
336+
descriptionFlagChanged = false
248337

249338
createWorkPackage(nil, []string{"Build reusable skill"})
250339

@@ -318,6 +407,8 @@ func TestCreateWorkPackagePrintsPostApplyInspectFailure(t *testing.T) {
318407
printCreatedWorkPackageAsJSON = true
319408
dryRunCreateWorkPackage = false
320409
typeFlag = "Implementation"
410+
descriptionFlag = ""
411+
descriptionFlagChanged = false
321412

322413
createWorkPackage(nil, []string{"Build reusable skill"})
323414

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: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ var (
1717
actionFlag string
1818
assigneeFlag uint64
1919
attachFlag string
20+
descriptionFlag string
21+
descriptionFlagChanged bool
2022
dryRunUpdateWorkPackage bool
2123
printUpdatedWorkPackageAsJSON bool
2224
setFlags []string
@@ -32,7 +34,11 @@ provided by a flag is executed on its own.`,
3234
Run: updateWorkPackage,
3335
}
3436

35-
func updateWorkPackage(_ *cobra.Command, args []string) {
37+
func updateWorkPackage(cmd *cobra.Command, args []string) {
38+
if cmd != nil {
39+
descriptionFlagChanged = cmd.Flags().Changed("description")
40+
}
41+
3642
if len(args) != 1 {
3743
printUpdateError("invalid_argument", fmt.Sprintf("Expected 1 argument [id], but got %d", len(args)))
3844
return
@@ -145,6 +151,7 @@ func updateWorkPackage(_ *cobra.Command, args []string) {
145151
printUpdateError("api_error", err.Error())
146152
return
147153
}
154+
148155
printer.Error(err)
149156
return
150157
}
@@ -184,6 +191,9 @@ func updateOptions() map[work_packages.UpdateOption]string {
184191
if len(subjectFlag) > 0 {
185192
options[work_packages.UpdateSubject] = subjectFlag
186193
}
194+
if descriptionFlagChanged {
195+
options[work_packages.UpdateDescription] = descriptionFlag
196+
}
187197
if len(typeFlag) > 0 {
188198
options[work_packages.UpdateType] = typeFlag
189199
}
@@ -196,6 +206,13 @@ func validateUpdateWorkPackageFlags() error {
196206
return fmt.Errorf("cannot use --dry-run without --json")
197207
}
198208

209+
if descriptionFlagChanged {
210+
conflicts := activeDescriptionConflicts()
211+
if len(conflicts) > 0 {
212+
return fmt.Errorf("cannot combine --description with %s", strings.Join(conflicts, ", "))
213+
}
214+
}
215+
199216
if len(setFlags) > 0 && hasLegacyUpdateFlags() {
200217
return fmt.Errorf("cannot combine --set with %s", strings.Join(activeLegacyUpdateFlags(), ", "))
201218
}
@@ -222,13 +239,29 @@ func activeLegacyUpdateFlags() []string {
222239
if len(subjectFlag) > 0 {
223240
flags = append(flags, "--subject")
224241
}
242+
if descriptionFlagChanged {
243+
flags = append(flags, "--description")
244+
}
225245
if len(typeFlag) > 0 {
226246
flags = append(flags, "--type")
227247
}
228248

229249
return flags
230250
}
231251

252+
func activeDescriptionConflicts() []string {
253+
flags := []string{}
254+
255+
if len(actionFlag) > 0 {
256+
flags = append(flags, "--action")
257+
}
258+
if len(attachFlag) > 0 {
259+
flags = append(flags, "--attach")
260+
}
261+
262+
return flags
263+
}
264+
232265
func printUpdateError(code, message string) {
233266
if !printUpdatedWorkPackageAsJSON {
234267
printer.ErrorText(message)

0 commit comments

Comments
 (0)