Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,12 @@ projects -- Lists projects
workpackages -- Lists work packages

# Discover flags: hitting completion key after
op update workpackge 42 -
op update workpackage 42 -
# returns
--action -a -- Executes a custom action on a work package
--assignee -- Assign a user to the work package
--attach -- Attach a file to the work package
--description -- Change the raw work package description
--help -h -- help for workpackage
--subject -- Change the subject of the work package
--type -t -- Change the work package type
Expand All @@ -146,10 +147,19 @@ of examples, that might be useful for a great number of people.
# Creating a work package in a project only by subject.
# Work package is created with many default values (as for type and status),
# very similar to how a work package is created inline in a work package table.
op create workpackge --project 11 'Document new CLI tool'
op create workpackage --project 11 'Document new CLI tool'

# Same command with shorthands and directly open it in a browser to continue working on it.
op create workpackge -p11 'Document new CLI tool' -o
op create workpackage -p11 'Document new CLI tool' -o

# Validating the creation of a child work package without persisting it.
# The parent determines the project automatically.
op create workpackage --parent 74316 --type Implementation 'Build reusable skill' --dry-run --json

# Creating a child work package with an explicit raw description.
op create workpackage --parent 74316 --type Implementation \
--description 'Use openproject-cli JSON workflows from a reusable agent skill.' \
'Build reusable skill'
```

#### Listing
Expand All @@ -174,6 +184,14 @@ op update workpackage 42 --subject 'The new subject' --status 'In Progress' --ty

# Uploading an attachment to a work package
op update workpackage 42 --attach ./Downloads/Report.pdf

# Resolving and validating field updates as JSON before applying them
# This first slice supports schema-resolved custom fields via --set.
op update workpackage 74316 --set 'Votes=3' --dry-run --json

# Updating core fields, including the raw description, in one PATCH request.
op update workpackage 74416 --subject 'Add explicit description support' \
--description 'Allow create and update to write the raw ticket body.'
```

#### Inspecting
Expand All @@ -182,8 +200,32 @@ op update workpackage 42 --attach ./Downloads/Report.pdf
# Inspecting a work package with more details,
# then in the work package list command
op inspect workpackage 42

# Inspecting a work package and its direct children as machine-readable JSON
op inspect workpackage 74316 --children --json
```

#### JSON error codes

The `--json` variants of `inspect`, `create`, and `update` return a stable error envelope:

```json
{"error": {"code": "<code>", "message": "<message>"}}
```

| Code | Trigger | Mutation persisted? |
|---|---|---|
| `invalid_argument` | Missing positional arg, invalid id, local validation failure, malformed `--set`, or no update options under `--json`/`--dry-run` | No |
| `conflicting_arguments` | Incompatible flag combinations, e.g. `--description` + `--set`, `--open` + `--json`, `--dry-run` without `--json` | No |
| `api_error` | Underlying OpenProject API call failed while resolving, creating, updating, or dry-running a work package | No |
| `post_apply_inspect_failed` | CREATE or PATCH succeeded but the follow-up inspect to build the JSON response failed — the mutation **has persisted**, only the response payload could not be assembled | **Yes** |
| `ambiguous_field` | A `--set` label matched more than one schema field | No |
| `duplicate_field` | A `--set` assignment names the same API field twice | No |
| `unknown_field` | A `--set` label or API field is not present in the resolved schema | No |
| `unsupported_field_type` | A `--set` field's schema type is not yet coercible | No |
| `invalid_field_value` | A `--set` value fails type coercion (e.g. non-integer for an `Integer` field) | No |
| `non_writable_field` | A `--set` field exists in the schema but is not writable | No |

## Creating a release

Releases are triggered by pushing a git tag. The tag name becomes the version string embedded in the binaries.
Expand Down
29 changes: 28 additions & 1 deletion cmd/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ func init() {
0,
"Project ID to create the work package in",
)
_ = createWorkPackageCmd.MarkFlagRequired("project")

createWorkPackageCmd.Flags().BoolVarP(
&shouldOpenWorkPackageInBrowser,
Expand All @@ -34,5 +33,33 @@ func init() {
"Change the work package type",
)

createWorkPackageCmd.Flags().Uint64Var(
&parentWorkPackageID,
"parent",
0,
"Create the work package as a child of an existing work package",
)

createWorkPackageCmd.Flags().StringVar(
&descriptionFlag,
"description",
"",
"Set the raw work package description",
)

createWorkPackageCmd.Flags().BoolVar(
&printCreatedWorkPackageAsJSON,
"json",
false,
"Print machine-readable JSON output",
)

createWorkPackageCmd.Flags().BoolVar(
&dryRunCreateWorkPackage,
"dry-run",
false,
"Resolve and validate without persisting the work package",
)

RootCmd.AddCommand(createWorkPackageCmd)
}
103 changes: 99 additions & 4 deletions cmd/create/work_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ import (

"github.com/spf13/cobra"

componentErrors "github.com/opf/openproject-cli/components/errors"
"github.com/opf/openproject-cli/components/launch"
"github.com/opf/openproject-cli/components/presenter"
"github.com/opf/openproject-cli/components/printer"
"github.com/opf/openproject-cli/components/resources/work_packages"
"github.com/opf/openproject-cli/components/routes"
)

var projectId uint64
var parentWorkPackageID uint64
var shouldOpenWorkPackageInBrowser bool
var printCreatedWorkPackageAsJSON bool
var dryRunCreateWorkPackage bool
var typeFlag string
var descriptionFlag string
var descriptionFlagChanged bool

var createWorkPackageCmd = &cobra.Command{
Use: "workpackage [subject]",
Expand All @@ -22,16 +29,61 @@ var createWorkPackageCmd = &cobra.Command{
Run: createWorkPackage,
}

func createWorkPackage(_ *cobra.Command, args []string) {
func createWorkPackage(cmd *cobra.Command, args []string) {
if cmd != nil {
descriptionFlagChanged = cmd.Flags().Changed("description")
}

if len(args) != 1 {
printer.ErrorText(fmt.Sprintf("Expected 1 argument [subject], but got %d", len(args)))
printCreateError("invalid_argument", fmt.Sprintf("Expected 1 argument [subject], but got %d", len(args)))
return
}

if err := validateCreateWorkPackageFlags(); err != nil {
printCreateError("conflicting_arguments", err.Error())
return
}

subject := args[0]
workPackage, err := work_packages.Create(projectId, createOptions(subject))
options := createOptions(subject)

if dryRunCreateWorkPackage {
plan, err := work_packages.DryRunCreate(projectId, options)
if err != nil {
printCreateError(createErrorCode(err), err.Error())
return
}

data, err := presenter.MarshalJSON(plan)
if err != nil {
printer.Error(err)
return
}

printer.Info(string(data))
return
Comment thread
myabc marked this conversation as resolved.
}

workPackage, err := work_packages.Create(projectId, options)
if err != nil {
printer.Error(err)
printCreateError(createErrorCode(err), err.Error())
return
}
Comment thread
myabc marked this conversation as resolved.

if printCreatedWorkPackageAsJSON {
payload, err := work_packages.Inspect(workPackage.Id)
if err != nil {
printCreateError("post_apply_inspect_failed", err.Error())
return
}

data, err := presenter.MarshalJSON(payload)
if err != nil {
printer.Error(err)
return
}

printer.Info(string(data))
return
}

Expand All @@ -54,5 +106,48 @@ func createOptions(subject string) map[work_packages.CreateOption]string {
options[work_packages.CreateType] = typeFlag
}

if parentWorkPackageID > 0 {
options[work_packages.CreateParent] = fmt.Sprintf("%d", parentWorkPackageID)
}

if descriptionFlagChanged {
options[work_packages.CreateDescription] = descriptionFlag
}

return options
}

func validateCreateWorkPackageFlags() error {
if shouldOpenWorkPackageInBrowser && printCreatedWorkPackageAsJSON {
return fmt.Errorf("cannot use --open together with --json")
}

if dryRunCreateWorkPackage && !printCreatedWorkPackageAsJSON {
return fmt.Errorf("cannot use --dry-run without --json")
}

return nil
}

func printCreateError(code, message string) {
if !printCreatedWorkPackageAsJSON {
printer.ErrorText(message)
return
}

data, err := presenter.MarshalError(code, message)
if err != nil {
printer.Error(err)
return
}

printer.Info(string(data))
}

func createErrorCode(err error) string {
if componentErrors.IsCustom(err) {
return "invalid_argument"
}

return "api_error"
}
Loading
Loading