feat: add project metadata to project list and view - #693
Draft
NickJosevski wants to merge 1 commit into
Draft
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #491
What changed
octopus project listandoctopus project viewreturned far less than the REST API does. Both now carry the metadata the issue asked for.project listSlug,SpaceId,ProjectGroupId,ProjectGroupName,LifecycleId,LifecycleName,IsDisabled,IsVersionControlled,TenantedDeploymentMode.SLUG,PROJECT GROUP,LIFECYCLE.xargs).project viewSpaceId,IsDisabled,ProjectGroupId/Name,LifecycleId/Name,TenantedDeploymentMode,DeploymentProcessId,VariableSetId,IncludedLibraryVariableSetIds,ClonedFromProjectId,AutoCreateRelease,DefaultGuidedFailureMode,DefaultToSkipIfAlreadyInstalled,DiscreteChannelRelease,ReleaseNotesTemplate,VersioningStrategy,ProjectConnectivityPolicy,Templates.PROJECT GROUPandLIFECYCLE; basic adds project group, lifecycle, tenanted deployment mode and enabled/disabled state.ID resolution. Group and lifecycle IDs resolve to names the way
channel listdoes since bf27551:listmakes two extraGetAllcalls for the whole listing (constant, not N+1),viewmakes twoGetByIDcalls. Both are best-effort — a permissions failure leaves the command working and the ID is displayed instead of the name, and the*NameJSON fields are omitted.Backwards compatibility. Every existing JSON field keeps its name, its type, and its presence — nothing was renamed, removed, or given
omitemptythat did not have it.Descriptionstill appears as""on a project with no description. Only additive changes. Table column sets did change (columns added, none removed), and basic output forviewgained lines;list --format basicis byte-for-byte unchanged.Is
project view -f jsonreally broken?No — not any more. The issue (Feb 2025) shows
project view -f jsonprinting 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 theJson/Table/Basicmappers. Verified with a newoutputFormat jsontest inpkg/cmd/project/view/view_test.go. No fix needed, only enrichment.Before / after
project list -f jsonBefore:
[ { "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 tableBefore:
After:
(
Lifecycles-99is the fallback: that lifecycle was not in the lookup.)project list -f basicUnchanged:
project view -f jsonBefore:
{ "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 tableBefore:
After:
project view -f basicBefore:
After:
Test evidence
New
pkg/cmd/project/list/list_test.go(4 cases) andpkg/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).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.
listis now 6 columns,view8 — wide, buttarget listis already 9, so this is within house style. I deliberately keptIsDisabled,IsVersionControlledandTenantedDeploymentModeout of thelisttable; they are boolean-ish and would push it toward a wall of text.Alternative: drop
DESCRIPTIONfrom thelisttable — 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
DESCRIPTIONif the table gets wider again.2. Should IDs resolve to names, and what does it cost?
listcosts two extra round trips regardless of project count (Lifecycles.GetAll+ProjectGroups.GetAll), which is whatchannel listandtarget listalready 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 leavelistshowing raw IDs.Recommendation: resolve in both. Raw
Lifecycles-1in 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/--detailflag?I went with always-richer. Arguments for a flag:
view -f jsonnow emitsTemplatesandProjectConnectivityPolicy, 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 jsonis already the "give me everything" format, and a flag that everyone has to remember to pass is a worse default.jqhandles the extra keys for free.Recommendation: no flag. If the payload size becomes a real complaint, the cleaner lever is a
--fieldsselector across all commands rather than a project-specific--full.4.
Templatesonproject view.Included because the issue explicitly lists it. It contains
DefaultValue, which can hold aSensitiveValue— the API returns those withHasValue/Hintand 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 enabledin 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