Skip to content

test: integration tests for the tier 1 release fixes - #703

Draft
NickJosevski wants to merge 10 commits into
mainfrom
nj/tier1-integration-tests
Draft

test: integration tests for the tier 1 release fixes#703
NickJosevski wants to merge 10 commits into
mainfrom
nj/tier1-integration-tests

Conversation

@NickJosevski

Copy link
Copy Markdown
Contributor

Adds end-to-end integration tests for the four "Tier 1" fixes — the ones whose correctness depends on how the real Octopus Server behaves, which testutil.MockHttpServer cannot validate by construction.

Refs #294, #426, #250, #556.

Why this branch stacks the four fixes

The tests assert post-fix behaviour, so they need the fixes present to pass. This branch merges the four feature branches and adds the tests on top:

Merged PR
nj/issue-294 #696
nj/issue-426 #695
nj/issue-250 #702
nj/issue-556 #692

This branch is not for merging as-is. It exists to prove the four compose and to carry the new tests. Once the four land on main, the last commit here rebases onto main on its own.

Tests added

All in test/integration/release_test.go, following the existing harness (integration.RunCli, CreateCommonProject, t.Cleanup teardown).

Verification

Run against a real Octopus Server (local dev instance, server main):

  • On this branch: all 4 pass, 20.7s, clean teardown.
  • On unmodified main with the same test file: all 4 fail. They are genuine regression tests, not tests that pass either way.
  • go build ./... clean; go test ./pkg/... green (63 packages).

Finding: the null-reference symptom no longer reproduces

#294 and #426 both describe Octopus API error: Object reference not set to an instance of an object. []. On a current server that is not what happens:

The server-side defect appears to have been fixed since those issues were filed (2022.3 and 2024.4 respectively). Both CLI fixes still improve the message materially, and older servers still exhibit the original behaviour — but the premise that the server null-refs is no longer true on current versions. Two consequences:

  1. The suggestion in fix: report missing package versions instead of a server null reference #695 to raise a matching Server issue is probably moot; worth confirming before filing.
  2. The assert.NotContains(..., "Object reference not set") lines in these tests are not load-bearing on a current server. They are kept as regression guards for older ones; the positive assertions are what carry the tests.

Finding: the four fixes do not compose without test changes

Each of the four is green on its own branch, but merged they break each other's unit tests — invisible on the individual branches by construction. Fixed in the first commit here:

None of these are defects in the individual PRs; they are ordinary merge fallout. Flagging them because whichever of the four merges last will hit exactly this, and the failure mode for two of them is a hang, not a red test.

Notes

  • test/integration has no build tag and GetApiClient calls os.Exit(999) when OCTOPUS_TEST_URL/OCTOPUS_TEST_APIKEY are unset, so a bare go test ./... from the repo root hard-exits. Run the suite from test/integration.
  • The two deploy tests queue a real server task and wait for it to complete, because a project cannot be deleted while one is running. Whether the deployment succeeds is not asserted.
  • allowDeploymentsTo restores the fixture lifecycle's phases on cleanup, otherwise the environment cannot be deleted.

🤖 Generated with Claude Code

NickJosevski and others added 9 commits August 19, 2026 11:42
`--deployment-target "ABC,XYZ"` was sent to the server as a single target
name because the flag is a pflag StringArray, while its legacy aliases
(`--target`, `--specificMachines`) are StringSlice and already split on
commas. Expand comma-separated values for the environment, tenant,
tenant-tag and target flags on `release deploy` and `runbook run`, so the
comma form matches the repeat-the-flag form. Values that can legitimately
contain a comma (--variable, --skip, package/git-resource specs) are left
alone.

Fixes #556

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`release create --no-prompt` sends the create request straight to the server
without resolving package versions first. When a package has no version in its
feed the server raises a null reference exception, which surfaces as
"Octopus API error: Object reference not set to an instance of an object. []".

On a 5xx failure the CLI now repeats the package version resolution the server
does, and reports the packages, steps and feeds that have no version available.
Where it can't identify a specific package, an unhandled server error now
carries a hint about the likely causes.

Fixes #426

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`release deploy` passed --version straight to the executions API, which
answers an unknown version with "Object reference not set to an instance
of an object". Resolve the release before deploying so a version that
doesn't exist is reported by name, and call out `latest` explicitly since
it is not a supported alias.

Refs #294

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…enant

The executions API only matches channels, environments and tenants by name,
so `release create`, `release deploy` and `runbook run` passed whatever the
caller typed straight through and the server rejected IDs. `--project`
already worked because the server accepts a project ID or name.

Resolve those identifiers client side through the shared selectors package
before handing them to the executor, preferring an ID match over a name
match so it behaves the same way as `--project`.

Fixes #250

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	pkg/cmd/release/deploy/deploy_test.go
The four fixes are green individually but their mock request sequences
disagree once merged: #294 adds a release pre-flight lookup and removes the
post-deploy web URL lookups, #250 adds an environment lookup, and the tests
#250 and #556 introduce expect neither. Two of those cases deadlock the mock
server rather than failing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers behaviour that only a real server exercises: unknown release versions,
packages with no version in their feed, channel and environment IDs on the
executions API, and comma-separated deployment targets.

Refs #294, #426, #250, #556

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
})
if err != nil {
return err
return DiagnoseCreateReleaseFailure(octopus, options, err)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nil pointer dereference on the post-create lookup failure path (pre-existing, but this function is being touched here and the new diagnosis flow makes create failures more visible): a few lines below at the options.Response handling, when octopus.Releases.GetByID(options.Response.ReleaseID) fails, the error branch still dereferences the nil result:

newlyCreatedRelease, lookupErr := octopus.Releases.GetByID(options.Response.ReleaseID)
if lookupErr != nil {
    cmd.PrintErrf("Warning: cannot fetch release details: %v\n", lookupErr)
    printReleaseVersion(options.Response.ReleaseVersion, newlyCreatedRelease.Assembled, newlyCreatedRelease.ReleaseNotes, nil)

ReleaseService.GetByID returns nil, err on failure, so a transient server error right after a successful create panics the CLI instead of printing the warning.


// diagnosis is best-effort; if any part of it fails we must not mask the original failure
if octopus != nil && options != nil {
if missingPackages, findErr := findPackagesWithoutVersions(octopus, options); findErr == nil && len(missingPackages) > 0 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package diagnosis can misattribute an unrelated 5xx and hide the real cause. This branch runs for any 5xx, not just the null-reference case, and MissingPackageVersionsError.Error() does not include the original server message (it is only reachable via Unwrap). If the server 500s for an unrelated reason (timeout, genuine server bug) while the project happens to contain a package with no version in its feed — or the CLI's re-derived baseline disagrees with the server (e.g. a --package override the CLI silently failed to parse but the server accepted) — the user is told to push packages instead of seeing the actual failure.

Consider gating the missing-package diagnosis on strings.Contains(apiError.ErrorMessage, serverNullReferenceMessage) as well, and/or including the wrapped cause text in the error output.

// `--flag A --flag B`. Whitespace around each entry is trimmed and blank entries are dropped.
// Only apply this to flags whose values cannot legitimately contain a comma; notably NOT to
// --variable, --skip or the package/git-resource specs.
func ExpandCommaSeparated(values []string) []string {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regression for names that legitimately contain commas, with no escape hatch. Octopus allows commas in environment/tenant/machine names. Before this change --environment "Dev, East" (one env named Dev, East) worked; now it is unconditionally split into Dev + East and the deploy fails with "cannot find an environment...". The doc comment acknowledges the constraint but there is no way for a user to opt out (quoting doesn't help — the split happens after shell parsing).

The old octo CLI had the same splitting behaviour, so this may be an accepted trade-off — but worth an explicit decision and a mention in the flag help/changelog, since previously-working invocations now break silently.


// FindTenant looks a tenant up by either its ID or its name.
func FindTenant(octopus *octopusApiClient.Client, tenantIdentifier string) (*tenants.Tenant, error) {
tenant, err := octopus.Tenants.GetByIdentifier(tenantIdentifier)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False "cannot find a tenant" for tenants beyond the first page of a partial-name search. The SDK's Tenants.GetByIdentifier name fallback (GetByName) issues Get(TenantsQuery{PartialName: name}) and scans only the first page of results for an exact match — it never pages. On a space with many tenants whose names share a common substring (e.g. dozens of "Store ..." tenants), a tenant whose exact name lands beyond page 1 returns ErrItemNotFound, and this new resolution step fails a deploy/runbook run that previously worked (the raw name used to be passed straight to the executions API, which matched it fine).

Also note GetByIdentifier uses a direct type assertion on the GetByID error, so a non-APIError failure (network blip) silently falls through to the name lookup rather than being reported.

// resolveEnvironmentNames maps environment names or IDs onto canonical environment names, because
// the executions API only matches environments by name. Ephemeral environments aren't part of the
// regular environment list, so they're looked up separately when the regular lookup comes up empty.
func resolveEnvironmentNames(octopus *octopusApiClient.Client, space *spaces.Space, environmentIdentifiers []string) ([]string, error) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A mixed list of regular + ephemeral environment identifiers can never resolve. selectors.FindEnvironments errors on the first identifier that isn't a regular environment, and the fallback findEphemeralEnvironments errors on the first identifier that isn't ephemeral — so --environment regularEnv,ephemeralEnv now fails client-side with "cannot find an environment ..." even though before this change both names were passed through verbatim for the server to judge. Probably invalid server-side anyway (one channel type per deployment), but if so the current error message points at the wrong thing: it claims the regular env doesn't exist when it does. Resolving each identifier individually (regular-then-ephemeral per item) would handle both this and give a precise error.

idLookup := make(map[string]*environments.Environment, len(allEnvs))
nameLookup := make(map[string]*environments.Environment, len(allEnvs))
for _, env := range allEnvs {
idLookup[strings.ToLower(env.GetID())] = env

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent precedence flip for existing callers. The old executionscommon.FindEnvironments checked the name lookup before the ID lookup; this new implementation checks ID first. Since executionscommon.FindEnvironments is now an alias to this, the change reaches all its existing callers (tenant connect, five target ... create commands, runbook, deploy): in a space where an environment is named the same as another environment's ID, those commands now resolve to a different environment than before. The tests show this is deliberate ("consistent with how projects and tenants resolve") — flagging it because it's an observable behaviour change to commands this PR doesn't otherwise touch, and may deserve a changelog note.


// the executions API only matches tenants by name, so resolve any IDs we were given
if len(options.Tenants) > 0 {
selectedTenants, err := selectors.FindTenants(octopus, options.Tenants)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every automation-mode invocation now pays extra HTTP round trips even when plain names were given. For each tenant supplied by name this is two requests (a guaranteed 404 on GET /tenants/<name>, then the partial-name search), plus GET /environments/all, plus the release lookup — sequentially, on every CI deploy. The same pattern is in runbook run (and there the environment/tenant resolution also runs in interactive mode, where AskQuestions resolves environments again — duplicate /environments/all calls).

A cheap win: only hit the ID lookup when the identifier actually looks like an ID (^Tenants-\d+$ / ^Environments-\d+$), or resolve tenants with a single query instead of per-tenant round trips. Related: selectors.FindEnvironment (singular) previously used a paged partial-name server query and now loads the entire space's environment list to find one environment.

}

// the executions API only matches environments by name, so resolve any IDs we were given
if len(options.Environments) > 0 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Altitude: the ID-to-name resolution is scattered per command, at differing depths. release create resolves the channel only in the automation branch; release deploy resolves tenants before both modes but environments only in the automation branch; runbook run resolves both unconditionally. The invariant they all enforce ("the executions API only matches by name") belongs to the layer that builds the executions-API commands (pkg/executor/release.go / the runbook executor), where one implementation would cover all three commands, both modes, and any future execution flag — instead of a pattern that has to be remembered (and is already applied inconsistently) in each command. Fine to land as-is for the tier-1 fixes, but worth a follow-up.

ReleaseService.GetByID returns (nil, err) on failure, so the warning
branch after a successful create panicked instead of printing the
warning: it read Assembled and ReleaseNotes off the nil result. A
transient server error immediately after the release was created took
the CLI down with a nil pointer dereference.

Print the version with empty details instead, and treat a nil release
with no error as a lookup failure too.

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.

1 participant