镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 110 additions & 1 deletion pkg/cmd/release/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
Expand All @@ -28,6 +29,7 @@ import (
"github.com/OctopusDeploy/cli/pkg/util/flag"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/channels"
octopusApiClient "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/deployments"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
Expand Down Expand Up @@ -318,7 +320,7 @@ func createRun(cmd *cobra.Command, f factory.Factory, flags *CreateFlags) error
executor.NewTask(executor.TaskTypeCreateRelease, options),
})
if err != nil {
return err
return DiagnoseCreateReleaseFailure(octopus, options, err)
}

if options.Response != nil {
Expand Down Expand Up @@ -420,6 +422,113 @@ func BuildPackageVersionBaselineForChannel(octopus *octopusApiClient.Client, dep
return result, nil
}

// DiagnoseCreateReleaseFailure replaces an opaque server-side failure with an actionable message where
// it can. The server raises a null reference exception, surfaced as a bare 500, when it can't select a
// version for a package; see https://github.com/OctopusDeploy/cli/issues/426
//
// Any 500 is diagnosed, not just the null reference one, because the message a server sends for this
// varies by version: current servers report "no viable release plans" instead. The cost of being wrong
// is bounded, since MissingPackageVersionsError reports what the server actually said alongside the
// diagnosis. Other 5xx codes are excluded: the failure we are looking for is always raised by the API
// itself as a 500, so a 502/503/504 is something in front of the server and never worth replaying.
func DiagnoseCreateReleaseFailure(octopus *octopusApiClient.Client, options *executor.TaskOptionsCreateRelease, cause error) error {
var apiError *core.APIError
if !errors.As(cause, &apiError) || apiError.StatusCode != http.StatusInternalServerError {
return cause
}

// 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 {
return packages.NewMissingPackageVersionsError(missingPackages, cause)
}
}

if strings.Contains(apiError.ErrorMessage, packages.ServerNullReferenceMessage) {
return fmt.Errorf("%w\nthe server failed with an unhandled error; this usually means it could not resolve the packages, channel or git reference for the release", cause)
}
return cause
}

// findPackagesWithoutVersions repeats the package version resolution the server does when it assembles a
// release, so we can report which packages have no version available in their feed.
func findPackagesWithoutVersions(octopus *octopusApiClient.Client, options *executor.TaskOptionsCreateRelease) ([]releases.ReleaseTemplatePackage, error) {
project, err := selectors.FindProject(octopus, options.ProjectName)
if err != nil {
return nil, err
}

gitReferenceKey := ""
if project.PersistenceSettings != nil && project.PersistenceSettings.Type() == projects.PersistenceSettingsTypeVersionControlled {
gitReferenceKey = options.GitReference
if options.GitCommit != "" { // prefer a specific git commit if one was specified
gitReferenceKey = options.GitCommit
}
}

deploymentProcess, err := octopus.DeploymentProcesses.Get(project, gitReferenceKey)
if err != nil {
return nil, err
}

channel, err := findChannelForDiagnosis(octopus, project, options.ChannelName)
if err != nil {
return nil, err
}

deploymentProcessTemplate, err := octopus.DeploymentProcesses.GetTemplate(deploymentProcess, channel.ID, "")
if err != nil {
return nil, err
}

// mirror what the server did: with --ignore-channel-rules it selects versions without applying the
// channel's version rules, so applying them here would report packages as missing when they only
// failed the rules.
var packageVersionBaseline []*packages.StepPackageVersion
if options.IgnoreChannelRules {
packageVersionBaseline, err = packages.BuildPackageVersionBaseline(octopus, deploymentProcessTemplate.Packages, nil)
} else {
packageVersionBaseline, err = BuildPackageVersionBaselineForChannel(octopus, deploymentProcessTemplate, channel)
}
if err != nil {
return nil, err
}

overrides := packages.BuildPackageVersionOverrides(packageVersionBaseline, options.DefaultPackageVersion, options.PackageVersionOverrides)
resolvedVersions := packages.ApplyPackageOverrides(packageVersionBaseline, overrides)

return packages.FindPackagesWithoutVersions(deploymentProcessTemplate.Packages, resolvedVersions), nil
}

// findChannelForDiagnosis locates the channel the server would have used. --channel reaches the server as
// ChannelIDOrName, so we match on either. When no channel was specified we can only guess; the default
// channel is the best approximation available to us.
func findChannelForDiagnosis(octopus *octopusApiClient.Client, project *projects.Project, channelIDOrName string) (*channels.Channel, error) {
existingChannels, err := octopus.Projects.GetChannels(project)
if err != nil {
return nil, err
}

if channelIDOrName != "" {
for _, c := range existingChannels {
if strings.EqualFold(c.Name, channelIDOrName) || c.ID == channelIDOrName {
return c, nil
}
}
return nil, fmt.Errorf("no channel found with name or ID of %s", channelIDOrName)
}

if len(existingChannels) == 1 {
return existingChannels[0], nil
}
for _, c := range existingChannels {
if c.IsDefault {
return c, nil
}
}
return nil, fmt.Errorf("cannot determine the default channel for project %s", project.GetName())
}

func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker question.Asker, options *executor.TaskOptionsCreateRelease) error {
if octopus == nil {
return cliErrors.NewArgumentNullOrEmptyError("octopus")
Expand Down
Loading