Skip to content

feat: add project metadata to project list and view - #693

Draft
NickJosevski wants to merge 1 commit into
mainfrom
nj/issue-491
Draft

feat: add project metadata to project list and view#693
NickJosevski wants to merge 1 commit into
mainfrom
nj/issue-491

Conversation

@NickJosevski

Copy link
Copy Markdown
Contributor

Fixes #491

What changed

octopus project list and octopus project view returned far less than the REST API does. Both now carry the metadata the issue asked for.

project list

  • JSON adds Slug, SpaceId, ProjectGroupId, ProjectGroupName, LifecycleId, LifecycleName, IsDisabled, IsVersionControlled, TenantedDeploymentMode.
  • Table adds SLUG, PROJECT GROUP, LIFECYCLE.
  • Basic is unchanged (still just names — it is the format scripts pipe into xargs).

project view

  • JSON adds SpaceId, IsDisabled, ProjectGroupId/Name, LifecycleId/Name, TenantedDeploymentMode, DeploymentProcessId, VariableSetId, IncludedLibraryVariableSetIds, ClonedFromProjectId, AutoCreateRelease, DefaultGuidedFailureMode, DefaultToSkipIfAlreadyInstalled, DiscreteChannelRelease, ReleaseNotesTemplate, VersioningStrategy, ProjectConnectivityPolicy, Templates.
  • Table adds PROJECT GROUP and LIFECYCLE; basic adds project group, lifecycle, tenanted deployment mode and enabled/disabled state.

ID resolution. Group and lifecycle IDs resolve to names the way channel list does since bf27551: list makes two extra GetAll calls for the whole listing (constant, not N+1), view makes two GetByID calls. Both are best-effort — a permissions failure leaves the command working and the ID is displayed instead of the name, and the *Name JSON fields are omitted.

Backwards compatibility. Every existing JSON field keeps its name, its type, and its presence — nothing was renamed, removed, or given omitempty that did not have it. Description still appears as "" on a project with no description. Only additive changes. Table column sets did change (columns added, none removed), and basic output for view gained lines; list --format basic is byte-for-byte unchanged.

Is project view -f json really broken?

No — not any more. The issue (Feb 2025) shows project view -f json printing the human blob, and that was real at the time. It was fixed in e6a5343 "feat: support project view -f for project view (#533)" in Aug 2025, which added the Json/Table/Basic mappers. Verified with a new outputFormat json test in pkg/cmd/project/view/view_test.go. No fix needed, only enrichment.

Before / after

project list -f json

Before:

[
  { "Id": "Projects-22", "Name": "Fire Project", "Description": "", "ProjectTags": ["team/red"] }
]

After:

[
  {
    "Id": "Projects-22",
    "Name": "Fire Project",
    "Description": "",
    "ProjectTags": ["team/red"],
    "Slug": "fire-project",
    "SpaceId": "Spaces-1",
    "ProjectGroupId": "ProjectGroups-1",
    "ProjectGroupName": "Default Project Group",
    "LifecycleId": "Lifecycles-1",
    "LifecycleName": "Default Lifecycle",
    "IsDisabled": false,
    "IsVersionControlled": false,
    "TenantedDeploymentMode": "Untenanted"
  }
]

project list -f table

Before:

NAME           DESCRIPTION  TAGS
Fire Project                team/red
Water Project  Wet things   team/blue

After:

NAME           SLUG           PROJECT GROUP          LIFECYCLE          DESCRIPTION  TAGS
Fire Project   fire-project   Default Project Group  Default Lifecycle               team/red
Water Project  water-project  Default Project Group  Lifecycles-99      Wet things   team/blue

(Lifecycles-99 is the fallback: that lifecycle was not in the lookup.)

project list -f basic

Unchanged:

Fire Project
Water Project

project view -f json

Before:

{
  "Id": "Projects-22",
  "Name": "Fire Project",
  "Slug": "fire-project",
  "Description": "Fire things",
  "IsVersionControlled": false,
  "VersionControlBranch": "Not version controlled",
  "ProjectTags": ["team/red"],
  "WebUrl": "http://server/app#/Spaces-1/projects/Projects-22"
}

After:

{
  "Id": "Projects-22",
  "Name": "Fire Project",
  "Slug": "fire-project",
  "Description": "Fire things",
  "IsVersionControlled": false,
  "VersionControlBranch": "Not version controlled",
  "ProjectTags": ["team/red"],
  "WebUrl": "http://server/app#/Spaces-1/projects/Projects-22",
  "SpaceId": "Spaces-1",
  "IsDisabled": false,
  "ProjectGroupId": "ProjectGroups-1",
  "ProjectGroupName": "Default Project Group",
  "LifecycleId": "Lifecycles-1",
  "LifecycleName": "Default Lifecycle",
  "TenantedDeploymentMode": "Untenanted",
  "DeploymentProcessId": "deploymentprocess-Projects-22",
  "VariableSetId": "variableset-Projects-22",
  "IncludedLibraryVariableSetIds": ["LibraryVariableSets-1"],
  "AutoCreateRelease": false,
  "DefaultToSkipIfAlreadyInstalled": false,
  "DiscreteChannelRelease": false,
  "VersioningStrategy": {
    "Template": "#{Octopus.Version.LastMajor}.#{Octopus.Version.LastMinor}.#{Octopus.Version.NextPatch}"
  },
  "ProjectConnectivityPolicy": {
    "AllowDeploymentsToNoTargets": false,
    "ExcludeUnhealthyTargets": false
  }
}

project view -f table

Before:

NAME          SLUG          DESCRIPTION  VERSION CONTROL         TAGS      WEB URL
Fire Project  fire-project  Fire things  Not version controlled  team/red  http://server/app#/Spaces-1/projects/Projects-22

After:

NAME          SLUG          PROJECT GROUP          LIFECYCLE          DESCRIPTION  VERSION CONTROL         TAGS      WEB URL
Fire Project  fire-project  Default Project Group  Default Lifecycle  Fire things  Not version controlled  team/red  http://server/app#/Spaces-1/projects/Projects-22

project view -f basic

Before:

Fire Project (fire-project)
Version control branch: Not version controlled
Tags: team/red
Fire things
View this project in Octopus Deploy: http://server/app#/Spaces-1/projects/Projects-22

After:

Fire Project (fire-project)
Project group: Default Project Group
Lifecycle: Default Lifecycle
Tenanted deployment mode: Untenanted
Version control branch: Not version controlled
Tags: team/red
Fire things
Project is enabled
View this project in Octopus Deploy: http://server/app#/Spaces-1/projects/Projects-22

Test evidence

New pkg/cmd/project/list/list_test.go (4 cases) and pkg/cmd/project/view/view_test.go (4 cases) — neither command had any unit tests before. Both cover table, basic and strict-JSON output, plus the lookup-failure fallback path (403 on lifecycles and project groups still renders, showing IDs).

$ go build ./...
BUILD OK

$ go test ./pkg/cmd/project/...
ok  github.com/OctopusDeploy/cli/pkg/cmd/project/convert
ok  github.com/OctopusDeploy/cli/pkg/cmd/project/create
ok  github.com/OctopusDeploy/cli/pkg/cmd/project/list
ok  github.com/OctopusDeploy/cli/pkg/cmd/project/shared
ok  github.com/OctopusDeploy/cli/pkg/cmd/project/variables/create
ok  github.com/OctopusDeploy/cli/pkg/cmd/project/variables/shared
ok  github.com/OctopusDeploy/cli/pkg/cmd/project/variables/update
ok  github.com/OctopusDeploy/cli/pkg/cmd/project/view

$ go test ./pkg/...
all packages ok, no failures

Open questions / options

Draft because these are judgement calls I would rather have decided than assumed.

1. Which fields belong in the table vs JSON only?
I put slug, project group and lifecycle in the table because they are short, high-signal, and identify a project the way a name alone doesn't. list is now 6 columns, view 8 — wide, but target list is already 9, so this is within house style. I deliberately kept IsDisabled, IsVersionControlled and TenantedDeploymentMode out of the list table; they are boolean-ish and would push it toward a wall of text.
Alternative: drop DESCRIPTION from the list table — it is the one column that can be arbitrarily long and the only one that ever needs truncating. That is a removal, so I did not do it unilaterally.
Recommendation: keep as is; revisit DESCRIPTION if the table gets wider again.

2. Should IDs resolve to names, and what does it cost?
list costs two extra round trips regardless of project count (Lifecycles.GetAll + ProjectGroups.GetAll), which is what channel list and target list already do. On a space with many lifecycles those responses are not tiny, but they are two requests, not N.
Alternative: resolve only in view (2 × GetByID) and leave list showing raw IDs.
Recommendation: resolve in both. Raw Lifecycles-1 in a listing is not useful to a human, and the JSON keeps both the ID and the name so scripts lose nothing. Note both lookups are best-effort — a user without lifecycle or project-group read permission still gets a working listing, just with IDs.

3. Always-richer output, or a --full / --detail flag?
I went with always-richer. Arguments for a flag: view -f json now emits Templates and ProjectConnectivityPolicy, which can be large on a real project, and the two extra API calls become opt-in.
Arguments against: the issue is asking for the data to be available, -f json is already the "give me everything" format, and a flag that everyone has to remember to pass is a worse default. jq handles the extra keys for free.
Recommendation: no flag. If the payload size becomes a real complaint, the cleaner lever is a --fields selector across all commands rather than a project-specific --full.

4. Templates on project view.
Included because the issue explicitly lists it. It contains DefaultValue, which can hold a SensitiveValue — the API returns those with HasValue/Hint and no plaintext, so nothing secret is disclosed, but worth a second opinion from someone who knows that contract better than I do.

5. Project is enabled in basic view.
Added unconditionally to match tenant view, which always prints enabled/disabled. If the preference is to keep the common case tighter, printing the line only when the project is disabled is a one-line change.

🤖 Generated with Claude Code

Both commands returned far less than the REST API does. list and view now
carry the project group, lifecycle, slug, space, disabled state and tenanted
deployment mode, and view additionally carries the process, variable set,
library variable sets, release settings, connectivity policy and templates.

Group and lifecycle IDs resolve to names the way channel list resolves
lifecycles: two GetAll lookups for the whole list rather than one per project,
best-effort, falling back to the ID when a name can't be resolved.

Existing JSON fields keep their names and their presence, so scripts parsing
the current output are unaffected.

Refs #491

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Add metadata to octopus project list / view

1 participant