Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/devcontainers/features/go:1": {
"version": "1.25"
"version": "1.26"
},
"ghcr.io/devcontainers/features/docker-in-docker:2.11.0": {
"version": "latest",
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/light/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/devcontainers/features/go:1": {
"version": "1.25"
"version": "1.26"
},
"ghcr.io/devcontainers/features/docker-in-docker:2.11.0": {
"version": "latest",
Expand Down
20 changes: 5 additions & 15 deletions .github/workflows/cli-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
paths:
- "cli/**"
- ".github/workflows/cli-ci.yml"
paths-ignore:
- "cli/azd/extensions/**"
branches: [main]

# If two events are triggered within a short time in the same PR, cancel the run of the oldest event
Expand All @@ -18,21 +20,9 @@ permissions:

jobs:
azd-lint:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/setup-go@v6
with:
go-version: "^1.25"
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.6
args: -v --timeout 10m0s
working-directory: cli/azd
uses: ./.github/workflows/lint-go.yml
with:
working-directory: cli/azd

cspell-lint:
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "^1.25"
go-version: "^1.26"

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install golangci-lint
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.6.0
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.10.1

- name: Install cspell
run: npm install -g cspell@8.13.1
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ext-registry-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "^1.25"
go-version: "^1.26"
cache-dependency-path: |
cli/azd/go.sum

Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/lint-go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: lint-go

on:
workflow_call:
inputs:
working-directory:
description: "Path to the Go module directory to lint"
required: true
type: string
go-version:
description: "Go version to use"
required: false
type: string
default: "^1.26"
golangci-lint-version:
description: "GolangCI-Lint version to use"
required: false
type: string
default: "v2.10.1"

permissions:
contents: read
pull-requests: write

jobs:
golangci-lint:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/setup-go@v6
with:
go-version: ${{ inputs.go-version }}
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: ${{ inputs.golangci-lint-version }}
args: -v --timeout 10m0s
working-directory: ${{ inputs.working-directory }}
1 change: 1 addition & 0 deletions cli/azd/.golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ linters:
- G302 # TODO: Expect file permissions to be 0600 or less
- G304 # TODO: Potential file inclusion via variable
- G104 # TODO: Errors unhandled
- G117 # Exported struct fields matching secret patterns (false positives on field names)
formatters:
enable:
- gofmt
2 changes: 1 addition & 1 deletion cli/azd/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func (a *myAction) Run(ctx context.Context) (*actions.ActionResult, error) {

### Modern Go

This project uses Go 1.25. Use modern standard library features:
This project uses Go 1.26. Use modern standard library features:

- **`slices`, `maps`, `cmp` packages**: Use for searching, sorting, cloning, and iterating—avoid manual loops
- **Iterators**: Use `range` over functions/iterators (e.g., `maps.Keys()`, `slices.All()`)
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ In general, to make contributions a smooth and easy experience, we encourage the

Prerequisites:

- [Go](https://go.dev/dl/) 1.25
- [Go](https://go.dev/dl/) 1.26

Build:

Expand Down
2 changes: 1 addition & 1 deletion cli/azd/ci-test.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Write-Host "Running unit tests..."
# --test.gocoverdir is currently a "under-the-cover" way to pass the coverage directory to a test binary
# See https://github.com/golang/go/issues/51430#issuecomment-1344711300
#
# As of Go 1.25, it’s still an “under-the-hood” option.
# As of Go 1.26, it’s still an “under-the-hood” option.
& $gotestsum -- ./... -short -v -cover -args --test.gocoverdir="$($unitCoverDir.FullName)"
if ($LASTEXITCODE) {
exit $LASTEXITCODE
Expand Down
2 changes: 2 additions & 0 deletions cli/azd/cmd/middleware/experimentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func (m *ExperimentationMiddleware) Run(ctx context.Context, next NextFn) (*acti
// Allow overriding the assignment endpoint, either for local development (where you want to hit a private instance)
// or testing (we use this in our end to end tests to control assignment behavior for the CLI under test)/
if override := os.Getenv("AZD_DEBUG_EXPERIMENTATION_TAS_ENDPOINT"); override != "" {
//nolint:gosec // G706: env var in debug log
log.Printf("using override assignment endpoint: %s, from AZD_DEBUG_EXPERIMENTATION_TAS_ENDPOINT", override)
endpoint = override
}
Expand All @@ -42,6 +43,7 @@ func (m *ExperimentationMiddleware) Run(ctx context.Context, next NextFn) (*acti
if assignment, err := assignmentManager.Assignment(ctx); err != nil {
log.Printf("failed to get variant assignments: %v", err)
} else {
//nolint:gosec // G706: internal API value in debug log
log.Printf("assignment context: %v", assignment.AssignmentContext)
tracing.SetGlobalAttributes(fields.ExpAssignmentContextKey.String(assignment.AssignmentContext))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,27 +140,27 @@ func DownloadAssetToTemp(assetUrl, assetName string) (string, error) {
// #nosec G107: Potential HTTP request made with variable url
resp, err := http.Get(assetUrl)
if err != nil {
os.Remove(tempFile.Name())
os.Remove(tempFile.Name()) //nolint:gosec // G703: temp file cleanup
return "", fmt.Errorf("failed to download asset: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
os.Remove(tempFile.Name())
os.Remove(tempFile.Name()) //nolint:gosec // G703: temp file cleanup
return "", fmt.Errorf("download returned status %d", resp.StatusCode)
}
reader = resp.Body
} else {
localFile, err := os.Open(assetUrl)
if err != nil {
os.Remove(tempFile.Name())
os.Remove(tempFile.Name()) //nolint:gosec // G703: temp file cleanup
return "", fmt.Errorf("failed to open local file: %w", err)
}
defer localFile.Close()
reader = localFile
}

if _, err := io.Copy(tempFile, reader); err != nil {
os.Remove(tempFile.Name())
os.Remove(tempFile.Name()) //nolint:gosec // G703: temp file cleanup
return "", fmt.Errorf("failed to write to temp file: %w", err)
}
tempFile.Close()
Expand Down Expand Up @@ -377,6 +377,7 @@ func CreateLocalRegistry() error {
return fmt.Errorf("failed to marshal empty registry: %w", err)
}

//nolint:gosec // G703: path from azd config directory
if err := os.WriteFile(localRegistryPath, registryJson, PermissionFile); err != nil {
return fmt.Errorf("failed to create local registry file: %w", err)
}
Expand All @@ -389,7 +390,7 @@ func CreateLocalRegistry() error {
}

/* #nosec G204 - args are hardcoded above, not user-controlled */
createExtSourceCmd := exec.Command("azd", args...)
createExtSourceCmd := exec.Command("azd", args...) //nolint:gosec // G702: args are hardcoded above, not user-controlled
if _, err := createExtSourceCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create local extension source: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/azure/azure-dev/cli/azd

go 1.25
go 1.26

require (
dario.cat/mergo v1.0.2
Expand Down
1 change: 1 addition & 0 deletions cli/azd/internal/cmd/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ func Test_isNetworkError(t *testing.T) {
// 2. Add it to the excludedErrors list below with a comment explaining why.
func Test_PackageLevelErrorsMapped(t *testing.T) {
// Package-level error variables that are intentionally NOT mapped in MapError, with reasons:
//nolint:gosec // G101: map variable name, not credentials
excludedErrors := map[string]string{
// Internal-only errors that never propagate to command-level
"ErrDuplicateRegistration": "internal/mapper: programming error, not a runtime user error",
Expand Down
10 changes: 5 additions & 5 deletions cli/azd/internal/mcp/tools/azd_yaml_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestHandleAzdYamlSchema_ValidYaml(t *testing.T) {
oldWd, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(oldWd)
os.Rename(yamlPath, "azure.yaml")
os.Rename(yamlPath, "azure.yaml") //nolint:gosec // G703: test file rename with controlled paths
defer os.Remove("azure.yaml")

req := mcp.CallToolRequest{}
Expand Down Expand Up @@ -85,7 +85,7 @@ func TestHandleAzdYamlSchema_InvalidYaml(t *testing.T) {
oldWd, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(oldWd)
os.Rename(yamlPath, "azure.yaml")
os.Rename(yamlPath, "azure.yaml") //nolint:gosec // G703: test file rename with controlled paths
defer os.Remove("azure.yaml")

req := mcp.CallToolRequest{}
Expand Down Expand Up @@ -115,7 +115,7 @@ func TestHandleAzdYamlSchema_YamlNotValidSyntax(t *testing.T) {
oldWd, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(oldWd)
os.Rename(yamlPath, "azure.yaml")
os.Rename(yamlPath, "azure.yaml") //nolint:gosec // G703: test file rename with controlled paths
defer os.Remove("azure.yaml")

req := mcp.CallToolRequest{}
Expand Down Expand Up @@ -146,7 +146,7 @@ func TestHandleAzdYamlSchema_YamlValidButSchemaInvalid(t *testing.T) {
oldWd, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(oldWd)
os.Rename(yamlPath, "azure.yaml")
os.Rename(yamlPath, "azure.yaml") //nolint:gosec // G703: test file rename with controlled paths
defer os.Remove("azure.yaml")

req := mcp.CallToolRequest{}
Expand Down Expand Up @@ -176,7 +176,7 @@ func TestHandleAzdYamlSchema_InvalidYaml_Structural(t *testing.T) {
oldWd, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(oldWd)
os.Rename(yamlPath, "azure.yaml")
os.Rename(yamlPath, "azure.yaml") //nolint:gosec // G703: test file rename with controlled paths
defer os.Remove("azure.yaml")

req := mcp.CallToolRequest{}
Expand Down
3 changes: 3 additions & 0 deletions cli/azd/internal/scaffold/resource_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ var Resources = []ResourceMeta{
ResourceType: "Microsoft.Cache/redis",
ApiVersion: "2024-03-01",
StandardVarPrefix: "REDIS",
//nolint:gosec // G101: template variable references, not hardcoded credentials
Variables: map[string]string{
"host": "${.properties.hostName}",
"port": "6380",
Expand Down Expand Up @@ -136,6 +137,7 @@ var Resources = []ResourceMeta{
ApiVersion: "2023-12-30",
StandardVarPrefix: "MYSQL",
ParentForEval: "Microsoft.DBforMySQL/flexibleServers",
//nolint:gosec // G101: template variable references, not hardcoded credentials
Variables: map[string]string{
"database": "${spec.name}",
"host": "${.properties.fullyQualifiedDomainName}",
Expand All @@ -150,6 +152,7 @@ var Resources = []ResourceMeta{
ApiVersion: "2022-12-01",
StandardVarPrefix: "POSTGRES",
ParentForEval: "Microsoft.DBforPostgreSQL/flexibleServers",
//nolint:gosec // G101: template variable references, not hardcoded credentials
Variables: map[string]string{
"database": "${spec.name}",
"host": "${.properties.fullyQualifiedDomainName}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (transmitter *httpTransmitter) Transmit(payload []byte, items TelemetryItem
req.Header.Set("Content-Type", "application/x-json-stream")
req.Header.Set("Accept-Encoding", "gzip, deflate")

resp, err := transmitter.client.Do(req)
resp, err := transmitter.client.Do(req) //nolint:gosec // G704: endpoint is a known telemetry ingestion URL
if err != nil {
diagLog.Printf("Failed to transmit telemetry: %s", err.Error())
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/internal/telemetry/notice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func setupSuite(withFirstRunFile bool, t *testing.T) func(t *testing.T) {
file.Close()

tmpFilename = file.Name()
err = os.Rename(firstRunFilePath, tmpFilename)
err = os.Rename(firstRunFilePath, tmpFilename) //nolint:gosec // G703: test file paths
if err != nil {
t.Fatalf("failed to rename first run file: %v", err)
}
Expand Down
4 changes: 3 additions & 1 deletion cli/azd/internal/telemetry/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func (stg *StorageQueue) save(delayDuration time.Duration, retryCount int, messa
}

tempFileName := file.Name()
//nolint:gosec // G703: tempFileName from os.CreateTemp
err = os.WriteFile(tempFileName, message, osutil.PermissionFile)
if err != nil {
_ = removeIfExists(tempFileName)
Expand All @@ -124,6 +125,7 @@ func (stg *StorageQueue) save(delayDuration time.Duration, retryCount int, messa
randomSuffix := generatedFileName[:strings.LastIndex(generatedFileName, "_")]
readyTime := stg.clock.Now().Add(delayDuration)
fileName := formatFileName(readyTime, retryCount, randomSuffix, stg.itemFileExtension)
//nolint:gosec // G703: paths from os.CreateTemp within known folder
err = os.Rename(tempFileName, filepath.Join(stg.folder, fileName))
if err != nil {
_ = removeIfExists(tempFileName)
Expand Down Expand Up @@ -189,7 +191,7 @@ func (stg *StorageQueue) Remove(item *StoredItem) error {
}

func removeIfExists(filename string) error {
err := os.Remove(filename)
err := os.Remove(filename) //nolint:gosec // G703: internal cleanup
if errors.Is(err, os.ErrNotExist) {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/magefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func Preflight() error {

// Check required tools are installed before running anything.
if err := requireTool("golangci-lint",
"go install github.com/golangci/golangci-lint/cmd/golangci-lint@v2.6"); err != nil {
"go install github.com/golangci/golangci-lint/cmd/golangci-lint@v2.10.1"); err != nil {
return err
}
if err := requireTool("cspell", "npm install -g cspell@8.13.1"); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/pkg/auth/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func (e *AuthFailedError) Error() string {

func (e *AuthFailedError) httpErrorDetails() string {
msg := &bytes.Buffer{}
fmt.Fprintf(msg,
fmt.Fprintf(msg, //nolint:gosec // G705: writing to bytes.Buffer, not an HTTP response
"%s %s://%s%s\n",
e.RawResp.Request.Method,
e.RawResp.Request.URL.Scheme,
Expand Down
3 changes: 3 additions & 0 deletions cli/azd/pkg/config/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func GetUserConfigDir() (string, error) {
configDirPath = filepath.Join(homeDir, ".azd")
}

//nolint:gosec // G703: configDirPath is derived from user home directory
err := os.MkdirAll(configDirPath, osutil.PermissionDirectoryOwnerOnly)
if err != nil {
return configDirPath, err
Expand All @@ -87,6 +88,7 @@ func GetUserConfigDir() (string, error) {
// user. In cases where the config directory is ~/.azd, OS upgrades and
// other processes can remove the "x" permission
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
//nolint:gosec // G703: configDirPath is derived from user home directory
info, err := os.Stat(configDirPath)
if err != nil {
return configDirPath, err
Expand All @@ -95,6 +97,7 @@ func GetUserConfigDir() (string, error) {
permissions := info.Mode().Perm()
if permissions&osutil.PermissionMaskDirectoryExecute == 0 {
// Ensure user execute permissions
//nolint:gosec // G703: configDirPath from user home
err := os.Chmod(configDirPath, permissions|osutil.PermissionMaskDirectoryExecute)
return configDirPath, err
}
Expand Down
Loading
Loading