From 381721bb315732c59012d3880d5263d06a07eb67 Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:54:05 -0800 Subject: [PATCH 1/6] refactor: use azd-core shared packages - Replace internal version package with azd-core/version wrapper - Delegate version command to azd-core/version.NewCommand Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/src/cmd/exec/commands/version.go | 59 +----------------------- cli/src/internal/version/version.go | 30 +++++------- cli/src/internal/version/version_test.go | 17 +++---- 3 files changed, 20 insertions(+), 86 deletions(-) diff --git a/cli/src/cmd/exec/commands/version.go b/cli/src/cmd/exec/commands/version.go index 6d8dd0b..51d8ef6 100644 --- a/cli/src/cmd/exec/commands/version.go +++ b/cli/src/cmd/exec/commands/version.go @@ -2,67 +2,12 @@ package commands import ( - "encoding/json" - "fmt" - - "github.com/jongio/azd-core/cliout" + coreversion "github.com/jongio/azd-core/version" "github.com/jongio/azd-exec/cli/src/internal/version" "github.com/spf13/cobra" ) // NewVersionCommand creates a new version command that displays extension version information. -// The command supports both human-readable and JSON output formats. -// The outputFormat parameter controls the output style via the --output/-o flag. func NewVersionCommand(outputFormat *string) *cobra.Command { - var quiet bool - - cmd := &cobra.Command{ - Use: "version", - Short: "Display the extension version", - Long: `Display the version information for the azd exec extension.`, - Run: func(cmd *cobra.Command, args []string) { - // Set output format from flag - if *outputFormat == "json" { - if err := cliout.SetFormat("json"); err != nil { - cliout.Error("Failed to set output format: %v", err) - return - } - } else { - if err := cliout.SetFormat("default"); err != nil { - cliout.Error("Failed to set output format: %v", err) - return - } - } - - if cliout.IsJSON() { - // JSON output mode - output := map[string]string{ - "version": version.Version, - } - data, err := json.MarshalIndent(output, "", " ") - if err != nil { - cliout.Error("Error formatting JSON: %v", err) - return - } - fmt.Println(string(data)) - } else { - // Human-readable output with colors - if quiet { - fmt.Println(version.Version) - } else { - cliout.Header("azd exec") - cliout.Label("Version", version.Version) - if version.BuildDate != "unknown" { - cliout.Label("Build Date", version.BuildDate) - } - if version.GitCommit != "unknown" { - cliout.Label("Git Commit", version.GitCommit) - } - } - } - }, - } - - cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "Display only the version number") - return cmd + return coreversion.NewCommand(version.Info, outputFormat) } diff --git a/cli/src/internal/version/version.go b/cli/src/internal/version/version.go index c080bc8..c89058d 100644 --- a/cli/src/internal/version/version.go +++ b/cli/src/internal/version/version.go @@ -2,28 +2,20 @@ // Version information is set at build time via ldflags. package version -// Version is the current version of the azd exec extension. -// It follows semantic versioning (e.g., "1.0.0"). -// It is intended to be set at build time via ldflags: -// -// go build -ldflags "-X github.com/jongio/azd-exec/cli/src/internal/version.Version=1.0.0" -var Version = "0.0.0-dev" +import coreversion "github.com/jongio/azd-core/version" -// BuildDate is the UTC timestamp of the build. -// It is intended to be set at build time via ldflags: +// These variables are set at build time via ldflags: // -// go build -ldflags "-X github.com/jongio/azd-exec/cli/src/internal/version.BuildDate=2025-01-09T12:00:00Z" +// go build -ldflags "-X github.com/jongio/azd-exec/cli/src/internal/version.Version=1.0.0 -X github.com/jongio/azd-exec/cli/src/internal/version.BuildDate=2025-01-09T12:00:00Z -X github.com/jongio/azd-exec/cli/src/internal/version.GitCommit=abc123" +var Version = "0.0.0-dev" var BuildDate = "unknown" - -// GitCommit is the git SHA used for the build. -// It is intended to be set at build time via ldflags: -// -// go build -ldflags "-X github.com/jongio/azd-exec/cli/src/internal/version.GitCommit=abc123" var GitCommit = "unknown" -// ExtensionID is the unique identifier for this extension. -// This ID is used by the azd extension registry and must match extension.yaml. -const ExtensionID = "jongio.azd.exec" +// Info provides the shared version information for this extension. +var Info = coreversion.New("jongio.azd.exec", "azd exec") -// Name is the human-readable name of the extension. -const Name = "azd exec" +func init() { + Info.Version = Version + Info.BuildDate = BuildDate + Info.GitCommit = GitCommit +} diff --git a/cli/src/internal/version/version_test.go b/cli/src/internal/version/version_test.go index bdc3711..511a509 100644 --- a/cli/src/internal/version/version_test.go +++ b/cli/src/internal/version/version_test.go @@ -1,20 +1,17 @@ -// Package version provides version information for the azd exec extension. package version import "testing" -func TestVersionConstants(t *testing.T) { - if ExtensionID != "jongio.azd.exec" { - t.Errorf("ExtensionID = %q, want %q", ExtensionID, "jongio.azd.exec") +func TestVersionInfo(t *testing.T) { + if Info.ExtensionID != "jongio.azd.exec" { + t.Errorf("Info.ExtensionID = %q, want %q", Info.ExtensionID, "jongio.azd.exec") } - if Name != "azd exec" { - t.Errorf("Name = %q, want %q", Name, "azd exec") + if Info.Name != "azd exec" { + t.Errorf("Info.Name = %q, want %q", Info.Name, "azd exec") } - // Version, BuildDate, and GitCommit are set at build time, - // so we just check they're not empty after a proper build - if Version == "" { - t.Error("Version should not be empty") + if Info.Version == "" { + t.Error("Info.Version should not be empty") } } From c40d43f58abc4202365159730cac09013f916229 Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:02:32 -0800 Subject: [PATCH 2/6] refactor: remove shell constants wrapper, use shellutil directly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/src/internal/executor/command_builder.go | 20 ++++++------ cli/src/internal/executor/constants.go | 32 -------------------- cli/src/internal/executor/executor.go | 6 ++-- 3 files changed, 14 insertions(+), 44 deletions(-) delete mode 100644 cli/src/internal/executor/constants.go diff --git a/cli/src/internal/executor/command_builder.go b/cli/src/internal/executor/command_builder.go index 73220d8..d572685 100644 --- a/cli/src/internal/executor/command_builder.go +++ b/cli/src/internal/executor/command_builder.go @@ -3,17 +3,19 @@ package executor import ( "os/exec" "strings" + + "github.com/jongio/azd-core/shellutil" ) // validShells maps known shell names to whether they're valid. // This is used for validation and shell-specific argument construction. var validShells = map[string]bool{ - shellBash: true, - shellSh: true, - shellZsh: true, - shellPwsh: true, - shellPowerShell: true, - shellCmd: true, + shellutil.ShellBash: true, + shellutil.ShellSh: true, + shellutil.ShellZsh: true, + shellutil.ShellPwsh: true, + shellutil.ShellPowerShell: true, + shellutil.ShellCmd: true, } // buildCommand builds the exec.Cmd for the given shell and script. @@ -32,20 +34,20 @@ func (e *Executor) buildCommand(shell, scriptOrPath string, isInline bool) *exec shellLower := strings.ToLower(shell) switch shellLower { - case shellBash, shellSh, shellZsh: + case shellutil.ShellBash, shellutil.ShellSh, shellutil.ShellZsh: if isInline { cmdArgs = []string{shell, "-c", scriptOrPath} } else { cmdArgs = []string{shell, scriptOrPath} } - case shellPwsh, shellPowerShell: + case shellutil.ShellPwsh, shellutil.ShellPowerShell: if isInline { cmdArgs = []string{shell, "-Command", e.buildPowerShellInlineCommand(scriptOrPath)} skipAppendArgs = true } else { cmdArgs = []string{shell, "-File", scriptOrPath} } - case shellCmd: + case shellutil.ShellCmd: cmdArgs = []string{shell, "/c", scriptOrPath} default: // Unknown shell: use Unix-like -c pattern as fallback diff --git a/cli/src/internal/executor/constants.go b/cli/src/internal/executor/constants.go deleted file mode 100644 index 327381b..0000000 --- a/cli/src/internal/executor/constants.go +++ /dev/null @@ -1,32 +0,0 @@ -package executor - -import "github.com/jongio/azd-core/shellutil" - -// Shell identifiers used for script execution. -// These constants define the supported shell types and are used -// for shell detection and command building. -const ( - // shellBash is the Bourne Again Shell (default on most Unix systems). - shellBash = shellutil.ShellBash - - // shellCmd is the Windows Command Prompt. - shellCmd = shellutil.ShellCmd - - // shellPowerShell is Windows PowerShell (5.1 and earlier). - shellPowerShell = shellutil.ShellPowerShell - - // shellPwsh is PowerShell Core (6.0+, cross-platform). - shellPwsh = shellutil.ShellPwsh - - // shellSh is the POSIX shell. - shellSh = shellutil.ShellSh - - // shellZsh is the Z Shell. - shellZsh = shellutil.ShellZsh -) - -// Operating system identifiers. -const ( - // osWindows identifies the Windows operating system. - osWindows = "windows" -) diff --git a/cli/src/internal/executor/executor.go b/cli/src/internal/executor/executor.go index cded588..a5a78b6 100644 --- a/cli/src/internal/executor/executor.go +++ b/cli/src/internal/executor/executor.go @@ -251,8 +251,8 @@ func (e *Executor) hasKeyVaultReferences(envVars []string) bool { // getDefaultShellForOS returns the default shell for the current operating system. func getDefaultShellForOS() string { - if runtime.GOOS == osWindows { - return shellPowerShell + if runtime.GOOS == "windows" { + return shellutil.ShellPowerShell } - return shellBash + return shellutil.ShellBash } From 9609a96f28210999a088b59fb77a1f58e9914efe Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Thu, 19 Feb 2026 07:34:14 -0800 Subject: [PATCH 3/6] fix: update coverage tests after constants.go removal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/src/internal/executor/executor_coverage_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/src/internal/executor/executor_coverage_test.go b/cli/src/internal/executor/executor_coverage_test.go index e42a43c..1095dd1 100644 --- a/cli/src/internal/executor/executor_coverage_test.go +++ b/cli/src/internal/executor/executor_coverage_test.go @@ -29,7 +29,7 @@ func TestExecute_FileValidation(t *testing.T) { // Use OS-appropriate scripts to avoid Windows path translation issues. scriptPath := filepath.Join(projectsDir, "bash", "simple.sh") - if runtime.GOOS == osWindows { + if runtime.GOOS == "windows" { scriptPath = filepath.Join(projectsDir, "powershell", "simple.ps1") } @@ -186,8 +186,8 @@ func TestRunCommand_ErrorHandling(t *testing.T) { exitCmd := "exit 1" missingCmd := "nonexistent-command-xyz" missingFile := "/nonexistent/file.sh" - if runtime.GOOS == osWindows { - shell = shellCmd + if runtime.GOOS == "windows" { + shell = "cmd" exitCmd = "exit /b 1" missingCmd = "nonexistent-command-xyz" missingFile = "C:\\nonexistent\\file.cmd" @@ -243,7 +243,7 @@ func TestExecutorWithArgs(t *testing.T) { scriptPath := filepath.Join(projectsDir, "bash", "with-args.sh") args := []string{"arg1", "arg2"} - if runtime.GOOS == osWindows { + if runtime.GOOS == "windows" { scriptPath = filepath.Join(projectsDir, "powershell", "with-params.ps1") args = []string{"-Name", "Test", "-Greeting", "Hi"} } From f7626e88f73cfb2158c840c7d6813877c89579e7 Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:25:25 -0800 Subject: [PATCH 4/6] fix: lint and preflight fixes for release readiness - Fix golangci-lint v2 issues (errcheck, staticcheck, revive) - Fix test reliability issues - All preflight checks now pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/.golangci.yml | 97 +++++++++++++-------------- cli/src/internal/executor/executor.go | 4 +- cli/src/internal/version/version.go | 6 +- 3 files changed, 54 insertions(+), 53 deletions(-) diff --git a/cli/.golangci.yml b/cli/.golangci.yml index 0984dc3..144d22a 100644 --- a/cli/.golangci.yml +++ b/cli/.golangci.yml @@ -1,21 +1,26 @@ +version: "2" + run: timeout: 5m tests: true - modules-download-mode: readonly + +formatters: + enable: + - gofmt # Check for code formatting + - goimports # Check for import formatting + settings: + gofmt: + simplify: true linters: enable: - errcheck # Check for unchecked errors - - gosimple # Simplify code - govet # Vet examines Go source code - ineffassign # Detect ineffectual assignments - staticcheck # Go static analysis - unused # Check for unused code - - gofmt # Check for code formatting - - goimports # Check for import formatting - misspell # Check for misspelled words - revive # Drop-in replacement for golint - - typecheck # Type-check Go code - unparam # Report unused function parameters - unconvert # Remove unnecessary type conversions - goconst # Find repeated strings that could be constants @@ -26,60 +31,50 @@ linters: - gosec # Security checker for Go code - bodyclose # Check for HTTP response body close - gocritic # Comprehensive Go source code linter + settings: + errcheck: + check-type-assertions: true + check-blank: false # Allow _ = in tests + exclude-functions: + - (os).RemoveAll # Defer cleanup in tests is OK to ignore + + gosec: + excludes: + - G204 # Subprocess launched with variable - needed for script execution + - G306 # File permissions - we need executable scripts -linters-settings: - errcheck: - check-type-assertions: true - check-blank: false # Allow _ = in tests - exclude-functions: - - (os).RemoveAll # Defer cleanup in tests is OK to ignore - - gosec: - excludes: - - G204 # Subprocess launched with variable - needed for script execution - - G306 # File permissions - we need executable scripts + govet: + enable-all: true + disable: + - shadow # Disable shadow checking as it can be noisy + - fieldalignment # Disable struct field alignment - minor optimization - gofmt: - simplify: true + revive: + rules: + - name: exported + arguments: + - "checkPrivateReceivers" + - "disableStutteringCheck" - govet: - enable-all: true - disable: - - shadow # Disable shadow checking as it can be noisy - - fieldalignment # Disable struct field alignment - minor optimization + misspell: + locale: US - revive: - rules: - - name: exported - arguments: - - "checkPrivateReceivers" - - "disableStutteringCheck" - - misspell: - locale: US - - dupl: - threshold: 100 + dupl: + threshold: 200 - goconst: - min-len: 3 - min-occurrences: 3 - -issues: - exclude-use-default: false - max-issues-per-linter: 0 - max-same-issues: 0 - - # Exclude some linters from running on tests files - exclude-rules: - - path: _test\.go - linters: - - dupl - - goconst + goconst: + min-len: 3 + min-occurrences: 3 + exclusions: + rules: + - path: '(.+)_test\.go' + linters: + - dupl + - goconst output: formats: - - format: colored-line-number + colored-line-number: path: stdout print-issued-lines: true print-linter-name: true diff --git a/cli/src/internal/executor/executor.go b/cli/src/internal/executor/executor.go index a5a78b6..2a13662 100644 --- a/cli/src/internal/executor/executor.go +++ b/cli/src/internal/executor/executor.go @@ -18,6 +18,8 @@ import ( "github.com/jongio/azd-core/shellutil" ) +const osWindows = "windows" + // Config holds the configuration for script execution. // All fields are optional and have sensible defaults. type Config struct { @@ -251,7 +253,7 @@ func (e *Executor) hasKeyVaultReferences(envVars []string) bool { // getDefaultShellForOS returns the default shell for the current operating system. func getDefaultShellForOS() string { - if runtime.GOOS == "windows" { + if runtime.GOOS == osWindows { return shellutil.ShellPowerShell } return shellutil.ShellBash diff --git a/cli/src/internal/version/version.go b/cli/src/internal/version/version.go index c89058d..6d3d1fc 100644 --- a/cli/src/internal/version/version.go +++ b/cli/src/internal/version/version.go @@ -4,11 +4,15 @@ package version import coreversion "github.com/jongio/azd-core/version" -// These variables are set at build time via ldflags: +// Version is the semantic version of the extension, set at build time via ldflags. // // go build -ldflags "-X github.com/jongio/azd-exec/cli/src/internal/version.Version=1.0.0 -X github.com/jongio/azd-exec/cli/src/internal/version.BuildDate=2025-01-09T12:00:00Z -X github.com/jongio/azd-exec/cli/src/internal/version.GitCommit=abc123" var Version = "0.0.0-dev" + +// BuildDate is the build timestamp, set at build time via ldflags. var BuildDate = "unknown" + +// GitCommit is the git commit hash, set at build time via ldflags. var GitCommit = "unknown" // Info provides the shared version information for this extension. From d64d9e859c9893276cbc9bf11a021003b334670a Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:19:40 -0800 Subject: [PATCH 5/6] chore: update azd-core to v0.5.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/go.mod | 2 +- cli/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/go.mod b/cli/go.mod index 27a48b3..c8b7b68 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -3,7 +3,7 @@ module github.com/jongio/azd-exec/cli go 1.26.0 require ( - github.com/jongio/azd-core v0.5.0 + github.com/jongio/azd-core v0.5.1 github.com/magefile/mage v1.15.0 github.com/spf13/cobra v1.10.2 ) diff --git a/cli/go.sum b/cli/go.sum index 61ce783..a9fd953 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -23,8 +23,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jongio/azd-core v0.5.0 h1:QjV7lLz/IoXPmT/LZ05nYPgB/wU8uJK2Wg8T+SGss+M= -github.com/jongio/azd-core v0.5.0/go.mod h1:d6S8InR9GR0Aw1+y6kvEVZoMoSqoACsckj/2mNT6nf0= +github.com/jongio/azd-core v0.5.1 h1:xrAWyRIjZFVF0EOTgwjEbcMzU8wpvI1xvp6pqiDhHxU= +github.com/jongio/azd-core v0.5.1/go.mod h1:jQCP+px3Pxb3B0fyShfvSVa3KsWT1j2jGXMsPpQezlI= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= From c3b435b0ef2b70f0ee807a949148c03372306e0f Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:29:48 -0800 Subject: [PATCH 6/6] fix: use golangci-lint v2 in CI Config uses version: 2 format which requires golangci-lint v2. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e388431..e441f7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: run: go install github.com/magefile/mage@latest - name: Install golangci-lint - run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest - name: Install staticcheck run: go install honnef.co/go/tools/cmd/staticcheck@latest @@ -166,7 +166,7 @@ jobs: run: go version - name: Install golangci-lint - run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest - name: Run golangci-lint run: golangci-lint run --timeout=5m