Skip to content

fix: accept IDs as well as names for --channel, --environment and --tenant - #702

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

fix: accept IDs as well as names for --channel, --environment and --tenant#702
NickJosevski wants to merge 1 commit into
mainfrom
nj/issue-250

Conversation

@NickJosevski

Copy link
Copy Markdown
Contributor

Fixes #250

The bug

The reporter's build tooling uses IDs everywhere (names change, IDs don't). --project accepts an ID, but --channel, --environment and --tenant did not:

Octopus API error: Channel named 'Channels-681' within project '<name>' was not found.
Octopus API error: Environment named 'Environments-201' within project '<name>' was not found.

Root cause: the executions API (releases/create/v1, deployments/create/{un}tenanted/v1, runbook-runs/create/v1) takes projectName as an ID-or-name — the server resolves it — but channelIDOrName, environmentName(s) and tenants are matched by name only, server side. The CLI passed whatever the caller typed straight through, so IDs blew up.

Name-vs-ID inventory (before this PR)

release create

Flag Resolved by Accepted before
--project selectors.FindProjectProjects.GetByIdentifier ID or name
--channel selectors.FindChannel (interactive) / passed through (--no-prompt) name only
--git-ref, --git-commit passed to server git refs, n/a
--package, --package-version, --git-resource step/package names name only (no ID concept)
--version, --release-notes, --ignore-*, --custom-fields n/a n/a

release deploy

Flag Resolved by Accepted before
--project selectors.FindProject ID or name
--environment executionscommon.FindEnvironments (some paths) / passed through mixed — see note
--tenant passed straight through name only
--tenant-tag canonical tag paths (Regions/us-east) n/a — no ID form
--deployment-target / --exclude-deployment-target SpecificMachineNames / ExcludedMachineNames name only
--skip step names name only
--deployment-freeze-name DeploymentFreezeNames name only
--version, --deploy-at, --variable, … n/a n/a

Note on --environment: executionscommon.FindEnvironments already matched name-or-ID, but (a) only some code paths called it, (b) it never wrote the canonical name back into the options, so the raw ID still went to the server, and (c) the tenanted path used selectors.FindEnvironment, which was name-only.

runbook run

Flag Resolved by Accepted before
--project selectors.ResolveProject ID or name
--snapshot runbooks.GetSnapshot ID or name
--name / --runbook runbooks.GetByName name only
--runbook-tag tags n/a
--environment passed through in automation mode name only
--tenant passed straight through name only
--tenant-tag canonical tag paths n/a
--run-target / --exclude-run-target machine names name only
--skip step names name only

What changed

Everything goes through the shared selectors package, so every caller benefits:

  • selectors.FindEnvironments / FindEnvironment (pkg/question/selectors/environments.go) — the one environment resolver. One GET /environments/all, matched client side by ID first, then name. executionscommon.FindEnvironments now delegates to it, so its ~12 existing callers (target create commands, tenant connect) pick up the consistent precedence for free.
  • selectors.FindChannel — now matches a channel's ID as well as its name, still scoped to the project so an ID from another project is never returned.
  • selectors.FindTenant / FindTenants (new pkg/question/selectors/tenants.go) — wraps Tenants.GetByIdentifier (ID first, then name), turning "not found" into a clear message.
  • release create — resolves --channel in the --no-prompt path and sends the canonical name.
  • release deploy — resolves --tenant for both paths and --environment in the --no-prompt path; the interactive paths now write the resolved canonical name back into the options so what's sent (and what appears in the generated automation command) is the real name, not the raw input.
  • runbook run — resolves --environment and --tenant once, right after the project is resolved, covering the db/git/by-tag and prompt/no-prompt paths.
  • Ephemeral environments aren't in the regular environment list, so release deploy falls back to the ephemeral lookup when the regular one comes up empty (resolveEnvironmentNames).

Precedence rule

Try an exact ID match first, fall back to an exact name match (case-insensitive).

This matches what --project already does (Projects.GetByIdentifier = GetByID, then GetByName) and what channel/shared.ResolveChannel already did. So an entity that is genuinely named Environments-201 loses to the entity whose ID is Environments-201. It is documented in the resolver comments and covered by tests ("prefers an ID match over a name match").

Note this flips the old precedence inside executionscommon.FindEnvironments, which tried name first. That only matters when a name collides with a different entity's ID, and consistency with --project seemed more valuable than preserving the old order.

Error messages

Uniform and explicit about both forms being accepted:

  • cannot find an environment with the ID or name of 'Environments-404'
  • cannot find a channel in project 'Fire Project' with the ID or name of 'Channels-404'
  • cannot find a tenant with the ID or name of 'Tenants-404'

The channel message previously read no channel found with name of X; the two channel test files touched (channel/delete, channel/view) only update that expected string — no behaviour change to those commands.

Test evidence

New tests:

  • pkg/question/selectors/find_test.go — table tests for FindEnvironments/FindEnvironment/FindChannel/FindTenants: by name, by name ignoring case, by ID, several at once, ID beats a colliding name, and the not-found error text.
  • release create — "release creation specifying the project and channel by ID": asserts the POST body carries ChannelIDOrName: "BetaChannel" when --channel Channels-31 was passed.
  • release deploy — "release deploy specifying project, environment and tenant by ID": asserts EnvironmentName: "dev" / Tenants: ["Coke"] when --environment Environments-12 --tenant Tenants-29 was passed.
  • runbook run — "runbook run specifying project, environment and tenant by ID": same assertion for runbook-runs/create/v1.

Existing tests updated for the extra lookup requests (and one deploy test now asserts "Ephemeral Environment" instead of the lowercase "ephemeral environment" the user typed — that normalisation is the fix working).

$ go build ./...
(clean)

$ go test ./pkg/...
ok — 64 packages, 0 failures

go vet ./... reports only two pre-existing unreachable code warnings in pkg/cmd/tenant/variables/list/list.go, untouched here.

Open questions / options

  1. Precedence: ID first, or name first? Chosen: ID first, for consistency with --project. The alternative — name first — would mean an environment named Environments-201 shadows the environment with that ID, which is arguably the more surprising outcome and is inconsistent with --project. A third option is to error on ambiguity, which is the most correct but adds a failure mode to a hot path for a collision nobody has ever reported. Recommendation: keep ID first.

  2. Extra round trip. runbook run --environment X and release deploy --no-prompt --environment X now always do one GET /environments/all (and one or two calls per --tenant) that they previously skipped. That's the price of resolving client side. The alternative is a "looks like an ID" regex (^Environments-\d+$) to skip the lookup for plain names — faster, but it bakes in an ID-format assumption and makes the collision case incoherent. Recommendation: keep the unconditional lookup; revisit only if someone measures a problem.

  3. executionscommon.FindEnvironments is now a one-line alias for selectors.FindEnvironments. I kept it so the ~12 callers in the target-create commands and tenant connect stay untouched and this PR's diff stays on-topic. Recommendation: delete it and update the call sites in a follow-up, in line with the recent selectors.ResolveProject / selectors.Channel consolidation — happy to fold that in here instead if reviewers prefer.

  4. Flags deliberately left alone — all name-only, all out of scope, none reported: --deployment-target / --exclude-deployment-target / --run-target / --exclude-run-target (machine names), --skip (step names), --deployment-freeze-name, --name/--runbook (runbook name), and the package/git-resource step specs. Machines and runbooks would be the natural next candidates if we want ID support everywhere; the server-side commands for those take names only, so each needs the same client-side resolve-then-send treatment. Recommendation: follow-up issue rather than growing this PR. --tenant-tag has no ID form at all — canonical tag paths are the only identifier.

  5. Should release create --channel resolve in interactive mode too? It already did (via selectors.FindChannel), and now picks up ID support for free. Worth a reviewer sanity check that echoing the resolved name back (rather than the ID the user typed) is the desired UX. I think it is — it confirms what the ID pointed at.

🤖 Generated with Claude Code

…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>
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.

Some arguments do not accept the ID instead of the name

1 participant