Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Deployments now fail early with a clear error if the entrypoint file does not exist. (#2964)
- Log messages in the Publisher Logs panel are no longer duplicated when multiple deployments are made. (#3069)
- Fix creating deployments of R Markdown documents that include parameters on Connect Cloud. (#3388)
- The Publisher outputs (detailed logs) have been consolidated into one channel called "Posit Publisher". (#3168)
Expand Down
35 changes: 35 additions & 0 deletions internal/clients/connect/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,37 @@ func checkRequirementsFile(base util.AbsolutePath, cfg *config.Config) error {
return nil
}

const entrypointFileMissing = `entrypoint file '%s' does not exist`

type entrypointErrDetails struct {
Entrypoint string `json:"entrypoint"`
}

func checkEntrypoint(base util.AbsolutePath, cfg *config.Config) error {
// Module references (e.g., "app:myapp", "shiny.express.app:app_2e_py")
// are not file paths and should not be validated as files.
// Module references are currently constructed by reading the file so we
// already know the file exists at this point.
if strings.Contains(cfg.Entrypoint, ":") {
return nil
}

entrypointPath := base.Join(cfg.Entrypoint)
exists, err := entrypointPath.Exists()
if err != nil {
return err
}

if !exists {
return types.NewAgentError(
types.ErrorEntrypointNotFound,
fmt.Errorf(entrypointFileMissing, cfg.Entrypoint),
entrypointErrDetails{Entrypoint: cfg.Entrypoint},
)
}
return nil
}

func (c *ConnectClient) CheckCapabilities(base util.AbsolutePath, cfg *config.Config, contentID *types.ContentID, log logging.Logger) error {
if contentID != nil && *contentID != "" {
err := c.ValidateDeploymentTarget(*contentID, cfg, log)
Expand All @@ -72,6 +103,10 @@ func (c *ConnectClient) CheckCapabilities(base util.AbsolutePath, cfg *config.Co
return err
}
}
err := checkEntrypoint(base, cfg)
if err != nil {
return err
}
settings, err := c.GetSettings(base, cfg, log)
if err != nil {
return err
Expand Down
42 changes: 42 additions & 0 deletions internal/clients/connect/capabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/posit-dev/publisher/internal/clients/connect/server_settings"
"github.com/posit-dev/publisher/internal/config"
"github.com/posit-dev/publisher/internal/contenttypes"
"github.com/posit-dev/publisher/internal/util"
"github.com/posit-dev/publisher/internal/util/utiltest"
)

Expand Down Expand Up @@ -307,3 +308,44 @@ func (s *CapabilitiesSuite) TestKubernetesGPULimits() {
s.ErrorContains(a.checkConfig(makeGPURequest(5, 0)), "amd_gpu_limit value of 5 is higher than configured maximum of 1 on this server")
s.ErrorContains(a.checkConfig(makeGPURequest(0, 5)), "nvidia_gpu_limit value of 5 is higher than configured maximum of 2 on this server")
}

func (s *CapabilitiesSuite) TestCheckEntrypointNotFound() {
base := util.NewAbsolutePath(s.T().TempDir(), nil)
cfg := &config.Config{
Entrypoint: "nonexistent.py",
}
err := checkEntrypoint(base, cfg)
s.Error(err)
s.ErrorContains(err, "entrypoint file 'nonexistent.py' does not exist")
}

func (s *CapabilitiesSuite) TestCheckEntrypointExists() {
base := util.NewAbsolutePath(s.T().TempDir(), nil)
// Create a temporary file to test against
entrypointFile := base.Join("app.py")
err := entrypointFile.WriteFile([]byte("# test app"), 0644)
s.NoError(err)

cfg := &config.Config{
Entrypoint: "app.py",
}
err = checkEntrypoint(base, cfg)
s.NoError(err)
}

func (s *CapabilitiesSuite) TestCheckEntrypointModuleReference() {
base := util.NewAbsolutePath(s.T().TempDir(), nil)
// Module references like "app:myapp" or "shiny.express.app:app_2e_py"
// should not be validated as file paths
cfg := &config.Config{
Entrypoint: "shiny.express.app:app_2e_py",
}
err := checkEntrypoint(base, cfg)
s.NoError(err)

cfg2 := &config.Config{
Entrypoint: "app:myapp",
}
err = checkEntrypoint(base, cfg2)
s.NoError(err)
}
1 change: 1 addition & 0 deletions internal/types/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const (
ErrorDeviceAuthSlowDown ErrorCode = "deviceAuthSlowDown"
ErrorDeviceAuthAccessDenied ErrorCode = "deviceAuthAccessDenied"
ErrorDeviceAuthExpiredToken ErrorCode = "deviceAuthExpiredToken"
ErrorEntrypointNotFound ErrorCode = "entrypointNotFound"
)

type EventableError interface {
Expand Down