diff --git a/cli/azd/.vscode/cspell-azd-dictionary.txt b/cli/azd/.vscode/cspell-azd-dictionary.txt index a1227aab4ca..9ce71a74e4c 100644 --- a/cli/azd/.vscode/cspell-azd-dictionary.txt +++ b/cli/azd/.vscode/cspell-azd-dictionary.txt @@ -28,6 +28,7 @@ containerapp csharpapp devel dockerproject +dskip eastus envname errcheck @@ -40,11 +41,13 @@ GOARCH godotenv golangci ineffassign +javac jmes keychain ldflags mgmt mgutz +mvnw nobanner nodeapp nolint @@ -62,6 +65,7 @@ retriable rzip semconv serverfarms +setenvs sstore staticcheck staticwebapp @@ -73,6 +77,8 @@ teamcity tracesdk tracetest unmarshalling +unsets +unsetenvs utsname westus2 yacspin diff --git a/cli/azd/pkg/project/framework_service_maven.go b/cli/azd/pkg/project/framework_service_maven.go new file mode 100644 index 00000000000..8d71da3963b --- /dev/null +++ b/cli/azd/pkg/project/framework_service_maven.go @@ -0,0 +1,106 @@ +package project + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/tools" + "github.com/azure/azure-dev/cli/azd/pkg/tools/javac" + "github.com/azure/azure-dev/cli/azd/pkg/tools/maven" + "github.com/otiai10/copy" +) + +// The default, conventional App Service Java package name +const AppServiceJavaPackageName = "app.jar" + +type mavenProject struct { + config *ServiceConfig + env *environment.Environment + mavenCli maven.MavenCli + javacCli javac.JavacCli +} + +func (m *mavenProject) RequiredExternalTools() []tools.ExternalTool { + return []tools.ExternalTool{ + m.mavenCli, + m.javacCli, + } +} + +func (m *mavenProject) Package(ctx context.Context, progress chan<- string) (string, error) { + publishRoot, err := os.MkdirTemp("", "azd") + if err != nil { + return "", fmt.Errorf("creating staging directory: %w", err) + } + + progress <- "Creating deployment package" + if err := m.mavenCli.Package(ctx, m.config.Path()); err != nil { + return "", err + } + + publishSource := m.config.Path() + + if m.config.OutputPath != "" { + publishSource = filepath.Join(publishSource, m.config.OutputPath) + } else { + publishSource = filepath.Join(publishSource, "target") + } + + entries, err := os.ReadDir(publishSource) + if err != nil { + return "", fmt.Errorf("discovering JAR files in %s: %w", publishSource, err) + } + + matches := []string{} + for _, entry := range entries { + if entry.IsDir() { + continue + } + + if name := entry.Name(); strings.HasSuffix(name, ".jar") { + matches = append(matches, name) + } + } + + if len(matches) == 0 { + return "", fmt.Errorf("no JAR files found in %s", publishSource) + } + if len(matches) > 1 { + names := strings.Join(matches, ", ") + return "", fmt.Errorf("multiple JAR files found in %s: %s. Only a single runnable JAR file is expected", publishSource, names) + } + + err = copy.Copy(filepath.Join(publishSource, matches[0]), filepath.Join(publishRoot, AppServiceJavaPackageName)) + if err != nil { + return "", fmt.Errorf("copying to staging directory failed: %w", err) + } + + return publishRoot, nil +} + +func (m *mavenProject) InstallDependencies(ctx context.Context) error { + if err := m.mavenCli.ResolveDependencies(ctx, m.config.Path()); err != nil { + return fmt.Errorf("resolving maven dependencies: %w", err) + } + + return nil +} + +func (m *mavenProject) Initialize(ctx context.Context) error { + return nil +} + +func NewMavenProject(ctx context.Context, config *ServiceConfig, env *environment.Environment) FrameworkService { + runner := exec.GetCommandRunner(ctx) + return &mavenProject{ + config: config, + env: env, + mavenCli: maven.NewMavenCli(runner, config.Path(), config.Project.Path), + javacCli: javac.NewCli(runner), + } +} diff --git a/cli/azd/pkg/project/service_config.go b/cli/azd/pkg/project/service_config.go index d71bc06534b..647dc5c7a85 100644 --- a/cli/azd/pkg/project/service_config.go +++ b/cli/azd/pkg/project/service_config.go @@ -108,6 +108,8 @@ func (sc *ServiceConfig) GetFrameworkService(ctx context.Context, env *environme frameworkService = NewPythonProject(ctx, sc, env) case "js", "ts": frameworkService = NewNpmProject(ctx, sc, env) + case "java": + frameworkService = NewMavenProject(ctx, sc, env) default: return nil, fmt.Errorf("unsupported language '%s' for service '%s'", sc.Language, sc.Name) } diff --git a/cli/azd/pkg/tools/javac/javac.go b/cli/azd/pkg/tools/javac/javac.go new file mode 100644 index 00000000000..d448e55ecaf --- /dev/null +++ b/cli/azd/pkg/tools/javac/javac.go @@ -0,0 +1,124 @@ +package javac + +import ( + "context" + "errors" + "fmt" + "os" + osexec "os/exec" + "path/filepath" + + "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/tools" + "github.com/blang/semver/v4" +) + +const javac = "javac" + +type JavacCli interface { + tools.ExternalTool +} + +type javacCli struct { + cmdRun exec.CommandRunner +} + +func NewCli(cmdRun exec.CommandRunner) JavacCli { + return &javacCli{ + cmdRun: cmdRun, + } +} + +func (j *javacCli) VersionInfo() tools.VersionInfo { + return tools.VersionInfo{ + MinimumVersion: semver.Version{ + Major: 17, + Minor: 0, + Patch: 0}, + UpdateCommand: "Visit the website for your installed JDK to upgrade", + } +} + +func (j *javacCli) CheckInstalled(ctx context.Context) (bool, error) { + path, err := getInstalledPath() + if err != nil { + return false, err + } + + runResult, err := j.cmdRun.Run(ctx, exec.RunArgs{ + Cmd: path, + Args: []string{"--version"}, + }) + if err != nil { + return false, fmt.Errorf("checking javac version: %w", err) + } + + jdkVer, err := tools.ExtractSemver(runResult.Stdout) + if err != nil { + return false, fmt.Errorf("converting to semver version fails: %w", err) + } + + requiredVersion := j.VersionInfo() + if jdkVer.LT(requiredVersion.MinimumVersion) { + return false, &tools.ErrSemver{ToolName: j.Name(), VersionInfo: requiredVersion} + } + + return true, nil +} + +func (j *javacCli) InstallUrl() string { + return "https://www.microsoft.com/openjdk" +} + +func (j *javacCli) Name() string { + return "Java JDK" +} + +// getInstalledPath returns the installed javac path. +// +// javac is located by consulting, in search order: +// - JAVA_HOME (if set) +// - PATH +// +// An error is returned if javac could not be found, or if invalid locations are provided. +func getInstalledPath() (string, error) { + path, err := findByEnvVar("JAVA_HOME") + if path != "" { + return path, nil + } + if err != nil { + return "", fmt.Errorf("JAVA_HOME is set to an invalid directory: %w", err) + } + + path, err = osexec.LookPath(javac) + if err == nil { + return path, nil + } + + if !errors.Is(err, osexec.ErrNotFound) { + return "", fmt.Errorf("failed looking up javac in PATH: %w", err) + } + + return "", errors.New( + "javac could not be found. Set JAVA_HOME environment variable to point to your Java JDK installation, " + + "or include javac in your PATH environment variable") +} + +// findByEnvVar returns the javac path by the following environment variable home directory. +// +// An error is returned if an error occurred while finding. +// If the environment variable home directory is unset, an empty string is returned with no error. +func findByEnvVar(envVar string) (string, error) { + home := os.Getenv(envVar) + if home == "" { + return "", nil + } + + absPath := filepath.Join(home, "bin", javac) + absPath, err := osexec.LookPath(absPath) + if err != nil { + return "", err + } + + return absPath, nil +} diff --git a/cli/azd/pkg/tools/javac/javac_test.go b/cli/azd/pkg/tools/javac/javac_test.go new file mode 100644 index 00000000000..3c683cea2cc --- /dev/null +++ b/cli/azd/pkg/tools/javac/javac_test.go @@ -0,0 +1,137 @@ +package javac + +import ( + "context" + "fmt" + "os" + "path/filepath" + "runtime" + "testing" + + azdexec "github.com/azure/azure-dev/cli/azd/pkg/exec" + mockexec "github.com/azure/azure-dev/cli/azd/test/mocks/exec" + "github.com/azure/azure-dev/cli/azd/test/ostest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCheckInstalledVersion(t *testing.T) { + javaHome := t.TempDir() + javaHomeBin := filepath.Join(javaHome, "bin") + require.NoError(t, os.Mkdir(javaHomeBin, 0755)) + + placeJavac(t, javaHomeBin) + ostest.Setenv(t, "JAVA_HOME", javaHome) + + tests := []struct { + name string + stdOut string + want bool + wantErr bool + }{ + {name: "MetExact", stdOut: "javac 17.0.0.0", want: true}, + {name: "Met", stdOut: "javac 18.0.2.1", want: true}, + {name: "NotMet", stdOut: "javac 15.0.0.0", wantErr: true}, + {name: "InvalidSemVer", stdOut: "javac NoVer", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + execMock := mockexec.NewMockCommandRunner(). + When(func(a azdexec.RunArgs, command string) bool { return true }). + Respond(azdexec.NewRunResult(0, tt.stdOut, "")) + + cli := NewCli(execMock) + ok, err := cli.CheckInstalled(context.Background()) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.want, ok) + }) + } +} + +func Test_getInstalledPath(t *testing.T) { + jdkHome := t.TempDir() + jdkHomeBin := filepath.Join(jdkHome, "bin") + require.NoError(t, os.Mkdir(jdkHomeBin, 0755)) + + javaHome := t.TempDir() + javaHomeBin := filepath.Join(javaHome, "bin") + require.NoError(t, os.Mkdir(javaHomeBin, 0755)) + + origPath := os.Getenv("PATH") + pathBin := t.TempDir() + pathVal := fmt.Sprintf("%s%c%s", pathBin, os.PathListSeparator, origPath) + ostest.Unsetenvs(t, []string{"JAVA_HOME", "PATH"}) + + tests := []struct { + name string + javacPaths []string + envVar map[string]string + testWindowsPathExt bool + want string + wantErr bool + }{ + { + name: "JavaHome", + javacPaths: []string{javaHomeBin}, + envVar: map[string]string{"JAVA_HOME": javaHome}, + want: filepath.Join(javaHomeBin, javacWithExt()), + wantErr: false, + }, + { + name: "Path", + javacPaths: []string{pathBin}, + envVar: map[string]string{"PATH": pathVal}, + want: filepath.Join(pathBin, javacWithExt()), + wantErr: false, + }, + { + name: "SearchJavaHomeFirst", + javacPaths: []string{javaHomeBin, pathBin}, + envVar: map[string]string{"JAVA_HOME": javaHome, "PATH": pathVal}, + want: filepath.Join(javaHomeBin, javacWithExt()), + wantErr: false, + }, + {name: "InvalidJavaHome", envVar: map[string]string{"JAVA_HOME": javaHome}, wantErr: true}, + {name: "NotFound", envVar: map[string]string{"PATH": pathBin}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + placeJavac(t, tt.javacPaths...) + ostest.Setenvs(t, tt.envVar) + + actual, err := getInstalledPath() + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.want, actual) + }) + } +} + +func placeJavac(t *testing.T, dirs ...string) { + for _, createPath := range dirs { + toCreate := filepath.Join(createPath, javacWithExt()) + ostest.Create(t, toCreate) + + err := os.Chmod(toCreate, 0755) + require.NoError(t, err) + } +} + +func javacWithExt() string { + if runtime.GOOS == "windows" { + // For Windows, we want to test EXT resolution behavior + return javac + ".exe" + } else { + return javac + } +} diff --git a/cli/azd/pkg/tools/maven/maven.go b/cli/azd/pkg/tools/maven/maven.go new file mode 100644 index 00000000000..abf335a14c4 --- /dev/null +++ b/cli/azd/pkg/tools/maven/maven.go @@ -0,0 +1,163 @@ +package maven + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "path/filepath" + "sync" + + osexec "os/exec" + + "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/tools" +) + +type MavenCli interface { + tools.ExternalTool + Package(ctx context.Context, projectPath string) error + ResolveDependencies(ctx context.Context, projectPath string) error +} + +type mavenCli struct { + commandRunner exec.CommandRunner + projectPath string + rootProjectPath string + + // Lazily initialized. Access through mvnCmd. + mvnCmdStr string + mvnCmdOnce sync.Once + mvnCmdErr error +} + +func (m *mavenCli) Name() string { + return "Maven" +} + +func (m *mavenCli) InstallUrl() string { + return "https://maven.apache.org" +} + +func (m *mavenCli) CheckInstalled(ctx context.Context) (bool, error) { + _, err := m.mvnCmd() + if err != nil { + return false, err + } + + return true, nil +} + +func (m *mavenCli) mvnCmd() (string, error) { + m.mvnCmdOnce.Do(func() { + mvnCmd, err := getMavenPath(m.projectPath, m.rootProjectPath) + if err != nil { + m.mvnCmdErr = err + } else { + m.mvnCmdStr = mvnCmd + } + }) + + if m.mvnCmdErr != nil { + return "", m.mvnCmdErr + } + + return m.mvnCmdStr, nil +} + +func getMavenPath(projectPath string, rootProjectPath string) (string, error) { + mvnw, err := getMavenWrapperPath(projectPath, rootProjectPath) + if mvnw != "" { + return mvnw, nil + } + + if err != nil { + return "", fmt.Errorf("failed finding mvnw in repository path: %w", err) + } + + mvn, err := osexec.LookPath("mvn") + if err == nil { + return mvn, nil + } + + if !errors.Is(err, osexec.ErrNotFound) { + return "", fmt.Errorf("failed looking up mvn in PATH: %w", err) + } + + return "", errors.New("maven could not be found. Install either Maven or Maven Wrapper by visiting https://maven.apache.org/ or https://maven.apache.org/wrapper/") +} + +// getMavenWrapperPath finds the path to mvnw in the project directory, up to the root project directory. +// +// An error is returned if an unexpected error occurred while finding. If mvnw is not found, an empty string is returned with no error. +func getMavenWrapperPath(projectPath string, rootProjectPath string) (string, error) { + searchDir, err := filepath.Abs(projectPath) + if err != nil { + return "", err + } + + root, err := filepath.Abs(rootProjectPath) + log.Printf("root: %s\n", root) + + if err != nil { + return "", err + } + + for { + log.Printf("searchDir: %s\n", searchDir) + + mvnw, err := osexec.LookPath(filepath.Join(searchDir, "mvnw")) + if err == nil { + log.Printf("found mvnw as: %s\n", mvnw) + return mvnw, nil + } + + if !errors.Is(err, os.ErrNotExist) { + return "", err + } + + searchDir = filepath.Dir(searchDir) + + // Past root, terminate search and return not found + if len(searchDir) < len(root) { + return "", nil + } + } +} + +func (cli *mavenCli) Package(ctx context.Context, projectPath string) error { + mvnCmd, err := cli.mvnCmd() + if err != nil { + return err + } + + // Maven's package phase includes tests by default. Skip it explicitly. + runArgs := exec.NewRunArgs(mvnCmd, "package", "-DskipTests").WithCwd(projectPath) + res, err := cli.commandRunner.Run(ctx, runArgs) + if err != nil { + return fmt.Errorf("mvn package on project '%s' failed: %s: %w", projectPath, res.String(), err) + } + return nil +} + +func (cli *mavenCli) ResolveDependencies(ctx context.Context, projectPath string) error { + mvnCmd, err := cli.mvnCmd() + if err != nil { + return err + } + runArgs := exec.NewRunArgs(mvnCmd, "dependency:resolve").WithCwd(projectPath) + res, err := cli.commandRunner.Run(ctx, runArgs) + if err != nil { + return fmt.Errorf("mvn dependency:resolve on project '%s' failed: %s: %w", projectPath, res.String(), err) + } + return nil +} + +func NewMavenCli(commandRunner exec.CommandRunner, projectPath string, rootProjectPath string) MavenCli { + return &mavenCli{ + commandRunner: commandRunner, + projectPath: projectPath, + rootProjectPath: rootProjectPath, + } +} diff --git a/cli/azd/pkg/tools/maven/maven_test.go b/cli/azd/pkg/tools/maven/maven_test.go new file mode 100644 index 00000000000..1fe5011d461 --- /dev/null +++ b/cli/azd/pkg/tools/maven/maven_test.go @@ -0,0 +1,113 @@ +package maven + +import ( + "log" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/azure/azure-dev/cli/azd/test/ostest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_getMavenPath(t *testing.T) { + rootPath := os.TempDir() + sourcePath := filepath.Join(rootPath, "src") + projectPath := filepath.Join(sourcePath, "api") + + pathDir := os.TempDir() + + require.NoError(t, os.MkdirAll(projectPath, 0755)) + ostest.Unsetenv(t, "PATH") + + type args struct { + projectPath string + rootProjectPath string + } + + tests := []struct { + name string + mvnwPath []string + mvnwRelative bool + mvnPath []string + envVar map[string]string + want string + wantErr bool + }{ + {name: "MvnwProjectPath", mvnwPath: []string{projectPath}, want: filepath.Join(projectPath, mvnwWithExt())}, + {name: "MvnwSrcPath", mvnwPath: []string{sourcePath}, want: filepath.Join(sourcePath, mvnwWithExt())}, + {name: "MvnwRootPath", mvnwPath: []string{rootPath}, want: filepath.Join(rootPath, mvnwWithExt())}, + {name: "MvnwFirst", mvnwPath: []string{rootPath}, want: filepath.Join(rootPath, mvnwWithExt()), + mvnPath: []string{pathDir}, envVar: map[string]string{"PATH": pathDir}}, + {name: "MvnwProjectPathRelative", mvnwPath: []string{projectPath}, want: filepath.Join(projectPath, mvnwWithExt()), mvnwRelative: true}, + {name: "MvnwSrcPathRelative", mvnwPath: []string{sourcePath}, want: filepath.Join(sourcePath, mvnwWithExt()), mvnwRelative: true}, + {name: "MvnwRootPathRelative", mvnwPath: []string{rootPath}, want: filepath.Join(rootPath, mvnwWithExt()), mvnwRelative: true}, + {name: "Mvn", mvnPath: []string{pathDir}, envVar: map[string]string{"PATH": pathDir}, want: filepath.Join(pathDir, mvnWithExt())}, + {name: "NotFound", want: "", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + placeExecutable(t, mvnwWithExt(), tt.mvnwPath...) + placeExecutable(t, mvnWithExt(), tt.mvnPath...) + ostest.Setenvs(t, tt.envVar) + + args := args{} + if tt.mvnwRelative { + ostest.Chdir(t, rootPath) + // Set PWD directly to avoid symbolic links + + ostest.Setenv(t, "PWD", rootPath) + projectPathRel, err := filepath.Rel(rootPath, projectPath) + require.NoError(t, err) + args.projectPath = projectPathRel + args.rootProjectPath = "" + } else { + args.projectPath = projectPath + args.rootProjectPath = rootPath + } + + wd, err := os.Getwd() + require.NoError(t, err) + log.Printf("rootPath: %s, cwd: %s, getMavenPath(%s, %s)\n", rootPath, wd, args.projectPath, args.rootProjectPath) + actual, err := getMavenPath(args.projectPath, args.rootProjectPath) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.want, actual) + }) + } +} + +func placeExecutable(t *testing.T, name string, dirs ...string) { + for _, createPath := range dirs { + toCreate := filepath.Join(createPath, name) + ostest.Create(t, toCreate) + + err := os.Chmod(toCreate, 0755) + require.NoError(t, err) + } +} + +func mvnWithExt() string { + if runtime.GOOS == "windows" { + // For Windows, we want to test EXT resolution behavior + return "mvn.cmd" + } else { + return "mvn" + } +} + +func mvnwWithExt() string { + if runtime.GOOS == "windows" { + // For Windows, we want to test EXT resolution behavior + return "mvnw.cmd" + } else { + return "mvnw" + } +} diff --git a/cli/azd/test/ostest/ostest.go b/cli/azd/test/ostest/ostest.go index aa8165e5a5b..aafa1495397 100644 --- a/cli/azd/test/ostest/ostest.go +++ b/cli/azd/test/ostest/ostest.go @@ -12,6 +12,7 @@ import ( "github.com/azure/azure-dev/cli/azd/internal/telemetry" azdexec "github.com/azure/azure-dev/cli/azd/pkg/exec" "github.com/sethvargo/go-retry" + "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" ) @@ -87,3 +88,108 @@ func removeAllWithDiagnostics(t *testing.T, path string) error { return retry.RetryableError(removeErr) }) } + +// Setenv sets the value of the environment variable named by the key. +// Any set values are automatically restored during test Cleanup. +func Setenv(t *testing.T, key string, value string) { + t.Setenv(key, value) +} + +// Unsetenv unsets the environment variable, which is later restored during test Cleanup. +func Unsetenv(t *testing.T, key string) { + orig, present := os.LookupEnv(key) + os.Unsetenv(key) + + t.Cleanup(func() { + if present { + os.Setenv(key, orig) + } + }) +} + +// Unsetenvs unsets the provided environment variables, which is later restored during test Cleanup. +func Unsetenvs(t *testing.T, keys []string) { + restoreContext := map[string]string{} + + for _, key := range keys { + orig, present := os.LookupEnv(key) + if present { + restoreContext[key] = orig + os.Unsetenv(key) + } + } + + if len(restoreContext) > 0 { + t.Cleanup(func() { + for _, key := range keys { + if restoreValue, present := restoreContext[key]; present { + os.Setenv(key, restoreValue) + } + } + }) + } +} + +// Setenvs sets the provided environment variables keys with their corresponding values. +// Any set values are automatically restored during test Cleanup. +func Setenvs(t *testing.T, envContext map[string]string) { + restoreContext := map[string]string{} + for key, value := range envContext { + orig, present := os.LookupEnv(key) + if present { + restoreContext[key] = orig + } + + os.Setenv(key, value) + } + + t.Cleanup(func() { + for key := range envContext { + if restoreValue, present := restoreContext[key]; present { + os.Setenv(key, restoreValue) + } else { + os.Unsetenv(key) + } + } + }) +} + +// Create creates or truncates the named file. If the file already exists, +// it is truncated. If the file does not exist, it is created with mode 0666 +// (before umask). +// Files created are automatically removed during test Cleanup. Ignores errors +// due to the file already being deleted. +func Create(t *testing.T, name string) { + CreateNoCleanup(t, name) + + t.Cleanup(func() { + err := os.Remove(name) + if !errors.Is(err, os.ErrNotExist) { + require.NoError(t, err) + } + }) +} + +// Create creates or truncates the named file. If the file already exists, +// it is truncated. If the file does not exist, it is created with mode 0666 +// (before umask). +func CreateNoCleanup(t *testing.T, name string) { + f, err := os.Create(name) + require.NoError(t, err) + defer f.Close() +} + +// Chdir changes the current working directory to the named directory. +// The working directory is automatically restored as part of Cleanup. +func Chdir(t *testing.T, dir string) { + wd, err := os.Getwd() + require.NoError(t, err) + + err = os.Chdir(dir) + require.NoError(t, err) + + t.Cleanup(func() { + err = os.Chdir(wd) + require.NoError(t, err) + }) +} diff --git a/schemas/v1.0/azure.yaml.json b/schemas/v1.0/azure.yaml.json index 09f0b6d1fa4..8af64cdec6a 100644 --- a/schemas/v1.0/azure.yaml.json +++ b/schemas/v1.0/azure.yaml.json @@ -94,7 +94,8 @@ "py", "python", "js", - "ts" + "ts", + "java" ] }, "module": { @@ -104,8 +105,7 @@ }, "dist": { "type": "string", - "title": "Relative path to service deployment artifacts", - "description": "The CLI will use files under this path to create the deployment artifact (ZIP file). If omitted, all files under service project directory will be included." + "title": "Relative path to service deployment artifacts" }, "docker": { "type": "object", @@ -132,21 +132,48 @@ } } }, - "if": { - "not": { + "required": ["project"], + "allOf": [ + { + "if": { + "not": { + "properties": { + "host": { + "const": "containerapp" + } + } + } + }, + "then": { + "properties": { + "docker": false + } + } + }, + { + "if": { + "properties": { + "language": { "const": "java" } + } + }, + "then": { + "properties": { + "dist": { + "type": "string", + "description": "The CLI will use the JAR file in this directory to create the deployment artifact (ZIP file). If omitted, the CLI will detect the output directory based on the build system in-use." + } + } + } + }, + { "properties": { - "host": { - "const": "containerapp" + "dist": { + "type": "string", + "description": "The CLI will use files under this path to create the deployment artifact (ZIP file). If omitted, all files under service project directory will be included." } } } - }, - "then": { - "properties": { - "docker": false - } - }, - "required": ["project"] + ] } }, "pipeline": {