Repository navigation
feat: pick an Octopus space from a dropdown and store its id - #239
NickJosevski wants to merge 8 commits into
Conversation
4793293 to
57880b0
Compare
The connection and all six step forms only offered a free-text "Space name", so renaming a space in Octopus silently broke every configuration naming it - a rename we just did needed five TeamCity params across four build types, found only by grepping the config repo. Add a space picker that lists the server's spaces and stores the chosen space's id, which survives a rename. Both CLIs already resolve --space by name or id (octo via Spaces.FindByNameOrIdOrFail, the Go CLI via SpaceNameOrID), so the id needs no agent or CLI change and existing name-based configurations keep working unchanged. The free-text field is kept deliberately. An API key held in a build parameter, and an OIDC token, only exist while a build runs, so spaces cannot be listed at edit time for those connections; the picker explains that rather than failing opaquely. For a saved connection the server reads the stored credentials itself, because TeamCity renders a saved secret as a placeholder and the browser never holds the real key. SpaceSelection replaces 13 near-duplicate blocks across CommandHelper and the legacy build processes. It keeps resolve() and resolveOrDefault() separate so the legacy path still omits --space when no space is set while the Go CLI path still substitutes "Default". The lookup endpoint requires EDIT_PROJECT on the project being edited, prefers a saved connection's credentials over anything posted, and never echoes the key back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three things CI caught that local unit tests could not: The props taglib renders these fields with name="prop:<property>", not "<property>" - a convention already noted in OctopusConnectionValidationUiTest. The picker's JS looked them up by the bare name, so the stored space id was never found and the inline-credentials path read nothing. Look the fields up by both spellings, and by id, so this no longer hinges on that prefix. CodeQL flagged the lookup's outbound request as server-side request forgery. The EDIT_PROJECT gate bounds who can reach it but not where it can point, so restrict the scheme to http/https and require a host, rejecting values such as file: and jar: that URL.openConnection() would otherwise read. Redirects are no longer followed either - a redirect would move the request off the address just validated and replay the API key there - and are reported as such. Plain http stays allowed, since on-prem Octopus is commonly http on an internal host. The e2e assertion used the same wrong field name and so failed on a real TeamCity; it now matches what the taglib renders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CodeQL flagged the lookup as server-side request forgery, and restricting the scheme did not address it: the host was still request-supplied, which is the actual vector. Rather than argue the alert, remove the taint path. The request now names a connection and nothing else - no URL, no key. The server reads both from the stored connection, so the only addresses it will ever fetch are ones an admin already persisted into project configuration. That is a real property rather than a mitigation, and it also matches how TeamCity works: a saved secret renders as a placeholder, so the browser could not have supplied the real key anyway. The cost is that a new connection must be saved once before its spaces can be listed, and a step using inline credentials cannot list them at all. Both say so and fall back to typing the space name, which has always worked. On a step form the connection selector identifies the connection. On the connection form it has to come from the request, and TeamCity's parameter name for it is not contractual, so each plausible name is tried and accepted only when it resolves to an Octopus connection in that project; an unknown name degrades to asking the admin to save first. The scheme/host validation and the no-redirect behaviour stay as defence in depth over the stored URL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Walks the space picker through the real stack in a browser and screenshots each state. It is a driver rather than a test - it asserts almost nothing - and earns its place by regenerating the screenshots after a change to the picker's markup, and by making the id-survives-a-rename behaviour something you can watch rather than infer. Two modes. OCTOPUS_PICKER_DEMO=auto runs headless straight through, so it needs no display and works as a screenshot generator. OCTOPUS_PICKER_DEMO=1 runs headed and holds the stack open at each checkpoint, resumed by creating the file it names. Skipped via assumeTrue unless that variable is set, matching LocalOctopusReleaseLinkManualTest; without the gate it would block forever on the first checkpoint. Everything it writes stays under build/reports/space-picker, overridable with SPACE_PICKER_OUTPUT_DIR, and it never prints the API key. The free tier permits only one space, so the walkthrough renames that space out from under an already-saved step rather than creating several. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3e46b32 to
5a2da26
Compare
Two conflicts. octopus-common's test dependencies wanted both sides: this branch's common-api and the JUnit and AssertJ ones the build-page work added. CommandHelper took main's structure - the command refactor moved several of these helpers to returning the argument array directly - and this branch's space handling was reapplied on top, so every site resolves through SpaceSelection rather than reading the space name and defaulting it locally. That includes spaceView, which arrived on main after this branch and had the old shape. CreateReleaseBuildProcess decides whether it can skip the space lookup by asking whether the step already names a space id. It read the space-name parameter, which is the only place in the agent still doing so, and would not have seen an id chosen from the dropdown; it now resolves through SpaceSelection like everywhere else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude here (on Matt's behalf): merged
The part worth checking: Verified: |
# Conflicts: # octopus-server/src/main/resources/buildServerResources/editOctopusConnection.jsp # octopus-server/src/main/resources/buildServerResources/forms/editOctopusBuildInformationForm.jsp # octopus-server/src/main/resources/buildServerResources/forms/editOctopusCreateReleaseForm.jsp # octopus-server/src/main/resources/buildServerResources/forms/editOctopusDeployReleaseForm.jsp # octopus-server/src/main/resources/buildServerResources/forms/editOctopusPromoteReleaseForm.jsp # octopus-server/src/main/resources/buildServerResources/forms/editOctopusPushPackageForm.jsp # octopus-server/src/main/resources/buildServerResources/forms/editOctopusRunRunbookForm.jsp # octopus-server/src/main/resources/buildServerResources/spacePicker.jsp # octopus-server/src/main/resources/buildServerResources/viewOctopusBuildInformation.jsp # octopus-server/src/main/resources/buildServerResources/viewOctopusCreateRelease.jsp # octopus-server/src/main/resources/buildServerResources/viewOctopusDeployRelease.jsp # octopus-server/src/main/resources/buildServerResources/viewOctopusPromoteRelease.jsp # octopus-server/src/main/resources/buildServerResources/viewOctopusPushPackage.jsp # octopus-server/src/main/resources/buildServerResources/viewOctopusRunRunbook.jsp
# Conflicts: # octopus-common/src/main/java/octopus/teamcity/common/SpaceSelection.java # octopus-common/src/test/java/octopus/teamcity/common/SpaceSelectionTest.java
…plete field (#260) The space field becomes TeamCity's autocomplete text field, so a space can still be chosen by id while parameter references keep working. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
Claude here: superseded by the space-picker work now on Not carried over: |



Why
Renaming a space in Octopus silently breaks every TeamCity configuration that names it. We just hit
this: renaming one space needed PRs across five repos, and the TeamCity side alone was five
octopus_space_nameparams across four build types — found only by grepping the config repo,because nothing surfaces a stale name until a build fails.
The plugin has only ever offered a free-text "Space name" field, on the connection and on all six
step forms, so every one of those is a hand-typed string that goes stale on rename.
What this does
Adds a space picker: Load spaces lists the spaces on the Octopus server and stores the chosen
space's id (
Spaces-1795), which survives a rename. The name is stored alongside it purely sothe UI stays readable.
The free-text field is deliberately kept rather than replaced - see the constraint below.
The storage half is free
Both CLIs the agent drives already resolve a space by name or id, so the stored id needs no CLI
change and no wire-format change:
octo:ApiCommand.csdeclares--spaceas "The name or ID of a space" and callsSpaces.FindByNameOrIdOrFail(spaceNameOrId).client_factory.go'sSpaceNameOrID, name first then id (cli#30).That also makes this backwards compatible by construction: existing configurations store names, and
names keep resolving exactly as before.
The constraint that shaped the design
A dropdown needs credentials at edit time, and two of the three API key sources cannot provide
them:
keyparameterOctopusConnectionBuildStartProcessor, at build startoidcEven for
key: it is stored assecure:octopus_apikey, so when editing a saved connectionTeamCity renders a placeholder and the browser does not hold the real key. The lookup therefore
sends a connection id and the server reads the stored credentials itself, falling back to posted
form values only for a connection that has not been saved yet.
So the picker cannot replace the text field. For
parameterandoidcit reports why, in words,and typing a name remains the supported path. Replacing the input with a plain
<select>would havebroken exactly the OIDC setups this repo already supports.
Shape
SpaceSelection(octopus-common) - one place that answers "which space does this step act on",preferring id over name. It replaces 13 near-duplicate blocks in
CommandHelperand the sixlegacy
*BuildProcessclasses. It deliberately exposes bothresolve(empty when unset, so thelegacy path keeps omitting
--space) andresolveOrDefault(substitutes"Default", as the GoCLI path always has) rather than flattening that difference.
OctopusSpacesFetcher/OctopusSpacesController(octopus-server) - the lookup. Hand-rolledHttpURLConnection+ gson followingCommitHistoryFetcher, rather than pulling incom.octopus:octopus-sdk, which is test-scope only today and would drag a gson upgrade into theshipped plugin (the repo pins gson 2.2.2; the e2e module already had to override it).
spacePicker.jsp- one shared include used by the connection form and all six step forms, so themarkup and JS exist once. Follows the existing conventions: HTML-escaped
data-attributes ratherthan server-interpolated JS string literals,
textContentfor admin-supplied names,$j, noframework.
Security
The endpoint makes an outbound request to a URL the caller influences, so it is deliberately narrow:
EDIT_PROJECTon the project whose form they are editing -the same permission needed to configure a connection at all, so it grants no reach they did not
already have. Without this, any logged-in user could aim the server at a URL of their choosing and
test credentials through it.
Cache-Control: no-store.All four are covered by tests.
Precedence subtlety
A step that identified a space - by id or by name - keeps its own selection, and the id and name
are inherited from the connection together. Inheriting the connection's id alongside a name the
step set deliberately would let the id win in
SpaceSelectionand silently retarget the step at thewrong space. There is a test for each direction.
Verification
./gradlew buildgreen.spotlessCheckclean.New unit tests: 8
SpaceSelectionTest, 10OctopusSpacesFetcherTest, 11OctopusSpacesControllerTest, plus 3 precedence and 2 connection-description tests. 269 testsacross the three modules, 0 failures; all pre-existing tests pass untouched.
New e2e
OctopusSpacePickerUiTestdrives the real form in a browser against the real Octopuscontainer: Load spaces returns the actual space, selecting it stores the real id in
octopus_space_id, and a parameter-sourced connection shows the explanatory message. Itcompiles but has not been executed - the Docker daemon on the machine this was written on stopped
responding (
docker psanddocker versionboth hang against a live OrbStack socket), so thecontainer stack never came up. It needs a run before merge:
OctopusConnectionUiTestis worth including in that run: it asserts the step's space field ishidden when the connection defines a space, and this change replaced that row with the shared
include. The picker test also covers the one assumption not provable by unit tests - that
props:hiddenPropertyrenders a field namedoctopus_space_idthat round-trips throughTeamCity's properties bean.
checkLicensesfails on this branch, but it fails identically onmain-teamcity-rest-client(no license found) andretrofit1-okhttp3-client. Pre-existing andunrelated; the deps added here (gson, commons-io, both already used elsewhere in the repo)
introduce no new license findings.
Follow-ups, not in here
create-release-action'saction.ymldocumentsspaceas name-only whilepush-package-actionsays "name or ID"; the JSapi-clientis genuinely name-only, fixedseparately in Allow a space id to be used wherever a space name is accepted api-client.ts#293.
TeamModernDeployments_*), the Slack channel in project descriptions, andmodern-deployment-calamari-jwt-tokenall still carry old team naming. Out of scope here.