@@ -22,7 +22,7 @@ const totalTime = getTotalEstimatedTime();
🎯
Guided Tour
- Learn azd-app step by step. This comprehensive tour covers installation,
+ Learn azd-app step by step. This tour covers installation,
running apps, using the dashboard, viewing logs, monitoring health,
and integrating with GitHub Copilot.
From e8c1e38efe3e17c3eee507576654520d83a70bde Mon Sep 17 00:00:00 2001
From: Jon Gallant <2163001+jongio@users.noreply.github.com>
Date: Sat, 14 Mar 2026 14:45:32 -0700
Subject: [PATCH 3/3] feat: dispatch-parity quality improvements
- Pin all GitHub Actions to full commit SHAs
- Add CODEOWNERS file
- Add Dependabot for go modules and github-actions
- Add concurrency control to CI/PR workflows
- Add CodeQL security scanning workflow
- Add govulncheck vulnerability scanning workflow
- Standardize golangci-lint config with 30+ linters
- Add dispatch-level linters (errname, exhaustive, forcetypeassert, etc.)
- Add gofumpt strict formatting checks
- Add deadcode detection
- Add cosign code signing to release workflow
- Add SBOM generation (SPDX + CycloneDX) to release workflow
- Add comprehensive README badges (CI, CodeQL, Go Report Card, etc.)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.github/CODEOWNERS | 2 +
.github/dependabot.yml | 14 ++
.github/workflows/ci.yml | 52 +++----
.github/workflows/codeql.yml | 57 ++++++++
.github/workflows/govulncheck.yml | 38 +++++
.github/workflows/pr-build.yml | 18 +--
.github/workflows/release.yml | 97 ++++++++----
.github/workflows/sync-demo-template.yml | 6 +-
.github/workflows/update-azd-core.yml | 4 +-
.github/workflows/website.yml | 24 +--
.golangci.yml | 138 ++++++++++++------
README.md | 6 +
cli/go.mod | 2 +-
cli/magefile.go | 69 +++++++++
cli/src/cmd/app/commands/add.go | 6 +-
cli/src/cmd/app/commands/core_deps.go | 12 +-
cli/src/cmd/app/commands/core_helpers.go | 5 +-
cli/src/cmd/app/commands/deps.go | 1 +
cli/src/cmd/app/commands/generate.go | 71 +++++----
cli/src/cmd/app/commands/health.go | 12 +-
cli/src/cmd/app/commands/health_e2e_test.go | 2 +-
cli/src/cmd/app/commands/info.go | 33 +++--
cli/src/cmd/app/commands/logs.go | 70 ++++-----
.../cmd/app/commands/logs_executor_test.go | 6 +-
cli/src/cmd/app/commands/mcp.go | 10 +-
cli/src/cmd/app/commands/mcp_resources.go | 4 +-
cli/src/cmd/app/commands/mcp_tools.go | 13 +-
cli/src/cmd/app/commands/notifications.go | 4 +-
cli/src/cmd/app/commands/reqs.go | 68 +++++----
.../app/commands/reqs_fix_integration_test.go | 13 +-
cli/src/cmd/app/commands/restart.go | 2 +-
cli/src/cmd/app/commands/run.go | 6 +-
cli/src/cmd/app/commands/service_control.go | 12 +-
cli/src/cmd/app/commands/stop.go | 2 +-
cli/src/internal/azure/bicep.go | 2 +-
cli/src/internal/azure/client_pool_test.go | 4 +-
cli/src/internal/azure/diagnostic_engine.go | 2 +
cli/src/internal/azure/diagnostics.go | 1 +
cli/src/internal/azure/discovery.go | 3 +-
cli/src/internal/azure/loganalytics.go | 3 +-
.../azure/loganalytics_integration_test.go | 2 +-
cli/src/internal/azure/parse_results_test.go | 6 +-
cli/src/internal/azure/query_builder.go | 22 +--
cli/src/internal/azure/query_builder_test.go | 29 ++--
cli/src/internal/azure/realtime.go | 7 +-
cli/src/internal/azure/realtime_test.go | 2 +-
cli/src/internal/azure/standalone_logs.go | 45 +++---
cli/src/internal/azure/token_cache.go | 8 +-
.../internal/azure/validator_appservice.go | 6 +-
.../internal/azure/validator_containerapp.go | 6 +-
cli/src/internal/azure/validator_function.go | 6 +-
cli/src/internal/azure/verification.go | 2 +
cli/src/internal/config/notifications.go | 22 ++-
.../internal/dashboard/azure_logs_handlers.go | 2 +-
.../internal/dashboard/azure_logs_health.go | 40 ++---
.../internal/dashboard/azure_logs_query.go | 11 +-
.../internal/dashboard/azure_logs_tables.go | 18 ++-
cli/src/internal/dashboard/azure_setup.go | 6 +-
cli/src/internal/dashboard/classifications.go | 2 +-
cli/src/internal/dashboard/client.go | 18 ++-
cli/src/internal/dashboard/constants.go | 2 +-
cli/src/internal/dashboard/health_stream.go | 1 +
cli/src/internal/dashboard/logs_config.go | 2 +-
cli/src/internal/dashboard/mode.go | 2 +-
cli/src/internal/dashboard/server_handlers.go | 11 +-
.../dashboard/websocket_concurrency_test.go | 2 +-
.../dashboard/websocket_fixes_test.go | 2 +-
cli/src/internal/detector/detector_dotnet.go | 8 +-
.../internal/detector/detector_functions.go | 26 ++--
cli/src/internal/detector/detector_node.go | 12 +-
cli/src/internal/detector/detector_python.go | 2 +-
cli/src/internal/docker/exec.go | 22 +--
cli/src/internal/executor/executor.go | 10 +-
cli/src/internal/healthcheck/checker.go | 28 ++--
cli/src/internal/healthcheck/metrics_test.go | 3 +-
cli/src/internal/healthcheck/monitor.go | 60 +++++---
cli/src/internal/healthcheck/types.go | 20 ++-
.../installer/error_formatting_test.go | 7 +-
cli/src/internal/installer/installer.go | 34 +++--
cli/src/internal/installer/installer_test.go | 3 +-
cli/src/internal/logging/logger.go | 3 +-
cli/src/internal/monitor/state_monitor.go | 1 +
.../internal/monitor/state_monitor_test.go | 4 +-
cli/src/internal/notifications/database.go | 6 +-
cli/src/internal/notifications/integration.go | 8 +-
cli/src/internal/notifications/pipeline.go | 11 +-
cli/src/internal/portmanager/allocation.go | 7 +-
cli/src/internal/portmanager/portmanager.go | 12 +-
cli/src/internal/portmanager/testutil_test.go | 7 +-
cli/src/internal/runner/aspire_test.go | 2 +-
cli/src/internal/runner/runner.go | 11 +-
cli/src/internal/service/azure_logs_config.go | 4 +-
cli/src/internal/service/config.go | 2 +-
cli/src/internal/service/constants.go | 4 +-
.../service/container_integration_test.go | 6 +-
cli/src/internal/service/detector.go | 30 ++--
cli/src/internal/service/detector_commands.go | 29 ++--
.../internal/service/detector_frameworks.go | 66 +++++----
cli/src/internal/service/detector_helpers.go | 2 +-
cli/src/internal/service/executor.go | 6 +-
cli/src/internal/service/functions.go | 2 +-
cli/src/internal/service/health.go | 23 ++-
cli/src/internal/service/health_test.go | 5 +-
cli/src/internal/service/health_windows.go | 2 +-
cli/src/internal/service/logbuffer.go | 2 +
cli/src/internal/service/logmanager.go | 2 +
cli/src/internal/service/logmanager_test.go | 2 +-
cli/src/internal/service/operation_manager.go | 1 +
cli/src/internal/service/port.go | 7 +-
cli/src/internal/service/types.go | 3 +
.../internal/service/venv_integration_test.go | 11 +-
.../internal/serviceinfo/environment_test.go | 4 +-
cli/src/internal/serviceinfo/serviceinfo.go | 2 +-
cli/src/internal/testing/config_writer.go | 6 +-
cli/src/internal/testing/coverage.go | 20 +--
cli/src/internal/testing/detection.go | 118 ++++++++-------
cli/src/internal/testing/dotnet_runner.go | 32 ++--
cli/src/internal/testing/go_runner.go | 16 +-
cli/src/internal/testing/node_runner.go | 25 ++--
cli/src/internal/testing/orchestrator.go | 73 +++++----
cli/src/internal/testing/python_runner.go | 56 +++----
cli/src/internal/testing/reporter.go | 12 +-
.../internal/testing/testutil/bindloopback.go | 13 +-
.../testing/testutil/bindloopback_test.go | 4 +-
cli/src/internal/testing/validation.go | 48 +++---
cli/src/internal/testing/watcher.go | 10 +-
web/src/pages/reference/changelog/index.astro | 101 ++++++++++++-
web/src/pages/reference/cli/health.astro | 4 +-
web/src/pages/reference/cli/index.astro | 2 +-
web/src/pages/reference/whats-new/index.astro | 62 ++++++--
130 files changed, 1474 insertions(+), 826 deletions(-)
create mode 100644 .github/CODEOWNERS
create mode 100644 .github/dependabot.yml
create mode 100644 .github/workflows/codeql.yml
create mode 100644 .github/workflows/govulncheck.yml
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..d023a0d94
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,2 @@
+# Default code owners for all files
+* @jongio
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..bcce2bece
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+version: 2
+updates:
+ - package-ecosystem: "gomod"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ commit-message:
+ prefix: "deps"
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ commit-message:
+ prefix: "ci"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b827f2e28..fa5cc7313 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,7 +9,7 @@ on:
workflow_dispatch:
concurrency:
- group: ci-${{ github.ref }}
+ group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
@@ -27,28 +27,28 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '${{ env.GO_VERSION }}'
cache: true
cache-dependency-path: cli/go.sum
- name: Cache Go tools
- uses: actions/cache@v4
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: ~/go/bin
key: go-tools-${{ runner.os }}-${{ env.GO_VERSION }}
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@5b4374b04084dc1f9032b52464284b769ac5059e # v4
with:
version: 9
@@ -85,22 +85,22 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '${{ env.GO_VERSION }}'
cache: true
cache-dependency-path: cli/go.sum
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@5b4374b04084dc1f9032b52464284b769ac5059e # v4
with:
version: 9
@@ -147,7 +147,7 @@ jobs:
- name: Upload coverage to Codecov
if: github.repository == 'jongio/azd-app'
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
with:
file: coverage/coverage.out
flags: unittests
@@ -176,22 +176,22 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '${{ env.GO_VERSION }}'
cache: true
cache-dependency-path: cli/go.sum
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@5b4374b04084dc1f9032b52464284b769ac5059e # v4
with:
version: 9
@@ -220,22 +220,22 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '${{ env.GO_VERSION }}'
cache: true
cache-dependency-path: cli/go.sum
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@5b4374b04084dc1f9032b52464284b769ac5059e # v4
with:
version: 9
@@ -254,7 +254,7 @@ jobs:
GOOS=darwin GOARCH=arm64 go build -o bin/darwin-arm64/app ./src/cmd/app
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: binaries
path: cli/bin/
@@ -271,27 +271,27 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '${{ env.GO_VERSION }}'
cache: true
cache-dependency-path: cli/go.sum
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@5b4374b04084dc1f9032b52464284b769ac5059e # v4
with:
version: 9
- name: Set up Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.11'
@@ -361,7 +361,7 @@ jobs:
- name: Upload test logs on failure
if: failure()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: integration-test-logs-${{ matrix.os }}
path: |
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 000000000..0de7012ca
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,57 @@
+name: CodeQL
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ schedule:
+ - cron: '0 0 * * 0' # Weekly on Sundays at midnight UTC
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ security-events: write
+ actions: read
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ security-events: write
+ actions: read
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+
+ - name: Set up Go
+ uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
+ with:
+ go-version-file: cli/go.mod
+ cache-dependency-path: cli/go.sum
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@dd677812177e0c29f9c970a6c58d8607ae1bfefd # v4
+ with:
+ languages: go
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@dd677812177e0c29f9c970a6c58d8607ae1bfefd # v4
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@dd677812177e0c29f9c970a6c58d8607ae1bfefd # v4
+ continue-on-error: true
+ with:
+ upload: false
+
+ - name: Upload SARIF (if Code Scanning enabled)
+ uses: github/codeql-action/upload-sarif@dd677812177e0c29f9c970a6c58d8607ae1bfefd # v4
+ continue-on-error: true
+ with:
+ sarif_file: ../results
diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml
new file mode 100644
index 000000000..dabad49b5
--- /dev/null
+++ b/.github/workflows/govulncheck.yml
@@ -0,0 +1,38 @@
+name: Go Vulnerability Check
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ schedule:
+ - cron: '0 0 * * 0' # Weekly on Sundays at midnight UTC
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ govulncheck:
+ name: Run govulncheck
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+
+ - name: Set up Go
+ uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
+ with:
+ go-version-file: cli/go.mod
+ cache-dependency-path: cli/go.sum
+
+ - name: Install govulncheck
+ run: go install golang.org/x/vuln/cmd/govulncheck@latest
+
+ - name: Run govulncheck
+ working-directory: cli
+ run: govulncheck ./...
diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml
index e009d1ee1..5946e6db3 100644
--- a/.github/workflows/pr-build.yml
+++ b/.github/workflows/pr-build.yml
@@ -22,7 +22,7 @@ on:
type: number
concurrency:
- group: pr-build-${{ github.event.pull_request.number || github.ref }}
+ group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
@@ -57,7 +57,7 @@ jobs:
steps:
- name: Check if build is allowed
id: check
- uses: actions/github-script@v7
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
let allowed = false;
@@ -168,7 +168,7 @@ jobs:
steps:
- name: Get PR details
id: pr
- uses: actions/github-script@v7
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
let prNumber = '${{ needs.check-permission.outputs.pr_number }}';
@@ -203,24 +203,24 @@ jobs:
core.setOutput('title', pr.data.title);
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ steps.pr.outputs.sha }}
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '${{ env.GO_VERSION }}'
cache: true
cache-dependency-path: cli/go.sum
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@5b4374b04084dc1f9032b52464284b769ac5059e # v4
with:
version: 9
@@ -248,7 +248,7 @@ jobs:
azd extension install microsoft.azd.extensions --source azd
- name: Setup pnpm cache
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('cli/dashboard/pnpm-lock.yaml') }}
@@ -383,7 +383,7 @@ jobs:
EOF
- name: Comment on PR
- uses: actions/github-script@v7
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const fs = require('fs');
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f161e604a..8b7cc8098 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -32,17 +32,17 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '${{ env.GO_VERSION }}'
cache: true
cache-dependency-path: cli/go.sum
- name: Cache Go tools
- uses: actions/cache@v4
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: ~/go/bin
key: go-tools-${{ runner.os }}-${{ env.GO_VERSION }}
@@ -51,13 +51,13 @@ jobs:
run: go version
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
registry-url: 'https://npm.pkg.github.com'
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@5b4374b04084dc1f9032b52464284b769ac5059e # v4
with:
version: 9
@@ -100,22 +100,22 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '${{ env.GO_VERSION }}'
cache: true
cache-dependency-path: cli/go.sum
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@5b4374b04084dc1f9032b52464284b769ac5059e # v4
with:
version: 9
@@ -162,7 +162,7 @@ jobs:
- name: Upload coverage to Codecov
if: github.repository == 'jongio/azd-app'
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
with:
file: coverage/coverage.out
flags: unittests
@@ -171,34 +171,20 @@ jobs:
fail_ci_if_error: false
verbose: true
- build:
- name: Build
- runs-on: ubuntu-latest
- needs: [preflight, test]
- timeout-minutes: 15
-
- defaults:
- run:
- working-directory: cli
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '${{ env.GO_VERSION }}'
cache: true
cache-dependency-path: cli/go.sum
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@5b4374b04084dc1f9032b52464284b769ac5059e # v4
with:
version: 9
@@ -217,7 +203,7 @@ jobs:
GOOS=darwin GOARCH=arm64 go build -o bin/darwin-arm64/app ./src/cmd/app
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: binaries
path: cli/bin/
@@ -230,27 +216,28 @@ jobs:
permissions:
contents: write
pull-requests: write
+ id-token: write # Required for cosign OIDC signing
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '${{ env.GO_VERSION }}'
cache: true
cache-dependency-path: cli/go.sum
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@5b4374b04084dc1f9032b52464284b769ac5059e # v4
with:
version: 9
@@ -506,6 +493,52 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Install cosign
+ uses: sigstore/cosign-installer@3454372be43e8ddeec39df0f73bb47954da3a1f1 # v3
+
+ - name: Install syft
+ uses: anchore/sbom-action/download-syft@e11c554f704a0b820cbf8c51673f6945e0731532 # v0
+
+ - name: Sign release artifacts with cosign
+ working-directory: cli
+ run: |
+ VERSION="${{ steps.version.outputs.next }}"
+ echo "Signing release artifacts for v${VERSION}..."
+
+ # Download release assets
+ gh release download "v${VERSION}" --dir /tmp/release-assets --repo "${{ github.repository }}" || true
+
+ # Sign each artifact
+ if [ -d /tmp/release-assets ]; then
+ for file in /tmp/release-assets/*; do
+ if [ -f "$file" ]; then
+ echo "Signing $(basename $file)..."
+ cosign sign-blob --yes "$file" --output-signature "${file}.sig" --output-certificate "${file}.pem"
+ fi
+ done
+
+ # Upload signatures and certificates to the release
+ gh release upload "v${VERSION}" /tmp/release-assets/*.sig /tmp/release-assets/*.pem --repo "${{ github.repository }}" || true
+ echo "✅ All artifacts signed with cosign (keyless/OIDC)"
+ fi
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Generate SBOM
+ working-directory: cli
+ run: |
+ VERSION="${{ steps.version.outputs.next }}"
+ echo "Generating SBOM for v${VERSION}..."
+
+ # Generate SBOM for the Go module
+ syft . -o spdx-json=/tmp/sbom-spdx.json -o cyclonedx-json=/tmp/sbom-cyclonedx.json
+
+ # Upload SBOM to the release
+ gh release upload "v${VERSION}" /tmp/sbom-spdx.json /tmp/sbom-cyclonedx.json --repo "${{ github.repository }}" || true
+ echo "✅ SBOM generated and uploaded (SPDX + CycloneDX)"
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
- name: Update registry
working-directory: cli
run: |
diff --git a/.github/workflows/sync-demo-template.yml b/.github/workflows/sync-demo-template.yml
index 7af13f459..cf2172f00 100644
--- a/.github/workflows/sync-demo-template.yml
+++ b/.github/workflows/sync-demo-template.yml
@@ -127,7 +127,7 @@ jobs:
- name: Checkout source repository
if: env.secret_configured == 'true'
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
path: source
@@ -175,7 +175,7 @@ jobs:
if: env.secret_configured == 'true'
id: checkout-target
continue-on-error: true
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
repository: ${{ env.TARGET_REPO }}
token: ${{ secrets.DEMO_REPO_PAT }}
@@ -277,7 +277,7 @@ jobs:
steps:
- name: Checkout synced demo repository
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
repository: ${{ env.TARGET_REPO }}
token: ${{ secrets.DEMO_REPO_PAT }}
diff --git a/.github/workflows/update-azd-core.yml b/.github/workflows/update-azd-core.yml
index e3f039840..03ab5d583 100644
--- a/.github/workflows/update-azd-core.yml
+++ b/.github/workflows/update-azd-core.yml
@@ -63,7 +63,7 @@ jobs:
working-directory: .
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ steps.inputs.outputs.branch || 'main' }}
fetch-depth: 0
@@ -83,7 +83,7 @@ jobs:
working-directory: .
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '${{ env.GO_VERSION }}'
cache: true
diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml
index f44c2f9c6..97ed2c69d 100644
--- a/.github/workflows/website.yml
+++ b/.github/workflows/website.yml
@@ -33,15 +33,15 @@ jobs:
timeout-minutes: 15
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@5b4374b04084dc1f9032b52464284b769ac5059e # v4
with:
version: 9
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'pnpm'
@@ -72,7 +72,7 @@ jobs:
- name: Upload production artifact
if: github.event_name != 'pull_request'
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: production-build
path: web/dist
@@ -80,7 +80,7 @@ jobs:
- name: Upload PR preview artifact
if: github.event_name == 'pull_request'
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: pr-preview-${{ github.event.pull_request.number }}
path: web/dist
@@ -93,13 +93,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout gh-pages
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: gh-pages
fetch-depth: 0
- name: Download production artifact
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: production-build
path: _new_build
@@ -131,13 +131,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: gh-pages
fetch-depth: 0
- name: Download PR preview artifact
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: pr-preview-${{ github.event.pull_request.number }}
path: pr/${{ github.event.pull_request.number }}
@@ -160,7 +160,7 @@ jobs:
git push origin gh-pages
- name: Comment PR with preview URL
- uses: actions/github-script@v7
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const prNumber = context.payload.pull_request.number;
@@ -210,7 +210,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout gh-pages
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: gh-pages
fetch-depth: 0
@@ -233,7 +233,7 @@ jobs:
fi
- name: Update PR comment
- uses: actions/github-script@v7
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const prNumber = context.payload.pull_request.number;
diff --git a/.golangci.yml b/.golangci.yml
index 28b6c76a4..57c983a4a 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -5,78 +5,130 @@ run:
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
- dupl # Code clone detection
- - godot # Check for proper comment punctuation
- - goprintffuncname # Check printf-like function names
- nolintlint # Ensure nolint directives are properly formatted
- gosec # Security checker for Go code
- bodyclose # Check for HTTP response body close
- gocritic # Comprehensive Go source code linter
+ # Dispatch-level linters
+ - errname # Check error type naming conventions
+ - exhaustive # Check exhaustiveness of enum switch statements
+ - forcetypeassert # Find forced type assertions
+ - makezero # Find slice declarations not initialized with zero length
+ - nilerr # Find code returning nil error incorrectly
+ - noctx # Find HTTP requests without context
+ - prealloc # Find slice pre-allocation opportunities
+ - predeclared # Find shadowed predeclared identifiers
+ - tparallel # Detect inappropriate t.Parallel() usage
+ - wastedassign # Find wasted assignments
-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
+ settings:
+ errcheck:
+ check-type-assertions: true
+ check-blank: false
+ exclude-functions:
+ - (os).RemoveAll
+ - (os).Remove
+ - fmt.Print
+ - fmt.Println
+ - fmt.Printf
+ - fmt.Fprint
+ - fmt.Fprintln
+ - fmt.Fprintf
+ - (*net/http.Response.Body).Close
+ - (io.Closer).Close
- gofmt:
- simplify: true
+ govet:
+ disable:
+ - fieldalignment
- govet:
- enable-all: true
- disable:
- - shadow # Disable shadow checking as it can be noisy
- - fieldalignment # Disable struct field alignment - minor optimization
+ revive:
+ rules:
+ - name: exported
+ arguments:
+ - "checkPrivateReceivers"
+ - "disableStutteringCheck"
- revive:
- rules:
- - name: exported
- arguments:
- - "checkPrivateReceivers"
- - "disableStutteringCheck"
+ misspell:
+ locale: US
+
+ dupl:
+ threshold: 200
- misspell:
- locale: US
+ goconst:
+ min-len: 3
+ min-occurrences: 4
- dupl:
- threshold: 100
+ exhaustive:
+ default-signifies-exhaustive: true
- goconst:
- min-len: 3
- min-occurrences: 3
+ gocritic:
+ disabled-checks:
+ - ifElseChain
+ - appendAssign
+ - elseif
+
+ exclusions:
+ rules:
+ - path: _test\.go
+ linters:
+ - dupl
+ - errcheck
+ - goconst
+ - unparam
+ - forcetypeassert
+ - noctx
+ - exhaustive
+ - prealloc
+ - gosec
+ - revive
+ - misspell
+ - bodyclose
+ - nilerr
+ - gocritic
+ - nolintlint
+ - wastedassign
+ - staticcheck
+ - path: magefile\.go
+ linters:
+ - unparam
+ - errcheck
+ # CLI tool legitimately uses exec.Command with variable args
+ - path: (cmd|internal)/
+ linters:
+ - gosec
+ text: "G204:"
+ # CLI tool needs to read files with variable paths
+ - path: internal/
+ linters:
+ - gosec
+ text: "G304:"
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
-
output:
formats:
- - format: colored-line-number
+ text:
path: stdout
- print-issued-lines: true
- print-linter-name: true
diff --git a/README.md b/README.md
index 613b53626..4ae6d26c5 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,12 @@ One command starts all services, manages dependencies, and provides real-time mo
[](https://codecov.io/gh/jongio/azd-app)
[](https://goreportcard.com/report/github.com/jongio/azd-app/cli)
[](https://opensource.org/licenses/MIT)
+[](https://github.com/jongio/azd-app/actions/workflows/codeql.yml)
+[](https://pkg.go.dev/github.com/jongio/azd-app/cli)
+[](https://github.com/jongio/azd-app/actions/workflows/govulncheck.yml)
+[](https://github.com/jongio/azd-app/actions/workflows/ci.yml)
+[](https://go.dev/)
+[](https://github.com/jongio/azd-app)
diff --git a/cli/go.mod b/cli/go.mod
index c252ee424..0f539116e 100644
--- a/cli/go.mod
+++ b/cli/go.mod
@@ -1,6 +1,6 @@
module github.com/jongio/azd-app/cli
-go 1.26.0
+go 1.26.1
// Local development: use go.work to resolve azd-core, no local replace
// CI: azd-core is pinned to a tagged version in go.mod
diff --git a/cli/magefile.go b/cli/magefile.go
index 94976c8a1..c12a80c9c 100644
--- a/cli/magefile.go
+++ b/cli/magefile.go
@@ -1185,6 +1185,52 @@ func fmtCheck() error {
return nil
}
+// preflightGofumpt checks that all Go files are formatted with gofumpt (stricter than gofmt).
+func preflightGofumpt() error {
+ if _, err := exec.LookPath("gofumpt"); err != nil {
+ fmt.Println(" ⚠️ gofumpt not installed — skipping strict format check")
+ fmt.Println(" Install with: go install mvdan.cc/gofumpt@latest")
+ return nil
+ }
+ output, err := sh.Output("gofumpt", "-l", ".")
+ if err != nil {
+ return fmt.Errorf("gofumpt check failed: %w", err)
+ }
+ if strings.TrimSpace(output) != "" {
+ fmt.Println(" Files not formatted with gofumpt:")
+ for _, f := range strings.Split(strings.TrimSpace(output), "\n") {
+ fmt.Printf(" • %s\n", f)
+ }
+ return fmt.Errorf("code is not gofumpt-formatted. Run 'gofumpt -w .' to fix")
+ }
+ fmt.Println(" ✅ Code is gofumpt-formatted")
+ return nil
+}
+
+// preflightDeadcode checks for unreachable functions using golang.org/x/tools deadcode analyzer.
+func preflightDeadcode() error {
+ if _, err := exec.LookPath("deadcode"); err != nil {
+ fmt.Println(" ⚠️ deadcode not installed — skipping dead code check")
+ fmt.Println(" Install with: go install golang.org/x/tools/cmd/deadcode@latest")
+ return nil
+ }
+ output, err := sh.Output("deadcode", "-test", "./...")
+ if err != nil {
+ fmt.Println(" ⚠️ Dead code found:")
+ fmt.Println(output)
+ // Non-fatal for now — report but don't fail
+ fmt.Println(" ⚠️ Dead code check completed with findings (non-fatal)")
+ return nil
+ }
+ if strings.TrimSpace(output) != "" {
+ fmt.Println(" ⚠️ Potential dead code found:")
+ fmt.Println(output)
+ } else {
+ fmt.Println(" ✅ No dead code detected")
+ }
+ return nil
+}
+
// dashboardInstall runs pnpm install for the dashboard project.
func dashboardInstall() error {
return runQuiet("pnpm", "install", "--dir", dashboardDir)
@@ -1216,6 +1262,23 @@ func quietLint() error {
fmt.Sprintf("--concurrency=%d", coresPerSlot))
}
+// preflightCrossGOOSLint runs golangci-lint with GOOS=linux to catch cross-platform issues.
+func preflightCrossGOOSLint() error {
+ if runtime.GOOS == "linux" {
+ fmt.Println(" ⏭️ Already on Linux — skipping cross-OS lint")
+ return nil
+ }
+ cmd := exec.Command("golangci-lint", "run", "./...")
+ cmd.Env = append(os.Environ(), "GOOS=linux")
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("cross-OS lint (GOOS=linux) failed: %w", err)
+ }
+ fmt.Println(" ✅ Cross-OS lint passed (GOOS=linux)")
+ return nil
+}
+
// quietModTidy runs go mod tidy and verifies no changes, with captured output.
func quietModTidy() error {
goModBefore, err := fileHash("go.mod")
@@ -1726,6 +1789,7 @@ func Preflight() error {
return nil
}))
g.run(preflightStep("Checking format (no rewrite)", fmtCheck))
+ g.run(preflightStep("Checking gofumpt formatting", preflightGofumpt))
g.run(preflightStep("Tidying go.mod and go.sum", quietModTidy, modTidyDone))
g.run(preflightStep("Installing dashboard deps", dashboardInstall, dashInstall))
g.run(preflightStep("Installing website deps", websiteInstall, webInstall))
@@ -1757,6 +1821,8 @@ func Preflight() error {
// === After dashboard build: Go tests need go:embed dist from dashboard ===
g.run(preflightStepAfter([]*gate{dashBuild}, "Running linting (includes vet + staticcheck)", heavy(quietLint)))
+ g.run(preflightStepAfter([]*gate{dashBuild}, "Checking for dead code", heavy(preflightDeadcode)))
+ g.run(preflightStepAfter([]*gate{dashBuild}, "Cross-OS lint (GOOS=linux)", preflightCrossGOOSLint))
g.run(preflightStepAfter([]*gate{dashBuild}, "Running tests with coverage", heavy(quietTestCoverage)))
// === After mod tidy + dashboard build: build Go binary ===
@@ -1808,6 +1874,9 @@ func PreflightSequential() error {
{"Running go vet", Vet},
{"Running staticcheck", Staticcheck},
{"Running standard linting", Lint},
+ {"Checking gofumpt formatting", preflightGofumpt},
+ {"Checking for dead code", preflightDeadcode},
+ {"Cross-OS lint (GOOS=linux)", preflightCrossGOOSLint},
{"Running quick security scan", runQuickSecurity},
{"Checking for known vulnerabilities", runVulncheck},
{"Running all tests with coverage", TestCoverage},
diff --git a/cli/src/cmd/app/commands/add.go b/cli/src/cmd/app/commands/add.go
index b769303b5..51774a041 100644
--- a/cli/src/cmd/app/commands/add.go
+++ b/cli/src/cmd/app/commands/add.go
@@ -224,7 +224,8 @@ func findAzureYamlForAdd() (string, error) {
}
func serviceExistsInYaml(path string, serviceName string) (bool, error) {
- data, err := os.ReadFile(path)
+ cleanPath := filepath.Clean(path)
+ data, err := os.ReadFile(cleanPath)
if err != nil {
return false, err
}
@@ -266,7 +267,8 @@ func serviceExistsInYaml(path string, serviceName string) (bool, error) {
func addServiceToYaml(path string, serviceName string, def *wellknown.ServiceDefinition) error {
// Read file
- data, err := os.ReadFile(path)
+ cleanPath := filepath.Clean(path)
+ data, err := os.ReadFile(cleanPath)
if err != nil {
return err
}
diff --git a/cli/src/cmd/app/commands/core_deps.go b/cli/src/cmd/app/commands/core_deps.go
index 9860d7f1d..615038e23 100644
--- a/cli/src/cmd/app/commands/core_deps.go
+++ b/cli/src/cmd/app/commands/core_deps.go
@@ -107,7 +107,7 @@ func (di *DependencyInstaller) InstallAllFiltered() ([]InstallResult, error) {
// installNodeProjectList installs dependencies for a list of Node.js projects.
func (di *DependencyInstaller) installNodeProjectList(nodeProjects []types.NodeProject) []InstallResult {
- var results []InstallResult
+ results := make([]InstallResult, 0, len(nodeProjects))
for _, nodeProject := range nodeProjects {
result := di.installProject("node", nodeProject.Dir, nodeProject.PackageManager, func() error {
return installer.InstallNodeDependencies(nodeProject)
@@ -119,7 +119,7 @@ func (di *DependencyInstaller) installNodeProjectList(nodeProjects []types.NodeP
// installPythonProjectList installs dependencies for a list of Python projects.
func (di *DependencyInstaller) installPythonProjectList(pythonProjects []types.PythonProject) []InstallResult {
- var results []InstallResult
+ results := make([]InstallResult, 0, len(pythonProjects))
for _, pyProject := range pythonProjects {
result := di.installProject("python", pyProject.Dir, pyProject.PackageManager, func() error {
return installer.SetupPythonVirtualEnv(pyProject)
@@ -131,7 +131,7 @@ func (di *DependencyInstaller) installPythonProjectList(pythonProjects []types.P
// installDotnetProjectList installs dependencies for a list of .NET projects.
func (di *DependencyInstaller) installDotnetProjectList(dotnetProjects []types.DotnetProject) []InstallResult {
- var results []InstallResult
+ results := make([]InstallResult, 0, len(dotnetProjects))
for _, dotnetProject := range dotnetProjects {
result := di.installProject("dotnet", filepath.Dir(dotnetProject.Path), "dotnet", func() error {
return installer.RestoreDotnetProject(dotnetProject)
@@ -155,7 +155,7 @@ func (di *DependencyInstaller) installNodeProjects() ([]InstallResult, error) {
cliout.Step("📦", "Found %s Node.js project(s)", cliout.Count(len(nodeProjects)))
}
- var results []InstallResult
+ results := make([]InstallResult, 0, len(nodeProjects))
for _, nodeProject := range nodeProjects {
result := di.installProject("node", nodeProject.Dir, nodeProject.PackageManager, func() error {
return installer.InstallNodeDependencies(nodeProject)
@@ -181,7 +181,7 @@ func (di *DependencyInstaller) installPythonProjects() ([]InstallResult, error)
cliout.Step("🐍", "Found %s Python project(s)", cliout.Count(len(pythonProjects)))
}
- var results []InstallResult
+ results := make([]InstallResult, 0, len(pythonProjects))
for _, pyProject := range pythonProjects {
result := di.installProject("python", pyProject.Dir, pyProject.PackageManager, func() error {
return installer.SetupPythonVirtualEnv(pyProject)
@@ -207,7 +207,7 @@ func (di *DependencyInstaller) installDotnetProjects() ([]InstallResult, error)
cliout.Step("🔷", "Found %s .NET project(s)", cliout.Count(len(dotnetProjects)))
}
- var results []InstallResult
+ results := make([]InstallResult, 0, len(dotnetProjects))
for _, dotnetProject := range dotnetProjects {
result := InstallResult{
Type: "dotnet",
diff --git a/cli/src/cmd/app/commands/core_helpers.go b/cli/src/cmd/app/commands/core_helpers.go
index 2e437c3a1..0ee08db9b 100644
--- a/cli/src/cmd/app/commands/core_helpers.go
+++ b/cli/src/cmd/app/commands/core_helpers.go
@@ -337,7 +337,10 @@ func cleanDependencies(nodeProjects []types.NodeProject, pythonProjects []types.
// Returns an error if removal fails.
func cleanDirectory(path string) error {
if _, err := os.Stat(path); err != nil {
- return nil // Directory doesn't exist, nothing to clean
+ if os.IsNotExist(err) {
+ return nil
+ }
+ return err
}
// Validate that we're only cleaning expected dependency directories
diff --git a/cli/src/cmd/app/commands/deps.go b/cli/src/cmd/app/commands/deps.go
index ad07805d6..16ccc483f 100644
--- a/cli/src/cmd/app/commands/deps.go
+++ b/cli/src/cmd/app/commands/deps.go
@@ -156,6 +156,7 @@ func (e *depsExecutor) handleNoProjectsCase(searchRoot string) error {
}
// GetDepsOptions is a legacy getter function for backward compatibility.
+//
// Deprecated: Use executor pattern instead.
func GetDepsOptions() *DepsOptions {
depsOptionsMutex.RLock()
diff --git a/cli/src/cmd/app/commands/generate.go b/cli/src/cmd/app/commands/generate.go
index af539527c..73240f04d 100644
--- a/cli/src/cmd/app/commands/generate.go
+++ b/cli/src/cmd/app/commands/generate.go
@@ -2,6 +2,7 @@
package commands
import (
+ "context"
"fmt"
"os"
"os/exec"
@@ -41,6 +42,13 @@ type GenerateResult struct {
Skipped int // Number of existing reqs preserved
}
+const (
+ langPython = "python"
+ langDotnet = "dotnet"
+ pkgPNPM = "pnpm"
+ pkgPoetry = "poetry"
+)
+
// runGenerate is the main entry point for the generate command.
func runGenerate(config GenerateConfig) error {
cliout.CommandHeader("reqs --generate", "Generate requirements from project")
@@ -110,7 +118,7 @@ func runGenerate(config GenerateConfig) error {
}
// detectProjectReqs scans the project directory for all dependencies.
-func detectProjectReqs(projectDir string) ([]DetectedRequirement, error) {
+func detectProjectReqs(projectDir string) ([]DetectedRequirement, error) { //nolint:unparam // return value kept for future use/interface conformance
var requirements []DetectedRequirement
foundSources := make(map[string]bool)
@@ -186,9 +194,9 @@ func detectProjectReqs(projectDir string) ([]DetectedRequirement, error) {
foundSources["Logic Apps Standard"] = true
case "nodejs":
foundSources["Node.js Functions"] = true
- case "python":
+ case langPython:
foundSources["Python Functions"] = true
- case "dotnet":
+ case langDotnet:
foundSources[".NET Functions"] = true
case "java":
foundSources["Java Functions"] = true
@@ -220,7 +228,7 @@ func detectProjectReqs(projectDir string) ([]DetectedRequirement, error) {
requirements = append(requirements, req)
}
}
- case "python":
+ case langPython:
// Python already detected above if requirements.txt exists
// But we should ensure it's added for Functions projects
if !hasPythonProject(projectDir) {
@@ -228,7 +236,7 @@ func detectProjectReqs(projectDir string) ([]DetectedRequirement, error) {
requirements = append(requirements, req)
}
}
- case "dotnet":
+ case langDotnet:
// .NET already detected above if .csproj exists
// But we should ensure it's added for Functions projects
if !hasDotnetProject(projectDir) {
@@ -348,7 +356,7 @@ func detectNodePackageManager(projectDir string) DetectedRequirement {
}
func detectPython(_ string) DetectedRequirement {
- return detectToolWithSource("python", "requirements.txt or pyproject.toml", false)
+ return detectToolWithSource(langPython, "requirements.txt or pyproject.toml", false)
}
func detectPythonPackageManager(projectDir string) DetectedRequirement {
@@ -358,7 +366,7 @@ func detectPythonPackageManager(projectDir string) DetectedRequirement {
}
func detectDotnet(_ string) DetectedRequirement {
- return detectToolWithSource("dotnet", ".csproj or .sln", false)
+ return detectToolWithSource(langDotnet, ".csproj or .sln", false)
}
func detectAspire(_ string) DetectedRequirement {
@@ -459,7 +467,7 @@ func getToolVersion(toolName string) (string, error) {
// Execute version command directly to capture output
// #nosec G204 -- Command and args come from toolRegistry which is a controlled map
- cmd := exec.Command(toolConfig.Command, toolConfig.Args...)
+ cmd := exec.CommandContext(context.Background(), toolConfig.Command, toolConfig.Args...)
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("tool not installed: %s", toolName)
@@ -531,17 +539,17 @@ func normalizeVersion(installedVersion string, toolName string) string {
parts := strings.Split(installedVersion, ".")
switch toolName {
- case "node", "dotnet", "go", "rust", "docker", "git":
+ case "node", langDotnet, "go", "rust", "docker", "git":
// Major version only: "22.3.0" -> "22.0.0"
if len(parts) >= 1 {
return parts[0] + ".0.0"
}
- case "python":
+ case langPython:
// Major.Minor version: "3.12.5" -> "3.12.0"
if len(parts) >= 2 {
return parts[0] + "." + parts[1] + ".0"
}
- case "pnpm", "npm", "yarn", "poetry", "uv", "pip", "pipenv":
+ case pkgPNPM, "npm", "yarn", pkgPoetry, "uv", "pip", "pipenv":
// Major version for package managers: "9.1.4" -> "9.0.0"
if len(parts) >= 1 {
return parts[0] + ".0.0"
@@ -583,13 +591,13 @@ func displayDetectedDependencies(requirements []DetectedRequirement) {
if req.Name == "node" {
// Look for package manager in other requirements
for _, r := range requirements {
- if r.Name == "pnpm" || r.Name == "yarn" || r.Name == "npm" {
+ if r.Name == pkgPNPM || r.Name == "yarn" || r.Name == "npm" {
pkgMgr = r.Name
break
}
}
}
- if req.Name == "node" || req.Name == "npm" || req.Name == "pnpm" || req.Name == "yarn" {
+ if req.Name == "node" || req.Name == "npm" || req.Name == pkgPNPM || req.Name == "yarn" {
sources[fmt.Sprintf("Node.js project (%s)", pkgMgr)] = true
}
} else if strings.Contains(req.Source, "AppHost.cs") {
@@ -602,15 +610,15 @@ func displayDetectedDependencies(requirements []DetectedRequirement) {
sources["Docker configuration"] = true
} else if strings.Contains(req.Source, "requirements.txt") || strings.Contains(req.Source, "pyproject.toml") {
pkgMgr := req.Name
- if req.Name == "python" {
+ if req.Name == langPython {
for _, r := range requirements {
- if r.Name == "poetry" || r.Name == "uv" || r.Name == "pip" || r.Name == "pipenv" {
+ if r.Name == pkgPoetry || r.Name == "uv" || r.Name == "pip" || r.Name == "pipenv" {
pkgMgr = r.Name
break
}
}
}
- if req.Name == "python" || req.Name == "pip" || req.Name == "poetry" || req.Name == "uv" || req.Name == "pipenv" {
+ if req.Name == langPython || req.Name == "pip" || req.Name == pkgPoetry || req.Name == "uv" || req.Name == "pipenv" {
sources[fmt.Sprintf("Python project (%s)", pkgMgr)] = true
}
}
@@ -651,9 +659,9 @@ func displayDetectedReqs(reqs []DetectedRequirement) {
if req.InstalledVersion == "" {
cliout.ItemError("%s: NOT INSTALLED", req.Name)
switch req.Name {
- case "pnpm":
+ case pkgPNPM:
cliout.Item(" Install: npm install -g pnpm")
- case "poetry":
+ case pkgPoetry:
cliout.Item(" Install: curl -sSL https://install.python-poetry.org | python3 -")
case "uv":
cliout.Item(" Install: curl -LsSf https://astral.sh/uv/install.sh | sh")
@@ -732,7 +740,7 @@ func mergeReqs(azureYamlPath string, detected []DetectedRequirement) (int, int,
existingCount := len(azureYaml.Reqs)
// Convert detected requirements to generic map format for yamlutil
- var items []map[string]interface{}
+ items := make([]map[string]interface{}, 0, len(detected))
for _, det := range detected {
item := map[string]interface{}{
"name": det.Name,
@@ -771,26 +779,27 @@ func formatReqItem(item map[string]interface{}, arrayIndent string) string {
var builder strings.Builder
// Array item with Name
- builder.WriteString(arrayIndent)
- builder.WriteString("- name: ")
- builder.WriteString(item["name"].(string))
- builder.WriteString("\n")
+ _, _ = builder.WriteString(arrayIndent)
+ _, _ = builder.WriteString("- name: ")
+ name, _ := item["name"].(string)
+ _, _ = builder.WriteString(name)
+ _, _ = builder.WriteString("\n")
// MinVersion (quoted)
- builder.WriteString(arrayIndent)
- builder.WriteString(" minVersion: ")
- minVersion := item["minVersion"].(string)
+ _, _ = builder.WriteString(arrayIndent)
+ _, _ = builder.WriteString(" minVersion: ")
+ minVersion, _ := item["minVersion"].(string)
if strings.HasPrefix(minVersion, `"`) {
- builder.WriteString(minVersion)
+ _, _ = builder.WriteString(minVersion)
} else {
- builder.WriteString(`"` + minVersion + `"`)
+ _, _ = builder.WriteString(`"` + minVersion + `"`)
}
- builder.WriteString("\n")
+ _, _ = builder.WriteString("\n")
// CheckRunning (if present)
if checkRunning, ok := item["checkRunning"].(bool); ok && checkRunning {
- builder.WriteString(arrayIndent)
- builder.WriteString(" checkRunning: true\n")
+ _, _ = builder.WriteString(arrayIndent)
+ _, _ = builder.WriteString(" checkRunning: true\n")
}
return builder.String()
diff --git a/cli/src/cmd/app/commands/health.go b/cli/src/cmd/app/commands/health.go
index 1da734cf2..6b12e9696 100644
--- a/cli/src/cmd/app/commands/health.go
+++ b/cli/src/cmd/app/commands/health.go
@@ -269,7 +269,7 @@ func validateHealthFlags() error {
if healthStream && healthInterval <= healthTimeout {
return fmt.Errorf("interval (%v) must be greater than timeout (%v) in streaming mode", healthInterval, healthTimeout)
}
- if healthOutput != "text" && healthOutput != "json" && healthOutput != "table" {
+ if healthOutput != "text" && healthOutput != jsonOutputVal && healthOutput != "table" {
return fmt.Errorf("invalid output format: must be 'text', 'json', or 'table'")
}
// Validate metrics port is in valid range
@@ -300,7 +300,7 @@ func parseServiceFilter(serviceStr string) []string {
}
// setupSignalHandler sets up signal handling for graceful shutdown
-// The goroutine will exit when either a signal is received or the context is cancelled
+// The goroutine will exit when either a signal is received or the context is canceled
func setupSignalHandler(ctx context.Context, cancel context.CancelFunc) {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
@@ -317,7 +317,7 @@ func setupSignalHandler(ctx context.Context, cancel context.CancelFunc) {
fmt.Fprintf(os.Stderr, "\nReceived signal %v, shutting down gracefully...\n", sig)
cancel()
case <-ctx.Done():
- // Context cancelled (normal exit) - just clean up
+ // Context canceled (normal exit) - just clean up
}
}()
}
@@ -375,7 +375,7 @@ func runStreamingMode(ctx context.Context, monitor *healthcheck.HealthMonitor, s
case <-ticker.C:
if err := performStreamCheck(ctx, monitor, serviceFilter, &checkCount, &prevReport, isTTY); err != nil {
if ctx.Err() != nil {
- return nil // Context cancelled, normal shutdown
+ return nil // Context canceled, normal shutdown
}
return err
}
@@ -420,7 +420,7 @@ func performStreamCheck(ctx context.Context, monitor *healthcheck.HealthMonitor,
func displayHealthReport(report *healthcheck.HealthReport) error {
switch healthOutput {
- case "json":
+ case jsonOutputVal:
return displayJSONReport(report)
case "table":
return displayTableReport(report)
@@ -507,7 +507,7 @@ func displayTableReport(report *healthcheck.HealthReport) error {
if result.ResponseTime > 0 {
response = fmt.Sprintf("%dms", result.ResponseTime.Milliseconds())
} else if result.Error != "" {
- response = "error"
+ response = statusError
}
fmt.Printf("│ %-12s │ %-9s │ %-9s │ %-32s │ %-8s │\n",
diff --git a/cli/src/cmd/app/commands/health_e2e_test.go b/cli/src/cmd/app/commands/health_e2e_test.go
index f23f0ce76..a0175909e 100644
--- a/cli/src/cmd/app/commands/health_e2e_test.go
+++ b/cli/src/cmd/app/commands/health_e2e_test.go
@@ -106,7 +106,7 @@ func TestHealthCommandE2E_FullWorkflow(t *testing.T) {
time.Sleep(constants.TestShortSleepDuration)
if runtime.GOOS == "windows" {
// On Windows, we may need to force kill
- _ = exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", runCmd.Process.Pid)).Run()
+ _ = exec.CommandContext(context.Background(), "taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", runCmd.Process.Pid)).Run()
}
_ = runCmd.Process.Kill()
_ = runCmd.Wait()
diff --git a/cli/src/cmd/app/commands/info.go b/cli/src/cmd/app/commands/info.go
index cca0c7fdc..769d5143b 100644
--- a/cli/src/cmd/app/commands/info.go
+++ b/cli/src/cmd/app/commands/info.go
@@ -18,6 +18,13 @@ var (
infoAll bool
)
+const (
+ statusUnknown = "unknown"
+ statusRunning = "running"
+ statusStopped = "stopped"
+ statusError = "error"
+)
+
// NewInfoCommand creates the info command.
func NewInfoCommand() *cobra.Command {
cmd := &cobra.Command{
@@ -130,8 +137,8 @@ func printInfoDefault(projectDir string, services []*serviceinfo.ServiceInfo, az
}
// Get status and health from Local (with defaults if Local is nil)
- status := "unknown"
- health := "unknown"
+ status := statusUnknown
+ health := statusUnknown
if svc.Local != nil {
status = svc.Local.Status
health = svc.Local.Health
@@ -193,7 +200,7 @@ func printInfoDefault(projectDir string, services []*serviceinfo.ServiceInfo, az
}
// Runtime info (only if service is running)
- if svc.Local != nil && svc.Local.Status == "running" {
+ if svc.Local != nil && svc.Local.Status == statusRunning {
if svc.Local.Port > 0 {
cliout.Label(" Port", fmt.Sprintf("%d", svc.Local.Port))
}
@@ -211,7 +218,7 @@ func printInfoDefault(projectDir string, services []*serviceinfo.ServiceInfo, az
// Status and health (from Local)
if svc.Local != nil {
cliout.Label(" Status", formatStatus(svc.Local.Status))
- if svc.Local.Health != "unknown" {
+ if svc.Local.Health != statusUnknown {
cliout.Label(" Health", formatHealth(svc.Local.Health))
}
}
@@ -262,15 +269,15 @@ func getServiceEnvironmentVars(serviceName string, azureEnv map[string]string) m
// Valid statuses: "running", "starting", "error", "stopped", "not-running", "unknown"
func formatStatus(status string) string {
switch status {
- case "running":
+ case statusRunning:
return colorGreen + status + colorReset
case "starting":
return colorYellow + status + colorReset
- case "error":
+ case statusError:
return colorRed + status + colorReset
- case "stopped", "not-running":
+ case statusStopped, "not-running":
return colorGray + status + colorReset
- case "unknown":
+ case statusUnknown:
return colorYellow + status + colorReset
default:
return status
@@ -284,7 +291,7 @@ func formatHealth(health string) string {
return colorGreen + health + colorReset
case "unhealthy":
return colorRed + health + colorReset
- case "unknown":
+ case statusUnknown:
return colorYellow + health + colorReset
default:
return health
@@ -325,11 +332,11 @@ func formatInfoDuration(d time.Duration) string {
// Valid statuses: "running", "starting", "error", "stopped", "not-running", "unknown"
func getInfoStatusIcon(status, health string) string {
// Running and healthy - green check
- if status == "running" && health == "healthy" {
+ if status == statusRunning && health == "healthy" {
return colorGreen + "✓" + colorReset
}
// Running but unhealthy - red X
- if status == "running" && health == "unhealthy" {
+ if status == statusRunning && health == "unhealthy" {
return colorRed + "✗" + colorReset
}
// Starting - yellow circle
@@ -337,11 +344,11 @@ func getInfoStatusIcon(status, health string) string {
return colorYellow + "○" + colorReset
}
// Error status - red X
- if status == "error" {
+ if status == statusError {
return colorRed + "✗" + colorReset
}
// Stopped or not-running - gray dot
- if status == "stopped" || status == "not-running" {
+ if status == statusStopped || status == "not-running" {
return colorGray + "●" + colorReset
}
// Unknown or any other status - yellow question mark
diff --git a/cli/src/cmd/app/commands/logs.go b/cli/src/cmd/app/commands/logs.go
index 0235d60b2..8c7ff8315 100644
--- a/cli/src/cmd/app/commands/logs.go
+++ b/cli/src/cmd/app/commands/logs.go
@@ -52,6 +52,8 @@ const (
// filterCapacityEstimate is the estimated match rate for level filtering.
// Assumes ~25% of logs match a specific level filter.
filterCapacityEstimate = 4
+ logLevelWarn = "warn"
+ logLevelDebug = "debug"
)
// DashboardClient defines the interface for dashboard operations needed by logs.
@@ -61,7 +63,7 @@ type DashboardClient interface {
GetServices(ctx context.Context) ([]*serviceinfo.ServiceInfo, error)
StreamLogs(ctx context.Context, serviceName string, logs chan<- service.LogEntry) error
GetAzureLogs(ctx context.Context, services []string, tail int, since time.Time) ([]service.LogEntry, error)
- GetAzureStatus(ctx context.Context) (*service.AzureStatus, error)
+ GetAzureStatus(ctx context.Context) (*service.AzureStatus, error) //nolint:staticcheck // backward-compatible API
StreamAzureLogs(ctx context.Context, logs chan<- service.LogEntry) error
}
@@ -332,7 +334,7 @@ func (e *logsExecutor) execute(ctx context.Context, args []string) error {
cliout.Warning("%s", w)
}
- if e.opts.source == "azure" && !e.opts.follow {
+ if e.opts.source == string(LogSourceAzure) && !e.opts.follow {
isEmpty := (!collected.HasContext && len(collected.Entries) == 0) ||
(collected.HasContext && len(collected.EntriesWithContext) == 0)
if isEmpty {
@@ -354,13 +356,13 @@ func (e *logsExecutor) execute(ctx context.Context, args []string) error {
// Display logs
if collected.HasContext {
- if e.opts.format == "json" {
+ if e.opts.format == jsonOutputVal {
displayLogsWithContextJSON(collected.EntriesWithContext, outputWriter)
} else {
displayLogsWithContextText(collected.EntriesWithContext, outputWriter, e.opts.timestamps, e.opts.noColor)
}
} else {
- if e.opts.format == "json" {
+ if e.opts.format == jsonOutputVal {
displayLogsJSON(collected.Entries, outputWriter)
} else {
displayLogsText(collected.Entries, outputWriter, e.opts.timestamps, e.opts.noColor)
@@ -494,12 +496,12 @@ func (e *logsExecutor) collect(ctx context.Context, args []string) (*CollectedLo
// Get logs based on source option
var logs []service.LogEntry
switch e.opts.source {
- case "azure":
+ case string(LogSourceAzure):
logs, err = e.collectAzureLogs(ctx, cwd, dashboardClient, targetServices, sinceTime, result)
if err != nil {
return nil, err
}
- case "all":
+ case string(LogSourceAll):
logs, err = e.collectAllLogsQuiet(ctx, cwd, dashboardClient, targetServices, logManager, sinceTime, result)
if err != nil {
return nil, fmt.Errorf("failed to collect logs: %w", err)
@@ -555,7 +557,7 @@ func (e *logsExecutor) collect(ctx context.Context, args []string) (*CollectedLo
// collectAllLogsQuiet collects logs from both local and Azure sources,
// appending warnings to the CollectedLogs result instead of calling cliout.
-func (e *logsExecutor) collectAllLogsQuiet(ctx context.Context, cwd string, dashboardClient DashboardClient, targetServices []string, logManager LogManagerInterface, sinceTime time.Time, result *CollectedLogs) ([]service.LogEntry, error) {
+func (e *logsExecutor) collectAllLogsQuiet(ctx context.Context, cwd string, dashboardClient DashboardClient, targetServices []string, logManager LogManagerInterface, sinceTime time.Time, result *CollectedLogs) ([]service.LogEntry, error) { //nolint:unparam // return value kept for future use/interface conformance
var allLogs []service.LogEntry
// Collect local logs (requires dashboard)
@@ -647,7 +649,7 @@ func (e *logsExecutor) setupOutputWriter() (io.Writer, func(), error) {
// Ensure parent directory exists
outputDir := filepath.Dir(e.opts.file)
if outputDir != "" && outputDir != "." {
- if err := os.MkdirAll(outputDir, 0755); err != nil {
+ if err := os.MkdirAll(outputDir, 0750); err != nil {
return nil, nil, fmt.Errorf("failed to create output directory: %w", err)
}
}
@@ -866,11 +868,11 @@ func logLevelToString(level service.LogLevel) string {
case service.LogLevelInfo:
return "info"
case service.LogLevelWarn:
- return "warn"
+ return logLevelWarn
case service.LogLevelError:
- return "error"
+ return statusError
case service.LogLevelDebug:
- return "debug"
+ return logLevelDebug
default:
return "info"
}
@@ -958,9 +960,9 @@ func (e *logsExecutor) shouldDisplayEntry(entry service.LogEntry, levelFilter se
func (e *logsExecutor) followLogs(ctx context.Context, projectDir string, logManager LogManagerInterface, dashboardClient DashboardClient, serviceFilter []string, levelFilter service.LogLevel, logFilter *service.LogFilter, outputWriter io.Writer) error {
// Handle source-specific follow modes
switch e.opts.source {
- case "azure":
+ case string(LogSourceAzure):
return e.followAzureLogs(ctx, dashboardClient, projectDir, serviceFilter, levelFilter, logFilter, outputWriter)
- case "all":
+ case string(LogSourceAll):
return e.followAllLogs(ctx, projectDir, logManager, dashboardClient, serviceFilter, levelFilter, logFilter, outputWriter)
default: // "local"
return e.followLocalLogs(ctx, projectDir, logManager, dashboardClient, serviceFilter, levelFilter, logFilter, outputWriter)
@@ -968,7 +970,7 @@ func (e *logsExecutor) followLogs(ctx context.Context, projectDir string, logMan
}
// followLocalLogs subscribes to local log streams and displays them.
-func (e *logsExecutor) followLocalLogs(ctx context.Context, projectDir string, logManager LogManagerInterface, dashboardClient DashboardClient, serviceFilter []string, levelFilter service.LogLevel, logFilter *service.LogFilter, outputWriter io.Writer) error {
+func (e *logsExecutor) followLocalLogs(ctx context.Context, _ string, logManager LogManagerInterface, dashboardClient DashboardClient, serviceFilter []string, levelFilter service.LogLevel, logFilter *service.LogFilter, outputWriter io.Writer) error {
// Try in-memory subscriptions first
subscriptions := make(map[string]chan service.LogEntry)
@@ -1009,7 +1011,7 @@ func (e *logsExecutor) followLogsViaDashboard(ctx context.Context, dashboardClie
cliout.Info("Streaming logs from dashboard...")
- // Create context for streaming that can be cancelled
+ // Create context for streaming that can be canceled
streamCtx, cancel := context.WithCancel(ctx)
defer cancel()
@@ -1056,7 +1058,7 @@ func (e *logsExecutor) followLogsViaDashboard(ctx context.Context, dashboardClie
}
// Display log entry
- if e.opts.format == "json" {
+ if e.opts.format == jsonOutputVal {
displayLogsJSON([]service.LogEntry{entry}, outputWriter)
} else {
displayLogsText([]service.LogEntry{entry}, outputWriter, e.opts.timestamps, e.opts.noColor)
@@ -1147,7 +1149,7 @@ func (e *logsExecutor) followLogsInMemory(subscriptions map[string]chan service.
}
// Display log entry
- if e.opts.format == "json" {
+ if e.opts.format == jsonOutputVal {
displayLogsJSON([]service.LogEntry{entry}, outputWriter)
} else {
displayLogsText([]service.LogEntry{entry}, outputWriter, e.opts.timestamps, e.opts.noColor)
@@ -1194,7 +1196,7 @@ func (e *logsExecutor) followAzureLogsViaDashboard(ctx context.Context, dashboar
cliout.Info("Streaming Azure logs (polling every 30s)...")
- // Create context for streaming that can be cancelled
+ // Create context for streaming that can be canceled
streamCtx, cancel := context.WithCancel(ctx)
defer cancel()
@@ -1239,7 +1241,7 @@ func (e *logsExecutor) followAzureLogsViaDashboard(ctx context.Context, dashboar
}
// Display log entry
- if e.opts.format == "json" {
+ if e.opts.format == jsonOutputVal {
displayLogsJSON([]service.LogEntry{entry}, outputWriter)
} else {
displayLogsText([]service.LogEntry{entry}, outputWriter, e.opts.timestamps, e.opts.noColor)
@@ -1262,7 +1264,7 @@ func (e *logsExecutor) followAzureLogsViaDashboard(ctx context.Context, dashboar
func (e *logsExecutor) followAzureLogsStandalone(ctx context.Context, projectDir string, serviceFilter []string, levelFilter service.LogLevel, logFilter *service.LogFilter, outputWriter io.Writer) error {
cliout.Info("Streaming Azure logs (standalone, polling)...")
- // Create context for streaming that can be cancelled
+ // Create context for streaming that can be canceled
streamCtx, cancel := context.WithCancel(ctx)
defer cancel()
@@ -1328,7 +1330,7 @@ func (e *logsExecutor) followAzureLogsStandalone(ctx context.Context, projectDir
continue
}
- if e.opts.format == "json" {
+ if e.opts.format == jsonOutputVal {
displayLogsJSON([]service.LogEntry{entry}, outputWriter)
} else {
displayLogsText([]service.LogEntry{entry}, outputWriter, e.opts.timestamps, e.opts.noColor)
@@ -1348,7 +1350,7 @@ func (e *logsExecutor) followAzureLogsStandalone(ctx context.Context, projectDir
}
// followAllLogs streams logs from both local and Azure sources.
-func (e *logsExecutor) followAllLogs(ctx context.Context, projectDir string, logManager LogManagerInterface, dashboardClient DashboardClient, serviceFilter []string, levelFilter service.LogLevel, logFilter *service.LogFilter, outputWriter io.Writer) error {
+func (e *logsExecutor) followAllLogs(ctx context.Context, projectDir string, _ LogManagerInterface, dashboardClient DashboardClient, serviceFilter []string, levelFilter service.LogLevel, logFilter *service.LogFilter, outputWriter io.Writer) error {
if dashboardClient == nil {
cliout.Warning("Dashboard not running; following Azure logs only.")
return e.followAzureLogsStandalone(ctx, projectDir, serviceFilter, levelFilter, logFilter, outputWriter)
@@ -1362,7 +1364,7 @@ func (e *logsExecutor) followAllLogs(ctx context.Context, projectDir string, log
cliout.Info("Streaming logs from local and Azure sources...")
- // Create context for streaming that can be cancelled
+ // Create context for streaming that can be canceled
streamCtx, cancel := context.WithCancel(ctx)
defer cancel()
@@ -1452,7 +1454,7 @@ func (e *logsExecutor) followAllLogs(ctx context.Context, projectDir string, log
}
// Display log entry
- if e.opts.format == "json" {
+ if e.opts.format == jsonOutputVal {
displayLogsJSON([]service.LogEntry{entry}, outputWriter)
} else {
displayLogsText([]service.LogEntry{entry}, outputWriter, e.opts.timestamps, e.opts.noColor)
@@ -1508,7 +1510,8 @@ func readLogsFromFile(projectDir, serviceName string, tail int, sinceTime time.T
// readSingleLogFile reads log entries from a single log file.
func readSingleLogFile(logFile, serviceName string, sinceTime time.Time) ([]service.LogEntry, error) {
- file, err := os.Open(logFile)
+ cleanLogFile := filepath.Clean(logFile)
+ file, err := os.Open(cleanLogFile)
if err != nil {
return nil, err
}
@@ -1732,9 +1735,9 @@ func displayLogsWithContextText(logs []LogEntryWithContext, w io.Writer, showTim
switch entry.Level {
case "error":
line.WriteString(colorRed + entry.Message + colorReset)
- case "warn":
+ case logLevelWarn:
line.WriteString(colorYellow + entry.Message + colorReset)
- case "debug":
+ case logLevelDebug:
line.WriteString(colorGray + entry.Message + colorReset)
default:
line.WriteString(entry.Message)
@@ -1764,11 +1767,11 @@ func parseLogLevel(level string) service.LogLevel {
switch strings.ToLower(level) {
case "info":
return service.LogLevelInfo
- case "warn", "warning":
+ case logLevelWarn, "warning":
return service.LogLevelWarn
case "error":
return service.LogLevelError
- case "debug":
+ case logLevelDebug:
return service.LogLevelDebug
case "all":
return LogLevelAll
@@ -1799,6 +1802,7 @@ func filterLogsByLevel(logs []service.LogEntry, level service.LogLevel) []servic
// buildLogFilter creates a log filter from options and azure.yaml config.
// This is a test helper function that wraps buildLogFilterInternal.
+//
// Deprecated: Use executor.buildLogFilterInternal directly in new code.
func buildLogFilter(cwd string, exclude string, noBuiltins bool) (*service.LogFilter, error) {
var customPatterns []string
@@ -1848,7 +1852,7 @@ func validateLogsOptions(opts *logsOptions) error {
// Validate format
switch opts.format {
- case "text", "json":
+ case "text", jsonOutputVal:
// Valid formats
default:
return fmt.Errorf("--format must be 'text' or 'json', got '%s'", opts.format)
@@ -1856,7 +1860,7 @@ func validateLogsOptions(opts *logsOptions) error {
// Validate level
switch strings.ToLower(opts.level) {
- case "info", "warn", "warning", "error", "debug", "all":
+ case "info", logLevelWarn, "warning", "error", logLevelDebug, "all":
// Valid levels
default:
return fmt.Errorf("--level must be one of: info, warn, error, debug, all; got '%s'", opts.level)
@@ -1887,12 +1891,12 @@ func validateLogsOptions(opts *logsOptions) error {
// Validate source
switch strings.ToLower(opts.source) {
- case "local", "azure", "all":
+ case string(LogSourceLocal), string(LogSourceAzure), string(LogSourceAll):
// Valid sources - normalize to lowercase
opts.source = strings.ToLower(opts.source)
case "":
// Default to local if not specified
- opts.source = "local"
+ opts.source = string(LogSourceLocal)
default:
return fmt.Errorf("--source must be 'local', 'azure', or 'all'; got '%s'", opts.source)
}
diff --git a/cli/src/cmd/app/commands/logs_executor_test.go b/cli/src/cmd/app/commands/logs_executor_test.go
index f039cd6c0..9c30c65c7 100644
--- a/cli/src/cmd/app/commands/logs_executor_test.go
+++ b/cli/src/cmd/app/commands/logs_executor_test.go
@@ -28,7 +28,7 @@ type mockDashboardClient struct {
logEntries []service.LogEntry
azureLogs []service.LogEntry
getAzureLogsErr error
- azureStatus *service.AzureStatus
+ azureStatus *service.AzureStatus //nolint:staticcheck // required by interface
getAzureStatusErr error
}
@@ -60,12 +60,12 @@ func (m *mockDashboardClient) GetAzureLogs(ctx context.Context, services []strin
return m.azureLogs, nil
}
-func (m *mockDashboardClient) GetAzureStatus(ctx context.Context) (*service.AzureStatus, error) {
+func (m *mockDashboardClient) GetAzureStatus(ctx context.Context) (*service.AzureStatus, error) { //nolint:staticcheck // required by interface
if m.getAzureStatusErr != nil {
return nil, m.getAzureStatusErr
}
if m.azureStatus == nil {
- return &service.AzureStatus{Enabled: false}, nil
+ return &service.AzureStatus{Enabled: false}, nil //nolint:staticcheck // required by interface
}
return m.azureStatus, nil
}
diff --git a/cli/src/cmd/app/commands/mcp.go b/cli/src/cmd/app/commands/mcp.go
index 47d17cbd2..05e72a762 100644
--- a/cli/src/cmd/app/commands/mcp.go
+++ b/cli/src/cmd/app/commands/mcp.go
@@ -78,7 +78,7 @@ func runMCPServe(cmd *cobra.Command, args []string) error {
}
// runMCPServer implements the MCP server logic
-func runMCPServer(ctx context.Context) error {
+func runMCPServer(_ context.Context) error {
// System instructions to guide AI on how to use the tools
// This server is part of the azd extension framework and provides runtime operations
instructions := `This MCP server is provided by the azd app extension and focuses on runtime operations for azd projects.
@@ -149,9 +149,9 @@ func executeAzdAppCommand(ctx context.Context, command string, args []string) (m
// executeAzdAppCommandWithTimeout executes an azd app command with a custom timeout
func executeAzdAppCommandWithTimeout(ctx context.Context, command string, args []string, timeout time.Duration) (map[string]interface{}, error) {
- // Check if context is already cancelled
+ // Check if context is already canceled
if err := ctx.Err(); err != nil {
- return nil, fmt.Errorf("command cancelled before execution: %w", err)
+ return nil, fmt.Errorf("command canceled before execution: %w", err)
}
cmdArgs := append([]string{command}, args...)
@@ -169,9 +169,9 @@ func executeAzdAppCommandWithTimeout(ctx context.Context, command string, args [
output, err := cmd.CombinedOutput()
if err != nil {
- // Check if parent context was cancelled
+ // Check if parent context was canceled
if errors.Is(ctx.Err(), context.Canceled) {
- return nil, fmt.Errorf("command cancelled: %w", ctx.Err())
+ return nil, fmt.Errorf("command canceled: %w", ctx.Err())
}
// Check if command context timed out
if errors.Is(cmdCtx.Err(), context.DeadlineExceeded) {
diff --git a/cli/src/cmd/app/commands/mcp_resources.go b/cli/src/cmd/app/commands/mcp_resources.go
index c0580fb6c..54aed0c28 100644
--- a/cli/src/cmd/app/commands/mcp_resources.go
+++ b/cli/src/cmd/app/commands/mcp_resources.go
@@ -25,7 +25,7 @@ func newAzureYamlResource() server.ServerResource {
Handler: func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
// Check context
if err := ctx.Err(); err != nil {
- return nil, fmt.Errorf("request cancelled: %w", err)
+ return nil, fmt.Errorf("request canceled: %w", err)
}
// Get and validate project directory
@@ -88,7 +88,7 @@ func newServiceConfigResource() server.ServerResource {
Handler: func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
// Check context
if err := ctx.Err(); err != nil {
- return nil, fmt.Errorf("request cancelled: %w", err)
+ return nil, fmt.Errorf("request canceled: %w", err)
}
// Get service configurations from project directory
diff --git a/cli/src/cmd/app/commands/mcp_tools.go b/cli/src/cmd/app/commands/mcp_tools.go
index 953990610..7947aa331 100644
--- a/cli/src/cmd/app/commands/mcp_tools.go
+++ b/cli/src/cmd/app/commands/mcp_tools.go
@@ -149,7 +149,7 @@ func handleGetServiceLogs(ctx context.Context, args azdext.ToolArgs) (*mcp.CallT
}
if ctxErr := ctx.Err(); ctxErr != nil {
- return azdext.MCPErrorResult("Request cancelled: %v", ctxErr), nil
+ return azdext.MCPErrorResult("Request canceled: %v", ctxErr), nil
}
collectCtx, collectCancel := context.WithTimeout(ctx, defaultCommandTimeout)
@@ -243,7 +243,7 @@ func handleGetServiceErrors(ctx context.Context, args azdext.ToolArgs) (*mcp.Cal
}
if err := ctx.Err(); err != nil {
- return azdext.MCPErrorResult("Request cancelled: %v", err), nil
+ return azdext.MCPErrorResult("Request canceled: %v", err), nil
}
collectCtx, collectCancel := context.WithTimeout(ctx, defaultCommandTimeout)
@@ -354,9 +354,8 @@ func handleRunServices(ctx context.Context, args azdext.ToolArgs) (*mcp.CallTool
}
// Note: azd app run is interactive and long-running, so we run it in a non-blocking way
- // and return information about the command being executed
- // The context is intentionally NOT used here because the process should continue running
- cmd := exec.Command(azdCommand, append([]string{appSubcommand, "run"}, cmdArgs...)...)
+ // and return information about the command being executed.
+ cmd := exec.CommandContext(context.WithoutCancel(ctx), azdCommand, append([]string{appSubcommand, "run"}, cmdArgs...)...)
// Create pipes to capture startup errors without blocking
stderrPipe, err := cmd.StderrPipe()
@@ -575,7 +574,7 @@ func handleInstallDependencies(ctx context.Context, args azdext.ToolArgs) (*mcp.
}
if ctxErr := ctx.Err(); ctxErr != nil {
- return azdext.MCPErrorResult("Request cancelled: %v", ctxErr), nil
+ return azdext.MCPErrorResult("Request canceled: %v", ctxErr), nil
}
cmdCtx, cancel := context.WithTimeout(ctx, dependencyInstallTimeout)
@@ -585,7 +584,7 @@ func handleInstallDependencies(ctx context.Context, args azdext.ToolArgs) (*mcp.
output, err := cmd.CombinedOutput()
if err != nil {
if errors.Is(ctx.Err(), context.Canceled) {
- return azdext.MCPErrorResult("Request was cancelled"), nil
+ return azdext.MCPErrorResult("Request was canceled"), nil
}
if errors.Is(cmdCtx.Err(), context.DeadlineExceeded) {
return azdext.MCPErrorResult("Dependency installation timed out after %v", dependencyInstallTimeout), nil
diff --git a/cli/src/cmd/app/commands/notifications.go b/cli/src/cmd/app/commands/notifications.go
index 57cc66e95..1f10fe613 100644
--- a/cli/src/cmd/app/commands/notifications.go
+++ b/cli/src/cmd/app/commands/notifications.go
@@ -181,12 +181,12 @@ func newNotificationsClearCmd() *cobra.Command {
case response = <-responseChan:
// User provided input
case <-ctx.Done():
- fmt.Println("\nCancelled by context")
+ fmt.Println("\nCanceled by context")
return ctx.Err()
}
if response != "y" && response != "Y" {
- fmt.Println("Cancelled")
+ fmt.Println("Canceled")
return nil
}
diff --git a/cli/src/cmd/app/commands/reqs.go b/cli/src/cmd/app/commands/reqs.go
index 60cca9b8b..5a56d2bcc 100644
--- a/cli/src/cmd/app/commands/reqs.go
+++ b/cli/src/cmd/app/commands/reqs.go
@@ -1,6 +1,7 @@
package commands
import (
+ "context"
"fmt"
"os"
"os/exec"
@@ -53,6 +54,11 @@ type AzureYaml struct {
Services map[string]ReqsService `yaml:"services,omitempty"`
}
+const (
+ toolDocker = "docker"
+ osWindows = "windows"
+)
+
// hasContainerServices returns true if any service is a container service.
func (a *AzureYaml) hasContainerServices() bool {
for _, svc := range a.Services {
@@ -69,7 +75,7 @@ func (a *AzureYaml) hasContainerServices() bool {
// hasDockerReq returns true if Docker is already in the reqs list.
func (a *AzureYaml) hasDockerReq() bool {
for _, req := range a.Reqs {
- if strings.EqualFold(req.Name, "docker") {
+ if strings.EqualFold(req.Name, toolDocker) {
return true
}
}
@@ -148,8 +154,8 @@ var toolRegistry = map[string]ToolConfig{
Command: "aspire",
Args: []string{"--version"},
},
- "docker": {
- Command: "docker",
+ toolDocker: {
+ Command: toolDocker,
Args: []string{"--version"},
VersionField: 2, // "Docker version 28.5.1, build ..." -> take field 2
},
@@ -206,28 +212,28 @@ var toolAliases = map[string]string{
// installURLRegistry maps tool names to their installation page URLs.
var installURLRegistry = map[string]string{
- "node": "https://nodejs.org/",
- "npm": "https://nodejs.org/",
- "pnpm": "https://pnpm.io/installation",
- "yarn": "https://yarnpkg.com/getting-started/install",
- "python": "https://www.python.org/downloads/",
- "pip": "https://www.python.org/downloads/",
- "poetry": "https://python-poetry.org/docs/#installation",
- "uv": "https://docs.astral.sh/uv/getting-started/installation/",
- "pipenv": "https://pipenv.pypa.io/en/latest/installation.html",
- "dotnet": "https://dotnet.microsoft.com/download",
- "aspire": "https://learn.microsoft.com/dotnet/aspire/fundamentals/setup-tooling",
- "docker": "https://www.docker.com/products/docker-desktop",
- "git": "https://git-scm.com/downloads",
- "go": "https://go.dev/dl/",
- "azd": "https://aka.ms/install-azd",
- "az": "https://aka.ms/installazurecli",
- "air": "https://github.com/air-verse/air#installation",
- "func": "https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools",
- "java": "https://adoptium.net/",
- "mvn": "https://maven.apache.org/install.html",
- "gradle": "https://gradle.org/install/",
- "gh": "https://cli.github.com/",
+ "node": "https://nodejs.org/",
+ "npm": "https://nodejs.org/",
+ "pnpm": "https://pnpm.io/installation",
+ "yarn": "https://yarnpkg.com/getting-started/install",
+ "python": "https://www.python.org/downloads/",
+ "pip": "https://www.python.org/downloads/",
+ "poetry": "https://python-poetry.org/docs/#installation",
+ "uv": "https://docs.astral.sh/uv/getting-started/installation/",
+ "pipenv": "https://pipenv.pypa.io/en/latest/installation.html",
+ "dotnet": "https://dotnet.microsoft.com/download",
+ "aspire": "https://learn.microsoft.com/dotnet/aspire/fundamentals/setup-tooling",
+ toolDocker: "https://www.docker.com/products/docker-desktop",
+ "git": "https://git-scm.com/downloads",
+ "go": "https://go.dev/dl/",
+ "azd": "https://aka.ms/install-azd",
+ "az": "https://aka.ms/installazurecli",
+ "air": "https://github.com/air-verse/air#installation",
+ "func": "https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools",
+ "java": "https://adoptium.net/",
+ "mvn": "https://maven.apache.org/install.html",
+ "gradle": "https://gradle.org/install/",
+ "gh": "https://cli.github.com/",
}
// NewReqsCommand creates the reqs command.
@@ -357,7 +363,7 @@ func (pc *PrerequisiteChecker) Check(prereq Prerequisite) ReqResult {
// When Podman is aliased to Docker, skip version comparison since version schemes differ.
// Podman uses its own versioning (e.g., 5.7.0) which is not comparable to Docker versions (e.g., 20.10.0).
- if isPodman && prereq.Name == "docker" {
+ if isPodman && prereq.Name == toolDocker {
result.Message = "Podman detected (version check skipped)"
if !cliout.IsJSON() {
cliout.ItemSuccess("%s: %s via Podman (version check skipped)", prereq.Name, version)
@@ -445,7 +451,7 @@ func (pc *PrerequisiteChecker) getInstalledVersion(prereq Prerequisite) (install
config := pc.getToolConfig(prereq)
// #nosec G204 -- Command and args come from toolRegistry or validated azure.yaml prerequisite configuration
- cmd := exec.Command(config.Command, config.Args...)
+ cmd := exec.CommandContext(context.Background(), config.Command, config.Args...)
output, err := cmd.CombinedOutput()
if err != nil {
return false, "", false
@@ -506,8 +512,8 @@ func (pc *PrerequisiteChecker) checkIsRunning(prereq Prerequisite) bool {
// Default checks for known tools
if command == "" {
switch prereq.Name {
- case "docker":
- command = "docker"
+ case toolDocker:
+ command = toolDocker
args = []string{"ps"}
default:
// No default running check for this tool
@@ -518,7 +524,7 @@ func (pc *PrerequisiteChecker) checkIsRunning(prereq Prerequisite) bool {
}
// #nosec G204 -- Command and args come from azure.yaml running check configuration or default Docker check
- cmd := exec.Command(command, args...)
+ cmd := exec.CommandContext(context.Background(), command, args...)
output, err := cmd.CombinedOutput()
// Check exit code
@@ -920,7 +926,7 @@ func runReqsFix() error {
cliout.Info("ℹ️ Note: Tools may not be available in THIS terminal session")
// Provide platform-specific refresh instructions
- if runtime.GOOS == "windows" {
+ if runtime.GOOS == osWindows {
cliout.Info(" To refresh PATH in your current PowerShell session, run:")
cliout.Info(" %s$env:PATH = [System.Environment]::GetEnvironmentVariable(\"Path\",\"Machine\") + \";\" + [System.Environment]::GetEnvironmentVariable(\"Path\",\"User\")%s", cliout.Dim, cliout.Reset)
cliout.Info(" Or simply restart your terminal")
diff --git a/cli/src/cmd/app/commands/reqs_fix_integration_test.go b/cli/src/cmd/app/commands/reqs_fix_integration_test.go
index b8aab5006..bf9636fd6 100644
--- a/cli/src/cmd/app/commands/reqs_fix_integration_test.go
+++ b/cli/src/cmd/app/commands/reqs_fix_integration_test.go
@@ -3,6 +3,7 @@
package commands
import (
+ "context"
"fmt"
"os"
"os/exec"
@@ -327,7 +328,7 @@ func TestReqsFixIntegration_CacheClearing(t *testing.T) {
// Helper functions
-func buildTestTool(t *testing.T, targetDir string) string {
+func buildTestTool(t *testing.T, targetDir string) string { //nolint:unparam // return value kept for future use in integration tests
t.Helper()
if err := os.MkdirAll(targetDir, 0755); err != nil {
@@ -347,7 +348,7 @@ func buildTestTool(t *testing.T, targetDir string) string {
}
exePath := filepath.Join(targetDir, exeName)
- cmd := exec.Command("go", "build", "-o", exePath, sourcePath)
+ cmd := exec.CommandContext(context.Background(), "go", "build", "-o", exePath, sourcePath)
cmd.Dir = targetDir
output, err := cmd.CombinedOutput()
if err != nil {
@@ -371,7 +372,7 @@ func addToUserPATH(t *testing.T, dir string) {
}
// Read current User PATH from registry
- cmd := exec.Command("powershell", "-Command",
+ cmd := exec.CommandContext(context.Background(), "powershell", "-Command",
"[Environment]::GetEnvironmentVariable('Path', 'User')")
output, err := cmd.Output()
if err != nil {
@@ -386,7 +387,7 @@ func addToUserPATH(t *testing.T, dir string) {
// Add to User PATH
newPath := currentPath + ";" + dir
- cmd = exec.Command("powershell", "-Command",
+ cmd = exec.CommandContext(context.Background(), "powershell", "-Command",
fmt.Sprintf("[Environment]::SetEnvironmentVariable('Path', '%s', 'User')", newPath))
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to add to User PATH: %v", err)
@@ -403,7 +404,7 @@ func cleanupUserPATH(t *testing.T, dir string) {
}
// Read current User PATH
- cmd := exec.Command("powershell", "-Command",
+ cmd := exec.CommandContext(context.Background(), "powershell", "-Command",
"[Environment]::GetEnvironmentVariable('Path', 'User')")
output, err := cmd.Output()
if err != nil {
@@ -421,7 +422,7 @@ func cleanupUserPATH(t *testing.T, dir string) {
newPath = strings.ReplaceAll(newPath, dir+";", "")
newPath = strings.ReplaceAll(newPath, dir, "")
- cmd = exec.Command("powershell", "-Command",
+ cmd = exec.CommandContext(context.Background(), "powershell", "-Command",
fmt.Sprintf("[Environment]::SetEnvironmentVariable('Path', '%s', 'User')", newPath))
if err := cmd.Run(); err != nil {
t.Logf("Warning: Failed to remove from User PATH: %v", err)
diff --git a/cli/src/cmd/app/commands/restart.go b/cli/src/cmd/app/commands/restart.go
index 2adb8fde6..ffb426875 100644
--- a/cli/src/cmd/app/commands/restart.go
+++ b/cli/src/cmd/app/commands/restart.go
@@ -81,7 +81,7 @@ func runRestart(cmd *cobra.Command, args []string) error {
return nil
}
if !confirmBulkOperation(len(servicesToRestart), "restart", restartYes) {
- cliout.Info("Operation cancelled")
+ cliout.Info("Operation canceled")
return nil
}
} else {
diff --git a/cli/src/cmd/app/commands/run.go b/cli/src/cmd/app/commands/run.go
index 02e2aab66..81c64872c 100644
--- a/cli/src/cmd/app/commands/run.go
+++ b/cli/src/cmd/app/commands/run.go
@@ -137,7 +137,7 @@ func runServicesFromAzureYaml(ctx context.Context, azureYamlPath string, runtime
}
// runAzdMode runs services in azd mode with individual service orchestration.
-func runAzdMode(ctx context.Context, azureYamlPath, azureYamlDir string) error {
+func runAzdMode(_ context.Context, azureYamlPath, azureYamlDir string) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
@@ -520,7 +520,7 @@ func startDashboardMonitor(ctx context.Context, wg *sync.WaitGroup, dashboardSer
cliout.Hint("Press Ctrl+C to stop", "--web to open browser")
}
- // Block until context is cancelled
+ // Block until context is canceled
<-ctx.Done()
}()
}
@@ -691,7 +691,7 @@ func monitorServiceProcess(ctx context.Context, wg *sync.WaitGroup, serviceName
}
// Intentionally don't cancel context - other services should continue
case <-ctx.Done():
- // Context cancelled by signal - proceed to graceful shutdown
+ // Context canceled by signal - proceed to graceful shutdown
return
}
}
diff --git a/cli/src/cmd/app/commands/service_control.go b/cli/src/cmd/app/commands/service_control.go
index dec2b7ea2..67aced4e0 100644
--- a/cli/src/cmd/app/commands/service_control.go
+++ b/cli/src/cmd/app/commands/service_control.go
@@ -115,7 +115,7 @@ func newErrorResult(serviceName, errMsg string) *ServiceControlResult {
}
// validateAndGetService validates the service name and retrieves the registry entry.
-func (c *ServiceController) validateAndGetService(serviceName string) (*registry.ServiceRegistryEntry, *ServiceControlResult) {
+func (c *ServiceController) validateAndGetService(serviceName string) (*registry.ServiceRegistryEntry, *ServiceControlResult) { //nolint:unparam // return value kept for future use/interface conformance
if err := security.ValidateServiceName(serviceName, false); err != nil {
return nil, newErrorResult(serviceName, err.Error())
}
@@ -301,7 +301,7 @@ func (c *ServiceController) performStart(ctx context.Context, entry *registry.Se
// Check context before starting expensive operations
select {
case <-ctx.Done():
- return fmt.Errorf("operation cancelled: %w", ctx.Err())
+ return fmt.Errorf("operation canceled: %w", ctx.Err())
default:
}
@@ -429,7 +429,7 @@ func (c *ServiceController) performStop(ctx context.Context, entry *registry.Ser
// Check context before starting
select {
case <-ctx.Done():
- return fmt.Errorf("operation cancelled: %w", ctx.Err())
+ return fmt.Errorf("operation canceled: %w", ctx.Err())
default:
}
@@ -532,7 +532,7 @@ func printBulkResult(result *BulkServiceControlResult) {
// setupContextWithSignalHandling creates a context that cancels on SIGINT/SIGTERM.
// Returns the context, cancel function, and a cleanup function that should be deferred.
-func setupContextWithSignalHandling() (context.Context, context.CancelFunc, func()) {
+func setupContextWithSignalHandling() (context.Context, context.CancelFunc, func()) { //nolint:unparam // return value kept for future use/interface conformance
ctx, cancel := context.WithCancel(context.Background())
sigChan := make(chan os.Signal, 1)
@@ -577,7 +577,7 @@ func noServicesToOperateResult(stateDesc, opVerb string) BulkServiceControlResul
// handleNoServicesCase handles the common pattern when --all finds no applicable services.
// Returns true if the case was handled (caller should return), false otherwise.
-func handleNoServicesCase(ctrl *ServiceController, stateDesc, opVerb string) bool {
+func handleNoServicesCase(ctrl *ServiceController, stateDesc, opVerb string) bool { //nolint:unparam // return value kept for future use/interface conformance
if len(ctrl.GetAllServices()) == 0 {
printNoServicesRegistered()
if cliout.IsJSON() {
@@ -605,7 +605,7 @@ func oppositeState(state string) string {
}
// confirmBulkOperation prompts for confirmation of bulk operations.
-// Returns true if the user confirms or skipConfirm is true, false if cancelled.
+// Returns true if the user confirms or skipConfirm is true, false if canceled.
func confirmBulkOperation(count int, opVerb string, skipConfirm bool) bool {
if skipConfirm || cliout.IsJSON() {
return true
diff --git a/cli/src/cmd/app/commands/stop.go b/cli/src/cmd/app/commands/stop.go
index e8d382eac..40a6f0697 100644
--- a/cli/src/cmd/app/commands/stop.go
+++ b/cli/src/cmd/app/commands/stop.go
@@ -78,7 +78,7 @@ func runStop(cmd *cobra.Command, args []string) error {
}
}
if !confirmBulkOperation(len(servicesToStop), "stop", stopYes) {
- cliout.Info("Operation cancelled")
+ cliout.Info("Operation canceled")
return nil
}
} else {
diff --git a/cli/src/internal/azure/bicep.go b/cli/src/internal/azure/bicep.go
index 63dfbdebb..f503be2ab 100644
--- a/cli/src/internal/azure/bicep.go
+++ b/cli/src/internal/azure/bicep.go
@@ -270,7 +270,7 @@ resource functionAppDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-0
}
// buildInstructions creates integration instructions for the Bicep template.
-func (g *BicepGenerator) buildInstructions(serviceTypes map[ResourceType]bool) BicepInstructions {
+func (g *BicepGenerator) buildInstructions(_ map[ResourceType]bool) BicepInstructions {
steps := []string{
"Save this template as infra/modules/diagnostic-settings.bicep in your project",
"Ensure your main.bicep has a Log Analytics workspace resource or parameter",
diff --git a/cli/src/internal/azure/client_pool_test.go b/cli/src/internal/azure/client_pool_test.go
index d59cee841..b57451d61 100644
--- a/cli/src/internal/azure/client_pool_test.go
+++ b/cli/src/internal/azure/client_pool_test.go
@@ -227,7 +227,7 @@ func TestGetOrCreateLogAnalyticsClient_ThreadSafety(t *testing.T) {
}
// Collect all clients
- var clients []*LogAnalyticsClient
+ clients := make([]*LogAnalyticsClient, 0, numGoroutines)
for client := range results {
clients = append(clients, client)
}
@@ -465,7 +465,7 @@ func TestClientPool_DoubleCheckedLocking(t *testing.T) {
close(results)
// Collect results
- var clients []*LogAnalyticsClient
+ clients := make([]*LogAnalyticsClient, 0, 2)
for client := range results {
clients = append(clients, client)
}
diff --git a/cli/src/internal/azure/diagnostic_engine.go b/cli/src/internal/azure/diagnostic_engine.go
index 47913eb49..208b4a62b 100644
--- a/cli/src/internal/azure/diagnostic_engine.go
+++ b/cli/src/internal/azure/diagnostic_engine.go
@@ -11,6 +11,7 @@ import (
// DiagnosticStatus represents the overall health status of a service's logging configuration.
type DiagnosticStatus string
+// DiagnosticStatusHealthy and related constants define logging diagnostic outcomes for a service.
const (
DiagnosticStatusHealthy DiagnosticStatus = "healthy" // Logs flowing correctly
DiagnosticStatusPartial DiagnosticStatus = "partial" // Configured but no logs
@@ -21,6 +22,7 @@ const (
// RequirementStatus represents whether a specific requirement is met.
type RequirementStatus string
+// RequirementStatusMet and related constants define whether a diagnostic requirement is satisfied.
const (
RequirementStatusMet RequirementStatus = "met"
RequirementStatusNotMet RequirementStatus = "not-met"
diff --git a/cli/src/internal/azure/diagnostics.go b/cli/src/internal/azure/diagnostics.go
index 8af693177..42866608a 100644
--- a/cli/src/internal/azure/diagnostics.go
+++ b/cli/src/internal/azure/diagnostics.go
@@ -20,6 +20,7 @@ import (
// DiagnosticSettingsStatus represents the status of diagnostic settings for a service.
type DiagnosticSettingsStatus string
+// DiagnosticSettingsConfigured and related constants describe whether diagnostic settings are available for a service.
const (
DiagnosticSettingsConfigured DiagnosticSettingsStatus = "configured"
DiagnosticSettingsNotConfigured DiagnosticSettingsStatus = "not-configured"
diff --git a/cli/src/internal/azure/discovery.go b/cli/src/internal/azure/discovery.go
index d4694a0f4..9049d5795 100644
--- a/cli/src/internal/azure/discovery.go
+++ b/cli/src/internal/azure/discovery.go
@@ -20,6 +20,7 @@ import (
// ResourceType represents the type of Azure compute resource.
type ResourceType string
+// ResourceTypeContainerApp and related constants identify supported Azure compute resource types.
const (
ResourceTypeContainerApp ResourceType = "containerApp"
ResourceTypeAppService ResourceType = "appService"
@@ -170,7 +171,7 @@ func (d *ResourceDiscovery) Discover(ctx context.Context) (*DiscoveryResult, err
}
// getAzdEnvValues runs 'azd env get-values' and parses the output.
-func (d *ResourceDiscovery) getAzdEnvValues(ctx context.Context) (map[string]string, error) {
+func (d *ResourceDiscovery) getAzdEnvValues(ctx context.Context) (map[string]string, error) { //nolint:unparam // return value kept for future use/interface conformance
cmd := exec.CommandContext(ctx, "azd", "env", "get-values", "--output", "json")
if d.projectDir != "" {
cmd.Dir = d.projectDir
diff --git a/cli/src/internal/azure/loganalytics.go b/cli/src/internal/azure/loganalytics.go
index abcd7b043..195a8c7df 100644
--- a/cli/src/internal/azure/loganalytics.go
+++ b/cli/src/internal/azure/loganalytics.go
@@ -15,6 +15,7 @@ import (
// LogLevel represents the severity of a log message.
type LogLevel int
+// LogLevelInfo and related constants define the severity levels assigned to Log Analytics entries.
const (
LogLevelInfo LogLevel = iota
LogLevelWarn
@@ -315,7 +316,7 @@ func (c *LogAnalyticsClient) extractMessage(row []any, colIndex map[string]int,
}
// extractLevel extracts the log level from the row.
-func (c *LogAnalyticsClient) extractLevel(row []any, colIndex map[string]int, message string, resourceType ResourceType) LogLevel {
+func (c *LogAnalyticsClient) extractLevel(row []any, colIndex map[string]int, message string, _ ResourceType) LogLevel {
// Try to get explicit level field
levelStr := getStringFromRow(row, colIndex, "Level", "Stream_s")
levelStr = strings.ToLower(levelStr)
diff --git a/cli/src/internal/azure/loganalytics_integration_test.go b/cli/src/internal/azure/loganalytics_integration_test.go
index f831a6f4c..0bd82f85b 100644
--- a/cli/src/internal/azure/loganalytics_integration_test.go
+++ b/cli/src/internal/azure/loganalytics_integration_test.go
@@ -90,7 +90,7 @@ func TestLogAnalyticsQuery_Integration(t *testing.T) {
}
func truncateRow(row []any) []string {
- var result []string
+ result := make([]string, 0, len(row))
for _, v := range row {
s := fmt.Sprintf("%v", v)
if len(s) > 50 {
diff --git a/cli/src/internal/azure/parse_results_test.go b/cli/src/internal/azure/parse_results_test.go
index d081db3bb..d3785fe09 100644
--- a/cli/src/internal/azure/parse_results_test.go
+++ b/cli/src/internal/azure/parse_results_test.go
@@ -268,7 +268,7 @@ func createMockLogsResponse(rows []mockRow) azlogs.QueryWorkspaceResponse {
}
// Convert rows to interface slices
- var rowData []azlogs.Row
+ rowData := make([]azlogs.Row, 0, len(rows))
for _, r := range rows {
row := azlogs.Row{
r.TimeGenerated,
@@ -312,7 +312,7 @@ func createMockLogsResponseWithoutSource(rows []mockRowNoSource) azlogs.QueryWor
{Name: &levelCol, Type: &typeStr},
}
- var rowData []azlogs.Row
+ rowData := make([]azlogs.Row, 0, len(rows))
for _, r := range rows {
row := azlogs.Row{
r.TimeGenerated,
@@ -352,7 +352,7 @@ func createMockLogsResponseWithNullSource(rows []mockRowNullSource) azlogs.Query
{Name: &levelCol, Type: &typeStr},
}
- var rowData []azlogs.Row
+ rowData := make([]azlogs.Row, 0, len(rows))
for _, r := range rows {
row := azlogs.Row{
r.TimeGenerated,
diff --git a/cli/src/internal/azure/query_builder.go b/cli/src/internal/azure/query_builder.go
index 848d6e929..ac660ac8d 100644
--- a/cli/src/internal/azure/query_builder.go
+++ b/cli/src/internal/azure/query_builder.go
@@ -15,6 +15,8 @@ const (
// defaultQueryResultLimit is the default limit for query results to prevent excessive data transfer.
// Set to 1000 rows as a balance between completeness and performance.
defaultQueryResultLimit = 1000
+ testTimespan = "30m"
+ fieldMessage = "Message"
)
// QueryBuilder helps construct KQL queries from table selections.
@@ -34,7 +36,7 @@ func NewQueryBuilder(serviceName string, timespan string) *QueryBuilder {
sanitizedName := sanitizeKQLString(serviceName)
// Validate timespan format to prevent injection
if !kqlTimespanPattern.MatchString(timespan) {
- timespan = "30m" // Safe default
+ timespan = testTimespan // Safe default
}
return &QueryBuilder{
serviceName: sanitizedName,
@@ -96,7 +98,7 @@ func (qb *QueryBuilder) buildSingleTableQuery(tableName string) string {
// buildUnionQuery generates a union query for multiple tables.
func (qb *QueryBuilder) buildUnionQuery() string {
- var parts []string
+ parts := make([]string, 0, len(qb.tables))
for _, tableName := range qb.tables {
filter := qb.getServiceFilter(tableName)
@@ -164,11 +166,11 @@ func (qb *QueryBuilder) getProjectColumns(tableName string) string {
columns := TableColumns[tableName]
if len(columns) == 0 {
// Default columns if table is not known
- return "Message=tostring(column_ifexists('Message', '')), Level=tostring(column_ifexists('Level', 'INFO'))"
+ return fieldMessage + "=tostring(column_ifexists('" + fieldMessage + "', '')), Level=tostring(column_ifexists('Level', 'INFO'))"
}
// Build projection with column aliases for common fields
- var parts []string
+ parts := make([]string, 0, len(columns))
for _, col := range columns {
if col == "TimeGenerated" {
continue // Already in the base projection
@@ -178,8 +180,8 @@ func (qb *QueryBuilder) getProjectColumns(tableName string) string {
// Add Message alias for display
messageCol := getMessageColumn(tableName)
- if messageCol != "" && !containsString(parts, "Message") {
- parts = append(parts, fmt.Sprintf("Message=%s", messageCol))
+ if messageCol != "" && !containsString(parts, fieldMessage) {
+ parts = append(parts, fmt.Sprintf("%s=%s", fieldMessage, messageCol))
}
return strings.Join(parts, ", ")
@@ -197,9 +199,9 @@ func getMessageColumn(tableName string) string {
case "AppServiceHTTPLogs":
return "CsUriStem"
case "AppServicePlatformLogs":
- return "Message"
+ return fieldMessage
case "FunctionAppLogs":
- return "Message"
+ return fieldMessage
case "ContainerLogV2":
return "LogMessage"
case "ContainerLog":
@@ -207,7 +209,7 @@ func getMessageColumn(tableName string) string {
case "ContainerInstanceLog_CL":
return "Message_s"
case "KubeEvents":
- return "Message"
+ return fieldMessage
default:
return ""
}
@@ -237,7 +239,7 @@ func SubstitutePlaceholders(query, serviceName, timespan string) string {
query = strings.ReplaceAll(query, "{serviceName}", sanitizeKQLString(serviceName))
// Validate timespan format to prevent injection
if !kqlTimespanPattern.MatchString(timespan) {
- timespan = "30m" // Safe default
+ timespan = testTimespan // Safe default
}
query = strings.ReplaceAll(query, "{timespan}", timespan)
return query
diff --git a/cli/src/internal/azure/query_builder_test.go b/cli/src/internal/azure/query_builder_test.go
index b75d6bb70..3ba160094 100644
--- a/cli/src/internal/azure/query_builder_test.go
+++ b/cli/src/internal/azure/query_builder_test.go
@@ -7,7 +7,6 @@ import (
const (
testServiceName = "test-service"
- testTimespan = "30m"
testMyApp = "my-app"
nonEmptyQueryMessage = "Build should return non-empty query"
orderByTimeDescending = "order by TimeGenerated desc"
@@ -22,8 +21,8 @@ func TestNewQueryBuilder(t *testing.T) {
if qb.serviceName != "test-service" {
t.Errorf("serviceName = %q, want %q", qb.serviceName, "test-service")
}
- if qb.timespan != "30m" {
- t.Errorf("timespan = %q, want %q", qb.timespan, "30m")
+ if qb.timespan != testTimespan {
+ t.Errorf("timespan = %q, want %q", qb.timespan, testTimespan)
}
if len(qb.tables) != 0 {
t.Errorf("tables should be empty, got %d tables", len(qb.tables))
@@ -31,7 +30,7 @@ func TestNewQueryBuilder(t *testing.T) {
}
func TestQueryBuilder_WithTables(t *testing.T) {
- qb := NewQueryBuilder("test-service", "30m")
+ qb := NewQueryBuilder("test-service", testTimespan)
tables := []string{"ContainerAppConsoleLogs_CL", "AppServiceConsoleLogs"}
result := qb.WithTables(tables)
@@ -48,7 +47,7 @@ func TestQueryBuilder_WithTables(t *testing.T) {
}
func TestQueryBuilder_Build_EmptyTables(t *testing.T) {
- qb := NewQueryBuilder("test-service", "30m")
+ qb := NewQueryBuilder("test-service", testTimespan)
query := qb.Build()
@@ -58,7 +57,7 @@ func TestQueryBuilder_Build_EmptyTables(t *testing.T) {
}
func TestQueryBuilder_Build_SingleTable(t *testing.T) {
- qb := NewQueryBuilder("test-service", "30m").
+ qb := NewQueryBuilder("test-service", testTimespan).
WithTables([]string{"ContainerAppConsoleLogs_CL"})
query := qb.Build()
@@ -105,7 +104,7 @@ func TestQueryBuilder_Build_SingleTable_NoServiceFilter(t *testing.T) {
}
func TestQueryBuilder_Build_MultipleTablesUnion(t *testing.T) {
- qb := NewQueryBuilder("test-service", "30m").
+ qb := NewQueryBuilder("test-service", testTimespan).
WithTables([]string{"ContainerAppConsoleLogs_CL", "ContainerAppSystemLogs_CL"})
query := qb.Build()
@@ -188,7 +187,7 @@ func TestGetMessageColumn(t *testing.T) {
}
func TestGetProjectColumns(t *testing.T) {
- qb := NewQueryBuilder("test-service", "30m")
+ qb := NewQueryBuilder("test-service", testTimespan)
// Test with known table
columns := qb.getProjectColumns("ContainerAppConsoleLogs_CL")
@@ -247,7 +246,7 @@ func TestBuildQueryFromTables(t *testing.T) {
func TestBuildQueryFromTables_MultipleServices(t *testing.T) {
tables := []string{"AppServiceConsoleLogs", "AppServiceHTTPLogs"}
- query := BuildQueryFromTables(tables, "my-app", "30m")
+ query := BuildQueryFromTables(tables, "my-app", testTimespan)
if query == "" {
t.Fatal("BuildQueryFromTables should return non-empty query")
@@ -276,7 +275,7 @@ func TestSubstitutePlaceholders(t *testing.T) {
name: "Replace service name",
query: "MyTable | where Service == '{serviceName}'",
serviceName: "test-service",
- timespan: "30m",
+ timespan: testTimespan,
wantContain: []string{"test-service"},
},
{
@@ -297,7 +296,7 @@ func TestSubstitutePlaceholders(t *testing.T) {
name: "No placeholders",
query: "MyTable | project TimeGenerated, Message",
serviceName: "test-service",
- timespan: "30m",
+ timespan: testTimespan,
wantContain: []string{"MyTable", "TimeGenerated", "Message"},
},
}
@@ -327,7 +326,7 @@ func TestSubstitutePlaceholders_SanitizeServiceName(t *testing.T) {
// Service names should be sanitized to prevent KQL injection
query := "MyTable | where Service == '{serviceName}'"
serviceName := "test'; drop table--"
- timespan := "30m"
+ timespan := testTimespan
result := SubstitutePlaceholders(query, serviceName, timespan)
@@ -390,7 +389,7 @@ func TestQueryBuilder_Build_DifferentResourceTypes(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- qb := NewQueryBuilder(tt.serviceName, "30m").
+ qb := NewQueryBuilder(tt.serviceName, testTimespan).
WithTables([]string{tt.table})
query := qb.Build()
@@ -403,7 +402,7 @@ func TestQueryBuilder_Build_DifferentResourceTypes(t *testing.T) {
}
func TestQueryBuilder_Build_ResultLimit(t *testing.T) {
- qb := NewQueryBuilder("test-service", "30m").
+ qb := NewQueryBuilder("test-service", testTimespan).
WithTables([]string{"ContainerAppConsoleLogs_CL"})
query := qb.Build()
@@ -415,7 +414,7 @@ func TestQueryBuilder_Build_ResultLimit(t *testing.T) {
}
func TestQueryBuilder_Build_TimeOrdering(t *testing.T) {
- qb := NewQueryBuilder("test-service", "30m").
+ qb := NewQueryBuilder("test-service", testTimespan).
WithTables([]string{"ContainerAppConsoleLogs_CL"})
query := qb.Build()
diff --git a/cli/src/internal/azure/realtime.go b/cli/src/internal/azure/realtime.go
index 8a37ea313..03b58af5c 100644
--- a/cli/src/internal/azure/realtime.go
+++ b/cli/src/internal/azure/realtime.go
@@ -34,8 +34,8 @@ const (
// Implementations provide low-latency log streaming using service-specific APIs.
type RealtimeLogStreamer interface {
// Start begins streaming logs. Logs are sent to the provided channel.
- // The method blocks until the context is cancelled or an error occurs.
- // Returns nil on graceful shutdown (context cancelled), error otherwise.
+ // The method blocks until the context is canceled or an error occurs.
+ // Returns nil on graceful shutdown (context canceled), error otherwise.
Start(ctx context.Context, logs chan<- LogEntry) error
// Stop gracefully stops the streamer.
@@ -105,16 +105,19 @@ func (b *baseStreamer) setConnected(connected bool) {
b.connected = connected
}
+// IsConnected reports whether the streamer currently has an active connection.
func (b *baseStreamer) IsConnected() bool {
b.connectedMu.RLock()
defer b.connectedMu.RUnlock()
return b.connected
}
+// ServiceName returns the configured service name for the streamer.
func (b *baseStreamer) ServiceName() string {
return b.config.ServiceName
}
+// Stop closes the streamer stop channel and marks the streamer as disconnected.
func (b *baseStreamer) Stop() error {
b.stopOnce.Do(func() {
close(b.stopCh)
diff --git a/cli/src/internal/azure/realtime_test.go b/cli/src/internal/azure/realtime_test.go
index 9acd45746..5b40baeb9 100644
--- a/cli/src/internal/azure/realtime_test.go
+++ b/cli/src/internal/azure/realtime_test.go
@@ -367,7 +367,7 @@ data: {"Log":"Error occurred","Stream":"stderr","time":"2024-01-15T10:30:47.123Z
}()
// Collect results
- var entries []LogEntry
+ entries := make([]LogEntry, 0, 3)
for entry := range logs {
entries = append(entries, entry)
}
diff --git a/cli/src/internal/azure/standalone_logs.go b/cli/src/internal/azure/standalone_logs.go
index c20677448..30025e8f1 100644
--- a/cli/src/internal/azure/standalone_logs.go
+++ b/cli/src/internal/azure/standalone_logs.go
@@ -24,6 +24,7 @@ const (
kqlWhere = "| where %s\n"
kqlResourceIDFilter = "_ResourceId contains '%s'"
azureDirName = ".azure"
+ valTrue = "true"
)
// StandaloneLogsConfig holds configuration for standalone Azure log fetching.
@@ -73,11 +74,11 @@ func getServicesFromAzureYAML(projectDir string) ([]ServiceInfo, error) {
serviceNameMap := getServiceNameMap(projectDir)
// Debug: log service name mapping
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Service name map from environment: %v\n", serviceNameMap)
}
- var services []ServiceInfo
+ services := make([]ServiceInfo, 0, len(config.Services))
for name, svc := range config.Services {
// Skip local-only services
if svc.Host == "local" || svc.Host == "" {
@@ -104,7 +105,7 @@ func getServicesFromAzureYAML(projectDir string) ([]ServiceInfo, error) {
}
// Debug: log each service mapping
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Service %s: host=%s, resourceType=%s, azureName=%s\n",
name, svc.Host, info.ResourceType, info.AzureName)
}
@@ -121,7 +122,7 @@ var serviceNameEnvRe = regexp.MustCompile(`^SERVICE_(.+)_NAME$`)
// getServiceNameMap returns a map of azure.yaml service names to Azure resource names.
// Uses environment variables directly since the azd extension framework provides them.
-func getServiceNameMap(projectDir string) map[string]string {
+func getServiceNameMap(_ string) map[string]string {
serviceNameMap := make(map[string]string)
// When running as an azd extension, all environment variables are already available
@@ -172,10 +173,10 @@ func FetchAzureLogsStandalone(ctx context.Context, config StandaloneLogsConfig)
// Try auto-discovery
if discovered, wasDiscovered, err := DiscoverAndStoreWorkspaceID(ctx); err == nil && wasDiscovered {
workspaceID = discovered
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Auto-discovered workspace ID: %s\n", workspaceID)
}
- } else if err != nil && os.Getenv("AZD_APP_DEBUG") == "true" {
+ } else if err != nil && os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Workspace discovery failed: %v\n", err)
}
}
@@ -258,7 +259,7 @@ func FetchAzureLogsStandalone(ctx context.Context, config StandaloneLogsConfig)
}
// Debug: log service grouping
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Target services: %v\n", targetServices)
fmt.Fprintf(os.Stderr, "[DEBUG] Services by type: %v\n", servicesByType)
}
@@ -284,7 +285,7 @@ func FetchAzureLogsStandalone(ctx context.Context, config StandaloneLogsConfig)
if ctx.Err() == nil {
// Log error but continue with other resource types
slog.Warn("Query failed for resource type", "resourceType", resourceType, "error", err)
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Query failed for %s: %v\n", resourceType, err)
}
}
@@ -496,7 +497,7 @@ func DiscoverAndStoreWorkspaceID(ctx context.Context) (string, bool, error) {
}
// Try to discover workspace using az CLI
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Attempting to discover workspace in resource group: %s\n", resourceGroup)
}
@@ -519,7 +520,7 @@ func DiscoverAndStoreWorkspaceID(ctx context.Context) (string, bool, error) {
return "", false, fmt.Errorf("failed to store workspace ID: %w", err)
}
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Discovered and stored workspace ID: %s\n", workspaceID)
}
@@ -759,7 +760,7 @@ type StreamConfig struct {
}
// StreamAzureLogsStandalone streams Azure logs by polling Log Analytics.
-// Logs are sent to the provided channel. The function blocks until ctx is cancelled.
+// Logs are sent to the provided channel. The function blocks until ctx is canceled.
// This enables `azd app logs -f --source azure` without requiring `azd app run`.
func StreamAzureLogsStandalone(ctx context.Context, config StreamConfig, logs chan<- LogEntry) error {
// Get workspace ID from environment if not provided
@@ -773,10 +774,10 @@ func StreamAzureLogsStandalone(ctx context.Context, config StreamConfig, logs ch
// Try auto-discovery
if discovered, wasDiscovered, err := DiscoverAndStoreWorkspaceID(ctx); err == nil && wasDiscovered {
workspaceID = discovered
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Auto-discovered workspace ID: %s\n", workspaceID)
}
- } else if err != nil && os.Getenv("AZD_APP_DEBUG") == "true" {
+ } else if err != nil && os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Workspace discovery failed: %v\n", err)
}
}
@@ -856,7 +857,7 @@ func StreamAzureLogsStandalone(ctx context.Context, config StreamConfig, logs ch
if window <= 0 {
window = 1 * time.Hour
}
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Streaming initial window: %v\n", window)
}
lastSeen := time.Now().Add(-window)
@@ -867,7 +868,7 @@ func StreamAzureLogsStandalone(ctx context.Context, config StreamConfig, logs ch
// Do initial fetch immediately
if err := fetchAndSendLogsMultiType(ctx, client, servicesByType, lastSeen, logs, &lastSeen); err != nil {
// Log error but continue - transient failures shouldn't stop streaming
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Initial fetch failed: %v\n", err)
}
// Don't return - continue to poll loop
@@ -882,7 +883,7 @@ func StreamAzureLogsStandalone(ctx context.Context, config StreamConfig, logs ch
if err := fetchAndSendLogsMultiType(ctx, client, servicesByType, lastSeen, logs, &lastSeen); err != nil {
// For streaming, we don't return on transient errors
// Just skip this poll cycle
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Poll fetch failed: %v\n", err)
}
continue
@@ -892,7 +893,7 @@ func StreamAzureLogsStandalone(ctx context.Context, config StreamConfig, logs ch
}
// fetchAndSendLogsMultiType fetches logs from multiple resource types and sends them to the channel.
-func fetchAndSendLogsMultiType(ctx context.Context, client *LogAnalyticsClient, servicesByType map[ResourceType][]ServiceInfo, since time.Time, logs chan<- LogEntry, lastSeen *time.Time) error {
+func fetchAndSendLogsMultiType(ctx context.Context, client *LogAnalyticsClient, servicesByType map[ResourceType][]ServiceInfo, _ time.Time, logs chan<- LogEntry, lastSeen *time.Time) error {
// Use precise timestamp filtering instead of ago() to avoid duplicate fetches
// This queries: TimeGenerated > lastSeen instead of TimeGenerated > ago(Nm)
@@ -909,14 +910,14 @@ func fetchAndSendLogsMultiType(ctx context.Context, client *LogAnalyticsClient,
// Build query with timestamp-based filtering
query := buildTimestampQuery(resourceType, azureNames, *lastSeen)
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Streaming query for %s: %s\n", resourceType, strings.ReplaceAll(query, "\n", " | "))
}
// Query using custom query (bypasses ago() duration logic)
entries, err := client.QueryLogs(ctx, "", resourceType, 0, query)
if err != nil {
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Query failed for %s: %v\n", resourceType, err)
}
errorCount++
@@ -936,7 +937,7 @@ func fetchAndSendLogsMultiType(ctx context.Context, client *LogAnalyticsClient,
// Sort by timestamp ascending for streaming (oldest first)
sortLogEntriesByTimeAsc(allEntries)
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Fetched %d total entries, lastSeen=%v\n", len(allEntries), lastSeen.Format(time.RFC3339))
}
@@ -955,12 +956,12 @@ func fetchAndSendLogsMultiType(ctx context.Context, client *LogAnalyticsClient,
}
}
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] Sent %d entries to channel\n", sentCount)
}
// Fix 3: Last poll timestamp only shown in debug mode (was always shown, spamming stderr)
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
fmt.Fprintf(os.Stderr, "[DEBUG] [%s] Last polled (sent %d entries)\n", time.Now().Format("15:04:05"), sentCount)
}
diff --git a/cli/src/internal/azure/token_cache.go b/cli/src/internal/azure/token_cache.go
index d2858df46..c9b66a247 100644
--- a/cli/src/internal/azure/token_cache.go
+++ b/cli/src/internal/azure/token_cache.go
@@ -34,7 +34,7 @@ func (tc *TokenCache) Get() string {
// Check if token exists and hasn't expired
if tc.token == "" || time.Now().After(tc.expiresAt) {
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
if tc.token == "" {
slog.Debug("Token cache miss", "reason", "no token", "scope", tc.scope)
} else {
@@ -44,7 +44,7 @@ func (tc *TokenCache) Get() string {
return ""
}
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
slog.Debug("Token cache hit", "expiresIn", time.Until(tc.expiresAt).Round(time.Second), "scope", tc.scope)
}
@@ -62,7 +62,7 @@ func (tc *TokenCache) Set(token string) {
// Azure tokens are typically valid for 1 hour, so 5 minutes is a safe refresh interval
tc.expiresAt = time.Now().Add(5 * time.Minute)
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
slog.Debug("Token cached", "expiresAt", tc.expiresAt, "scope", tc.scope)
}
}
@@ -74,7 +74,7 @@ func (tc *TokenCache) Clear() {
tc.mu.Lock()
defer tc.mu.Unlock()
- if os.Getenv("AZD_APP_DEBUG") == "true" {
+ if os.Getenv("AZD_APP_DEBUG") == valTrue {
if tc.token != "" {
slog.Debug("Token cache cleared", "reason", "auth error", "scope", tc.scope)
}
diff --git a/cli/src/internal/azure/validator_appservice.go b/cli/src/internal/azure/validator_appservice.go
index c4420a97b..0e65e160c 100644
--- a/cli/src/internal/azure/validator_appservice.go
+++ b/cli/src/internal/azure/validator_appservice.go
@@ -31,7 +31,7 @@ func NewAppServiceValidator(credential azcore.TokenCredential, projectDir string
}
// Validate performs comprehensive validation of an App Service's logging configuration.
-func (v *AppServiceValidator) Validate(ctx context.Context, serviceName string, resource *AzureResource) (*ServiceDiagnosticResult, error) {
+func (v *AppServiceValidator) Validate(ctx context.Context, serviceName string, resource *AzureResource) (*ServiceDiagnosticResult, error) { //nolint:dupl // structurally similar validator but different Azure resource type
result := &ServiceDiagnosticResult{
HostType: ResourceTypeAppService,
Requirements: make([]Requirement, 0),
@@ -114,7 +114,7 @@ func (v *AppServiceValidator) Validate(ctx context.Context, serviceName string,
}
// checkDiagnosticSettings checks if diagnostic settings are configured for the App Service.
-func (v *AppServiceValidator) checkDiagnosticSettings(ctx context.Context, resourceID string) (bool, error) {
+func (v *AppServiceValidator) checkDiagnosticSettings(ctx context.Context, resourceID string) (bool, error) { //nolint:unparam // return value kept for future use/interface conformance
checker := &DiagnosticSettingsChecker{
credential: v.credential,
projectDir: v.projectDir,
@@ -125,7 +125,7 @@ func (v *AppServiceValidator) checkDiagnosticSettings(ctx context.Context, resou
}
// generateSetupGuide creates a setup guide for App Service.
-func (v *AppServiceValidator) generateSetupGuide(serviceName string, resource *AzureResource, hasSettings bool, hasLogs bool) *SetupGuide {
+func (v *AppServiceValidator) generateSetupGuide(_ string, _ *AzureResource, hasSettings bool, hasLogs bool) *SetupGuide {
if hasLogs {
return nil
}
diff --git a/cli/src/internal/azure/validator_containerapp.go b/cli/src/internal/azure/validator_containerapp.go
index d07e8d21c..c8d8eb9f2 100644
--- a/cli/src/internal/azure/validator_containerapp.go
+++ b/cli/src/internal/azure/validator_containerapp.go
@@ -31,7 +31,7 @@ func NewContainerAppValidator(credential azcore.TokenCredential, projectDir stri
}
// Validate performs comprehensive validation of a Container App's logging configuration.
-func (v *ContainerAppValidator) Validate(ctx context.Context, serviceName string, resource *AzureResource) (*ServiceDiagnosticResult, error) {
+func (v *ContainerAppValidator) Validate(ctx context.Context, serviceName string, resource *AzureResource) (*ServiceDiagnosticResult, error) { //nolint:dupl // structurally similar validator but different Azure resource type
result := &ServiceDiagnosticResult{
HostType: ResourceTypeContainerApp,
Requirements: make([]Requirement, 0),
@@ -114,7 +114,7 @@ func (v *ContainerAppValidator) Validate(ctx context.Context, serviceName string
}
// checkDiagnosticSettings checks if diagnostic settings are configured for the Container App.
-func (v *ContainerAppValidator) checkDiagnosticSettings(ctx context.Context, resourceID string) (bool, error) {
+func (v *ContainerAppValidator) checkDiagnosticSettings(ctx context.Context, resourceID string) (bool, error) { //nolint:unparam // return value kept for future use/interface conformance
checker := &DiagnosticSettingsChecker{
credential: v.credential,
projectDir: v.projectDir,
@@ -125,7 +125,7 @@ func (v *ContainerAppValidator) checkDiagnosticSettings(ctx context.Context, res
}
// generateSetupGuide creates a setup guide for Container Apps.
-func (v *ContainerAppValidator) generateSetupGuide(serviceName string, resource *AzureResource, hasSettings bool, hasLogs bool) *SetupGuide {
+func (v *ContainerAppValidator) generateSetupGuide(_ string, _ *AzureResource, hasSettings bool, hasLogs bool) *SetupGuide {
if hasLogs {
return nil // No guide needed if logs are flowing
}
diff --git a/cli/src/internal/azure/validator_function.go b/cli/src/internal/azure/validator_function.go
index 6d1681f8b..0b3f0e6b3 100644
--- a/cli/src/internal/azure/validator_function.go
+++ b/cli/src/internal/azure/validator_function.go
@@ -132,7 +132,7 @@ func (v *FunctionValidator) Validate(ctx context.Context, serviceName string, re
}
// checkApplicationInsights checks if Application Insights is configured in the environment.
-func (v *FunctionValidator) checkApplicationInsights(ctx context.Context, serviceName string) (bool, error) {
+func (v *FunctionValidator) checkApplicationInsights(ctx context.Context, _ string) (bool, error) {
// Run azd env get-values to check for APPLICATIONINSIGHTS_CONNECTION_STRING
cmd := exec.CommandContext(ctx, "azd", "env", "get-values")
if v.projectDir != "" {
@@ -149,7 +149,7 @@ func (v *FunctionValidator) checkApplicationInsights(ctx context.Context, servic
}
// checkDiagnosticSettings checks if diagnostic settings are configured for the Function App.
-func (v *FunctionValidator) checkDiagnosticSettings(ctx context.Context, resourceID string) (bool, error) {
+func (v *FunctionValidator) checkDiagnosticSettings(ctx context.Context, resourceID string) (bool, error) { //nolint:unparam // return value kept for future use/interface conformance
checker := &DiagnosticSettingsChecker{
credential: v.credential,
projectDir: v.projectDir,
@@ -160,7 +160,7 @@ func (v *FunctionValidator) checkDiagnosticSettings(ctx context.Context, resourc
}
// generateSetupGuide creates a setup guide for Azure Functions.
-func (v *FunctionValidator) generateSetupGuide(serviceName string, resource *AzureResource, hasAppInsights bool, hasLogs bool) *SetupGuide {
+func (v *FunctionValidator) generateSetupGuide(serviceName string, _ *AzureResource, hasAppInsights bool, hasLogs bool) *SetupGuide {
if hasLogs {
return nil
}
diff --git a/cli/src/internal/azure/verification.go b/cli/src/internal/azure/verification.go
index 19bd46834..485e92b23 100644
--- a/cli/src/internal/azure/verification.go
+++ b/cli/src/internal/azure/verification.go
@@ -13,6 +13,7 @@ import (
// WorkspaceVerificationStatus represents the overall status of workspace verification.
type WorkspaceVerificationStatus string
+// VerificationStatusSuccess and related constants describe the overall outcome of workspace verification.
const (
VerificationStatusSuccess WorkspaceVerificationStatus = "success"
VerificationStatusPartial WorkspaceVerificationStatus = "partial"
@@ -22,6 +23,7 @@ const (
// ServiceVerificationStatus represents the verification status of a single service.
type ServiceVerificationStatus string
+// ServiceStatusOK and related constants describe per-service verification results.
const (
ServiceStatusOK ServiceVerificationStatus = "ok"
ServiceStatusNoLogs ServiceVerificationStatus = "no-logs"
diff --git a/cli/src/internal/config/notifications.go b/cli/src/internal/config/notifications.go
index ddcb3ab7e..9910cb5a6 100644
--- a/cli/src/internal/config/notifications.go
+++ b/cli/src/internal/config/notifications.go
@@ -75,11 +75,17 @@ func ValidateServiceName(name string) error {
// Thread-safe via sync.Map.
var timeCache sync.Map
+const severityWarning = "warning"
+
// parseTimeCached parses a time string with caching.
// Returns the parsed time or an error if the format is invalid.
func parseTimeCached(timeStr string) (time.Time, error) {
if cached, ok := timeCache.Load(timeStr); ok {
- return cached.(time.Time), nil
+ parsed, parsedOK := cached.(time.Time)
+ if !parsedOK {
+ return time.Time{}, fmt.Errorf("cached time for %q has unexpected type %T", timeStr, cached)
+ }
+ return parsed, nil
}
parsed, err := time.Parse("15:04", timeStr)
@@ -208,10 +214,10 @@ func GetGlobalNotificationPreferences() *NotificationPreferences {
func (p *NotificationPreferences) Validate() error {
// Validate severity filter
validSeverities := map[string]bool{
- "critical": true,
- "warning": true,
- "info": true,
- "all": true,
+ "critical": true,
+ severityWarning: true,
+ "info": true,
+ "all": true,
}
if !validSeverities[p.SeverityFilter] {
return fmt.Errorf("invalid severity filter: %s (must be critical, warning, info, or all)", p.SeverityFilter)
@@ -335,10 +341,10 @@ func (p *NotificationPreferences) ShouldNotify(serviceName string, severity stri
switch p.SeverityFilter {
case "critical":
return severity == "critical"
- case "warning":
- return severity == "critical" || severity == "warning"
+ case severityWarning:
+ return severity == "critical" || severity == severityWarning
case "info":
- return severity == "critical" || severity == "warning" || severity == "info"
+ return severity == "critical" || severity == severityWarning || severity == "info"
case "all":
return true
default:
diff --git a/cli/src/internal/dashboard/azure_logs_handlers.go b/cli/src/internal/dashboard/azure_logs_handlers.go
index e64287d4d..53604ccda 100644
--- a/cli/src/internal/dashboard/azure_logs_handlers.go
+++ b/cli/src/internal/dashboard/azure_logs_handlers.go
@@ -150,7 +150,7 @@ func (s *Server) handleAzureLogs(w http.ResponseWriter, r *http.Request) {
}
if err != nil {
- response.Status = "error"
+ response.Status = StatusError
response.Count = 0
response.Error = mapAzureErrorToInfo(err)
diff --git a/cli/src/internal/dashboard/azure_logs_health.go b/cli/src/internal/dashboard/azure_logs_health.go
index 859141ab4..ca0044364 100644
--- a/cli/src/internal/dashboard/azure_logs_health.go
+++ b/cli/src/internal/dashboard/azure_logs_health.go
@@ -10,6 +10,12 @@ import (
"time"
)
+const (
+ statusPass = "pass"
+ statusFail = "fail"
+ statusWarn = "warn"
+)
+
// HealthCheckResponse represents the overall health check result.
type HealthCheckResponse struct {
Status string `json:"status"` // "healthy" | "degraded" | "error"
@@ -48,7 +54,7 @@ func (s *Server) handleAzureLogsHealth(w http.ResponseWriter, r *http.Request) {
response.Checks = append(response.Checks, servicesCheck)
// Check 4: Connectivity
- connectivityCheck := s.checkConnectivity(workspaceCheck.Status == "pass")
+ connectivityCheck := s.checkConnectivity(workspaceCheck.Status == statusPass)
response.Checks = append(response.Checks, connectivityCheck)
// Compute overall status
@@ -66,7 +72,7 @@ func (s *Server) checkAuthentication() HealthCheck {
// Try to create credentials
cred, err := newLogAnalyticsCredential()
if err != nil {
- check.Status = "fail"
+ check.Status = statusFail
check.Message = "Azure credentials not available"
check.Fix = "azd auth login"
return check
@@ -78,13 +84,13 @@ func (s *Server) checkAuthentication() HealthCheck {
err = validateCredentials(ctx, cred)
if err != nil {
- check.Status = "fail"
+ check.Status = statusFail
check.Message = "Azure credentials invalid or expired"
check.Fix = "azd auth login"
return check
}
- check.Status = "pass"
+ check.Status = statusPass
check.Message = "Azure credentials valid"
return check
}
@@ -97,19 +103,19 @@ func (s *Server) checkWorkspaceID() HealthCheck {
workspaceID, err := getWorkspaceIDFromEnv(context.Background())
if err != nil {
- check.Status = "fail"
+ check.Status = statusFail
check.Message = "Log Analytics workspace not configured"
check.Fix = "azd env refresh"
return check
}
if workspaceID == "" {
- check.Status = "warn"
+ check.Status = statusWarn
check.Message = "No azd environment available; Azure log streaming unavailable"
check.Fix = "Run 'azd init' and 'azd provision' to enable Azure log streaming"
return check
}
- check.Status = "pass"
+ check.Status = statusPass
check.Message = fmt.Sprintf("Workspace ID configured: %s", truncateMiddle(workspaceID, 20))
return check
}
@@ -131,13 +137,13 @@ func (s *Server) checkServicesDeployed() HealthCheck {
}
if serviceCount == 0 {
- check.Status = "fail"
+ check.Status = statusFail
check.Message = "No deployed services found"
check.Fix = "azd up"
return check
}
- check.Status = "pass"
+ check.Status = statusPass
check.Message = fmt.Sprintf("Found %d deployed service(s)", serviceCount)
return check
}
@@ -149,21 +155,21 @@ func (s *Server) checkConnectivity(hasWorkspace bool) HealthCheck {
}
if !hasWorkspace {
- check.Status = "warn"
+ check.Status = statusWarn
check.Message = "Cannot verify connectivity without workspace ID"
return check
}
workspaceID, err := getWorkspaceIDFromEnv(context.Background())
if err != nil || workspaceID == "" {
- check.Status = "warn"
+ check.Status = statusWarn
check.Message = "Cannot get workspace ID"
return check
}
cred, err := newLogAnalyticsCredential()
if err != nil {
- check.Status = "warn"
+ check.Status = statusWarn
check.Message = "Cannot create credentials for connectivity test"
return check
}
@@ -173,13 +179,13 @@ func (s *Server) checkConnectivity(hasWorkspace bool) HealthCheck {
_, err = getOrCreateLogAnalyticsClient(ctx, cred, workspaceID)
if err != nil {
slog.Error("failed to create Log Analytics client", "error", err)
- check.Status = "fail"
+ check.Status = statusFail
check.Message = "Failed to connect to Log Analytics"
check.Fix = "Check Azure subscription and permissions"
return check
}
- check.Status = "pass"
+ check.Status = statusPass
check.Message = "Log Analytics client created successfully"
return check
}
@@ -191,15 +197,15 @@ func (s *Server) computeOverallStatus(checks []HealthCheck) string {
for _, check := range checks {
switch check.Status {
- case "fail":
+ case statusFail:
hasError = true
- case "warn":
+ case statusWarn:
hasWarn = true
}
}
if hasError {
- return "error"
+ return StatusError
}
if hasWarn {
return "degraded"
diff --git a/cli/src/internal/dashboard/azure_logs_query.go b/cli/src/internal/dashboard/azure_logs_query.go
index fc918cfed..7ff48e560 100644
--- a/cli/src/internal/dashboard/azure_logs_query.go
+++ b/cli/src/internal/dashboard/azure_logs_query.go
@@ -9,6 +9,11 @@ import (
"github.com/jongio/azd-app/cli/src/internal/service"
)
+const (
+ queryTypeCustom = "custom"
+ defaultQueryTimespan = "30m"
+)
+
// AzureQueryResponse represents the KQL query used for a service.
type AzureQueryResponse struct {
Service string `json:"service"`
@@ -51,7 +56,7 @@ func (s *Server) handleAzureQuery(w http.ResponseWriter, r *http.Request) {
if svc, ok := azureYaml.Services[serviceName]; ok && svc.Logs != nil && svc.Logs.Analytics != nil {
if svc.Logs.Analytics.Query != "" {
query = svc.Logs.Analytics.Query
- resourceType = "custom"
+ resourceType = queryTypeCustom
}
}
}
@@ -62,7 +67,7 @@ func (s *Server) handleAzureQuery(w http.ResponseWriter, r *http.Request) {
resourceType = "containerapp" // Default assumption
query = azure.GetDefaultQuery(azure.ResourceType(resourceType))
// Substitute placeholders for display
- query = substituteQueryPlaceholders(query, serviceName, "30m")
+ query = substituteQueryPlaceholders(query, serviceName, defaultQueryTimespan)
}
response := AzureQueryResponse{
@@ -137,7 +142,7 @@ func (s *Server) handleSaveAzureQuery(w http.ResponseWriter, r *http.Request) {
// Return updated query info
response := AzureQueryResponse{
Service: req.Service,
- ResourceType: "custom",
+ ResourceType: queryTypeCustom,
Query: req.Query,
}
diff --git a/cli/src/internal/dashboard/azure_logs_tables.go b/cli/src/internal/dashboard/azure_logs_tables.go
index 68ba284dc..6814b7bce 100644
--- a/cli/src/internal/dashboard/azure_logs_tables.go
+++ b/cli/src/internal/dashboard/azure_logs_tables.go
@@ -10,6 +10,8 @@ import (
"github.com/jongio/azd-core/yamlutil"
)
+const resourceTables = "tables"
+
// TablesResponse represents the response from the tables API.
type TablesResponse struct {
Tables []azure.TableInfo `json:"tables"`
@@ -127,7 +129,7 @@ func (s *Server) handleGetLogConfig(w http.ResponseWriter, r *http.Request) {
response := LogConfigResponse{
Service: serviceName,
- Mode: "tables",
+ Mode: resourceTables,
ResourceType: "containerapp", // Default
}
@@ -150,16 +152,16 @@ func (s *Server) handleGetLogConfig(w http.ResponseWriter, r *http.Request) {
svcConfig := svc.Logs.Analytics
if len(svcConfig.Tables) > 0 {
response.Tables = svcConfig.Tables
- response.Mode = "tables"
+ response.Mode = resourceTables
}
if svcConfig.Query != "" {
response.Query = svcConfig.Query
- response.Mode = "custom"
+ response.Mode = queryTypeCustom
}
}
// If still no tables and mode is tables, use recommended
- if response.Mode == "tables" && len(response.Tables) == 0 {
+ if response.Mode == resourceTables && len(response.Tables) == 0 {
resourceType := azure.ResourceType(response.ResourceType)
response.Tables = azure.GetRecommendedTables(resourceType)
}
@@ -183,15 +185,15 @@ func (s *Server) handleSaveLogConfig(w http.ResponseWriter, r *http.Request) {
BadRequest(w, errModeRequired, nil)
return
}
- if req.Mode != "tables" && req.Mode != "custom" {
+ if req.Mode != resourceTables && req.Mode != queryTypeCustom {
BadRequest(w, "mode must be 'tables' or 'custom'", nil)
return
}
- if req.Mode == "tables" && len(req.Tables) == 0 {
+ if req.Mode == resourceTables && len(req.Tables) == 0 {
BadRequest(w, "tables required when mode is 'tables'", nil)
return
}
- if req.Mode == "custom" && req.Query == "" {
+ if req.Mode == queryTypeCustom && req.Query == "" {
BadRequest(w, "query required when mode is 'custom'", nil)
return
}
@@ -214,7 +216,7 @@ func (s *Server) handleSaveLogConfig(w http.ResponseWriter, r *http.Request) {
// Prepare tables and query based on mode
var tables []string
var query string
- if req.Mode == "tables" {
+ if req.Mode == resourceTables {
tables = req.Tables
query = ""
} else {
diff --git a/cli/src/internal/dashboard/azure_setup.go b/cli/src/internal/dashboard/azure_setup.go
index ed1ed16aa..72ae17a76 100644
--- a/cli/src/internal/dashboard/azure_setup.go
+++ b/cli/src/internal/dashboard/azure_setup.go
@@ -376,7 +376,7 @@ func (s *Server) checkDiagnosticSettings(ctx context.Context, serviceName, works
// checkLogsFlowing queries for recent logs to verify log flow.
// Returns ISO timestamp of most recent log, or empty string if no logs found.
-func (s *Server) checkLogsFlowing(ctx context.Context, serviceName, workspaceID string, cred azcore.TokenCredential) string {
+func (s *Server) checkLogsFlowing(ctx context.Context, serviceName, workspaceID string, _ azcore.TokenCredential) string {
if workspaceID == "" {
return ""
}
@@ -788,7 +788,7 @@ func (s *Server) handleAzureLogsVerify(w http.ResponseWriter, r *http.Request) {
}
}
- level := "INFO"
+ var level string
switch log.Level {
case azure.LogLevelError:
level = "ERROR"
@@ -796,6 +796,8 @@ func (s *Server) handleAzureLogsVerify(w http.ResponseWriter, r *http.Request) {
level = "WARN"
case azure.LogLevelDebug:
level = "DEBUG"
+ default:
+ level = "INFO"
}
samples = append(samples, VerifyLogsSample{
diff --git a/cli/src/internal/dashboard/classifications.go b/cli/src/internal/dashboard/classifications.go
index 985d991fd..0c1170bb1 100644
--- a/cli/src/internal/dashboard/classifications.go
+++ b/cli/src/internal/dashboard/classifications.go
@@ -23,7 +23,7 @@ type ClassificationsResponse struct {
}
// handleGetClassifications returns all classifications from azure.yaml
-func (s *Server) handleGetClassifications(w http.ResponseWriter, r *http.Request) {
+func (s *Server) handleGetClassifications(w http.ResponseWriter, _ *http.Request) {
classificationsMu.RLock()
defer classificationsMu.RUnlock()
diff --git a/cli/src/internal/dashboard/client.go b/cli/src/internal/dashboard/client.go
index 72ab10ba9..5f5663711 100644
--- a/cli/src/internal/dashboard/client.go
+++ b/cli/src/internal/dashboard/client.go
@@ -254,7 +254,7 @@ func (c *Client) GetWebSocketURL() string {
// StreamLogs connects to the dashboard's log stream via WebSocket and sends log entries to the provided channel.
// The serviceName parameter filters logs to a specific service (empty string for all services).
-// The function blocks until the context is cancelled or an error occurs.
+// The function blocks until the context is canceled or an error occurs.
// The caller is responsible for closing the logs channel after StreamLogs returns.
func (c *Client) StreamLogs(ctx context.Context, serviceName string, logs chan<- service.LogEntry) error {
// Build WebSocket URL
@@ -267,7 +267,10 @@ func (c *Client) StreamLogs(ctx context.Context, serviceName string, logs chan<-
dialCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
- conn, _, err := websocket.Dial(dialCtx, wsURL, nil)
+ conn, resp, err := websocket.Dial(dialCtx, wsURL, nil)
+ if resp != nil && resp.Body != nil {
+ defer resp.Body.Close() //nolint:errcheck // best-effort cleanup
+ }
if err != nil {
return fmt.Errorf("failed to connect to log stream: %w", err)
}
@@ -281,7 +284,7 @@ func (c *Client) StreamLogs(ctx context.Context, serviceName string, logs chan<-
default:
var entry service.LogEntry
if err := wsjson.Read(ctx, conn, &entry); err != nil {
- // Check if context was cancelled
+ // Check if context was canceled
if ctx.Err() != nil {
return ctx.Err()
}
@@ -378,6 +381,8 @@ func (c *Client) GetAzureLogs(ctx context.Context, services []string, tail int,
// GetAzureStatus retrieves the Azure connection status from the dashboard.
// Checks if Azure logging is configured in azure.yaml.
+//
+//nolint:staticcheck // service.AzureStatus is deprecated but kept for backward compatibility
func (c *Client) GetAzureStatus(ctx context.Context) (*service.AzureStatus, error) {
// Check if Azure services are available (indicates Azure is configured)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/azure/services", nil)
@@ -422,7 +427,7 @@ func (c *Client) GetAzureStatus(ctx context.Context) (*service.AzureStatus, erro
}
// StreamAzureLogs connects to the dashboard's Azure log stream via WebSocket.
-// The function blocks until the context is cancelled or an error occurs.
+// The function blocks until the context is canceled or an error occurs.
func (c *Client) StreamAzureLogs(ctx context.Context, logs chan<- service.LogEntry) error {
// Build WebSocket URL
wsURL := c.GetWebSocketURL() + "/api/azure/logs/stream"
@@ -431,7 +436,10 @@ func (c *Client) StreamAzureLogs(ctx context.Context, logs chan<- service.LogEnt
dialCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
- conn, _, err := websocket.Dial(dialCtx, wsURL, nil)
+ conn, resp, err := websocket.Dial(dialCtx, wsURL, nil)
+ if resp != nil && resp.Body != nil {
+ defer resp.Body.Close() //nolint:errcheck // best-effort cleanup
+ }
if err != nil {
return fmt.Errorf("failed to connect to Azure log stream: %w", err)
}
diff --git a/cli/src/internal/dashboard/constants.go b/cli/src/internal/dashboard/constants.go
index 9aa185ba2..81d70d969 100644
--- a/cli/src/internal/dashboard/constants.go
+++ b/cli/src/internal/dashboard/constants.go
@@ -27,7 +27,7 @@ const (
// Common messages
MsgWorkspaceNotConfigured = "Log Analytics workspace not configured"
- MsgAzureCredsNotAvailable = "Azure credentials not available. Run 'azd auth login' to authenticate."
+ MsgAzureCredsNotAvailable = "Azure credentials not available. Run 'azd auth login' to authenticate." //nolint:gosec // G101 - not a credential, just a status message constant
MsgAuthFailed = "Authentication failed or expired. Run 'azd auth login' to re-authenticate."
// Setup steps
diff --git a/cli/src/internal/dashboard/health_stream.go b/cli/src/internal/dashboard/health_stream.go
index ec65557ea..c80a72b06 100644
--- a/cli/src/internal/dashboard/health_stream.go
+++ b/cli/src/internal/dashboard/health_stream.go
@@ -35,6 +35,7 @@ const (
// HealthEventType represents the type of health event sent via SSE.
type HealthEventType string
+// HealthEventTypeHealth and related constants identify the SSE event payloads emitted by health streaming.
const (
HealthEventTypeHealth HealthEventType = "health"
HealthEventTypeChange HealthEventType = "health-change"
diff --git a/cli/src/internal/dashboard/logs_config.go b/cli/src/internal/dashboard/logs_config.go
index 94196bf45..01824162f 100644
--- a/cli/src/internal/dashboard/logs_config.go
+++ b/cli/src/internal/dashboard/logs_config.go
@@ -109,7 +109,7 @@ func savePreferencesWithClient(client azdconfig.ConfigClient, prefs UserPreferen
// HTTP Handlers
// handleGetPreferences returns user preferences
-func (s *Server) handleGetPreferences(w http.ResponseWriter, r *http.Request) {
+func (s *Server) handleGetPreferences(w http.ResponseWriter, _ *http.Request) {
client := s.getOrCreateConfigClient()
prefs, err := loadPreferencesWithClient(client)
if err != nil {
diff --git a/cli/src/internal/dashboard/mode.go b/cli/src/internal/dashboard/mode.go
index a0b01d4bd..00a3ad4cb 100644
--- a/cli/src/internal/dashboard/mode.go
+++ b/cli/src/internal/dashboard/mode.go
@@ -25,7 +25,7 @@ type ModeResponse struct {
}
// handleGetMode returns the current log source mode.
-func (s *Server) handleGetMode(w http.ResponseWriter, r *http.Request) {
+func (s *Server) handleGetMode(w http.ResponseWriter, _ *http.Request) {
// Get current mode from server state
s.modeMu.RLock()
currentMode := s.currentMode
diff --git a/cli/src/internal/dashboard/server_handlers.go b/cli/src/internal/dashboard/server_handlers.go
index 4eef07828..e68effd93 100644
--- a/cli/src/internal/dashboard/server_handlers.go
+++ b/cli/src/internal/dashboard/server_handlers.go
@@ -2,6 +2,7 @@ package dashboard
import (
"compress/gzip"
+ "context"
"encoding/json"
"fmt"
"html"
@@ -46,7 +47,7 @@ func (s *Server) handleGetEnvironment(w http.ResponseWriter, r *http.Request) {
// Detect if running in VS Code (desktop) vs browser-based Codespace
// In VS Code desktop (including VS Code connected to Codespace), localhost URLs work natively
// Only in browser-based Codespace do we need to transform localhost URLs
- isVsCodeDesktop := runningOnVsCodeDesktop()
+ isVsCodeDesktop := runningOnVsCodeDesktop(r.Context())
// Get Azure environment name if available
azureEnvName := os.Getenv("AZURE_ENV_NAME")
@@ -70,7 +71,7 @@ func (s *Server) handleGetEnvironment(w http.ResponseWriter, r *http.Request) {
// In browser-based Codespace, 'code --status' returns:
// "The --status argument is not yet supported in browsers."
// Reference: azure/azure-dev cli/azd/cmd/auth_login.go runningOnCodespacesBrowser
-func runningOnVsCodeDesktop() bool {
+func runningOnVsCodeDesktop(ctx context.Context) bool {
// Check if running in Codespace first - if not, no need to check
if os.Getenv("CODESPACES") != "true" {
return false
@@ -78,7 +79,7 @@ func runningOnVsCodeDesktop() bool {
// Try to run 'code --status' to detect VS Code desktop vs browser
// This command returns specific output in browser-based VS Code
- cmd := exec.Command("code", "--status")
+ cmd := exec.CommandContext(ctx, "code", "--status")
output, err := cmd.Output()
if err != nil {
// If code command fails or doesn't exist, we're likely in browser Codespace
@@ -246,8 +247,8 @@ func (s *Server) handleFallback(w http.ResponseWriter, r *http.Request) {
switch svc.Status {
case "ready":
statusClass = "ready"
- case "error":
- statusClass = "error"
+ case StatusError:
+ statusClass = StatusError
}
// Escape all user-controllable values to prevent XSS
diff --git a/cli/src/internal/dashboard/websocket_concurrency_test.go b/cli/src/internal/dashboard/websocket_concurrency_test.go
index 04ca25150..3eb68f430 100644
--- a/cli/src/internal/dashboard/websocket_concurrency_test.go
+++ b/cli/src/internal/dashboard/websocket_concurrency_test.go
@@ -310,7 +310,7 @@ func TestServer_ConcurrentBroadcasts(t *testing.T) {
}
// Start reading messages in background
- go func(idx int, w *websocket.Conn) {
+ go func(_ int, w *websocket.Conn) {
for {
var msg map[string]interface{}
if err := wsjson.Read(ctx, w, &msg); err != nil {
diff --git a/cli/src/internal/dashboard/websocket_fixes_test.go b/cli/src/internal/dashboard/websocket_fixes_test.go
index 567b16765..b21b2fec8 100644
--- a/cli/src/internal/dashboard/websocket_fixes_test.go
+++ b/cli/src/internal/dashboard/websocket_fixes_test.go
@@ -604,7 +604,7 @@ func TestConcurrentBroadcasts(t *testing.T) {
_ = wsjson.Read(ctx, ws, &msg)
// Read in background
- go func(w *websocket.Conn, idx int) {
+ go func(w *websocket.Conn, _ int) {
for {
var m map[string]interface{}
if err := wsjson.Read(ctx, w, &m); err != nil {
diff --git a/cli/src/internal/detector/detector_dotnet.go b/cli/src/internal/detector/detector_dotnet.go
index 614cec452..8beeb9fd7 100644
--- a/cli/src/internal/detector/detector_dotnet.go
+++ b/cli/src/internal/detector/detector_dotnet.go
@@ -23,13 +23,13 @@ func FindDotnetProjects(rootDir string) ([]types.DotnetProject, error) {
err = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
- return nil
+ return nil //nolint:nilerr // file not found is expected, means this detector doesn't match
}
// Ensure we don't traverse outside rootDir
absPath, err := filepath.Abs(path)
if err != nil {
- return nil
+ return nil //nolint:nilerr // file not found is expected, means this detector doesn't match
}
relPath, err := filepath.Rel(rootDir, absPath)
if err != nil || strings.HasPrefix(relPath, "..") {
@@ -93,7 +93,7 @@ func FindAppHost(rootDir string) (*types.AspireProject, error) {
// Ensure we don't traverse outside rootDir
absPath, err := filepath.Abs(path)
if err != nil {
- return nil
+ return nil //nolint:nilerr // file not found is expected, means this detector doesn't match
}
relPath, err := filepath.Rel(rootDir, absPath)
if err != nil || strings.HasPrefix(relPath, "..") {
@@ -113,7 +113,7 @@ func FindAppHost(rootDir string) (*types.AspireProject, error) {
dir := filepath.Dir(path)
matches, err := filepath.Glob(filepath.Join(dir, "*.csproj"))
if err != nil {
- return nil // Skip on error
+ return nil //nolint:nilerr // file not found is expected, means this detector doesn't match
}
if len(matches) > 0 {
aspireProject = &types.AspireProject{
diff --git a/cli/src/internal/detector/detector_functions.go b/cli/src/internal/detector/detector_functions.go
index c9df8bd1e..b137a0a8b 100644
--- a/cli/src/internal/detector/detector_functions.go
+++ b/cli/src/internal/detector/detector_functions.go
@@ -11,6 +11,12 @@ import (
types "github.com/jongio/azd-core/projecttype"
)
+const (
+ langPython = "python"
+ langDotnet = "dotnet"
+ langJava = "java"
+)
+
// FindFunctionApps searches for Azure Functions projects (all variants including Logic Apps).
// Only searches within rootDir and does not traverse outside it.
// Returns all discovered Function Apps with their variant and language detected.
@@ -34,7 +40,7 @@ func FindFunctionApps(rootDir string) ([]types.FunctionAppProject, error) {
// Ensure we don't traverse outside rootDir
absPath, err := filepath.Abs(path)
if err != nil {
- return nil
+ return nil //nolint:nilerr // file not found is expected, means this detector doesn't match
}
relPath, err := filepath.Rel(rootDir, absPath)
if err != nil || strings.HasPrefix(relPath, "..") {
@@ -99,10 +105,10 @@ func detectFunctionsVariantForDiscovery(dir string) string {
// Check for Python Functions (function_app.py or requirements.txt + function.json)
if fileExistsInDir(dir, "function_app.py") {
- return "python"
+ return langPython
}
if fileExistsInDir(dir, "requirements.txt") && hasFunctionJsonInDir(dir) {
- return "python"
+ return langPython
}
// Check for .NET Functions (.csproj with Azure Functions references)
@@ -111,7 +117,7 @@ func detectFunctionsVariantForDiscovery(dir string) string {
for _, csprojFile := range csprojFiles {
if containsTextInFile(csprojFile, "Microsoft.Azure.Functions.Worker") ||
containsTextInFile(csprojFile, "Microsoft.NET.Sdk.Functions") {
- return "dotnet"
+ return langDotnet
}
}
}
@@ -119,19 +125,19 @@ func detectFunctionsVariantForDiscovery(dir string) string {
// Check for Java Functions (pom.xml or build.gradle with Azure Functions plugin)
if fileExistsInDir(dir, "pom.xml") {
if containsTextInFile(filepath.Join(dir, "pom.xml"), "azure-functions-maven-plugin") {
- return "java"
+ return langJava
}
}
if fileExistsInDir(dir, "build.gradle") {
buildGradle := filepath.Join(dir, "build.gradle")
if containsTextInFile(buildGradle, "azurefunctions") || containsTextInFile(buildGradle, "azure-functions") {
- return "java"
+ return langJava
}
}
if fileExistsInDir(dir, "build.gradle.kts") {
buildGradleKts := filepath.Join(dir, "build.gradle.kts")
if containsTextInFile(buildGradleKts, "azurefunctions") || containsTextInFile(buildGradleKts, "azure-functions") {
- return "java"
+ return langJava
}
}
@@ -148,11 +154,11 @@ func detectFunctionsLanguageForDiscovery(variant string, dir string) string {
return "TypeScript"
}
return "JavaScript"
- case "python":
+ case langPython:
return "Python"
- case "dotnet":
+ case langDotnet:
return "C#"
- case "java":
+ case langJava:
return "Java"
default:
return ""
diff --git a/cli/src/internal/detector/detector_node.go b/cli/src/internal/detector/detector_node.go
index 9bedef27b..3748b2db8 100644
--- a/cli/src/internal/detector/detector_node.go
+++ b/cli/src/internal/detector/detector_node.go
@@ -11,6 +11,8 @@ import (
"github.com/jongio/azd-core/security"
)
+const pkgNPM = "npm"
+
// FindNodeProjects searches for package.json files.
// Only searches within rootDir and does not traverse outside it.
// Detects npm/yarn/pnpm workspace configurations and marks workspace relationships.
@@ -30,13 +32,13 @@ func FindNodeProjects(rootDir string) ([]types.NodeProject, error) {
// First pass: find all package.json files and identify workspace roots
err = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
- return nil
+ return nil //nolint:nilerr // file not found is expected, means this detector doesn't match
}
// Ensure we don't traverse outside rootDir
absPath, err := filepath.Abs(path)
if err != nil {
- return nil
+ return nil //nolint:nilerr // file not found is expected, means this detector doesn't match
}
relPath, err := filepath.Rel(rootDir, absPath)
if err != nil || strings.HasPrefix(relPath, "..") {
@@ -153,11 +155,11 @@ func DetectNodePackageManagerWithBoundaryAndSource(projectDir string, boundaryDi
return PackageManagerInfo{Name: "yarn", Source: "yarn.lock"}
}
if _, err := os.Stat(filepath.Join(absDir, "package-lock.json")); err == nil {
- return PackageManagerInfo{Name: "npm", Source: "package-lock.json"}
+ return PackageManagerInfo{Name: pkgNPM, Source: "package-lock.json"}
}
// Default to npm if no lock files found
- return PackageManagerInfo{Name: "npm", Source: "package.json"}
+ return PackageManagerInfo{Name: pkgNPM, Source: "package.json"}
}
// GetPackageManagerFromPackageJSON reads package.json and extracts the packageManager field.
@@ -202,7 +204,7 @@ func GetPackageManagerFromPackageJSON(projectDir string) string {
// Validate it's a supported package manager
switch pkgMgrName {
- case "npm", "yarn", "pnpm":
+ case pkgNPM, "yarn", "pnpm":
return pkgMgrName
default:
// Unsupported package manager, fall back to lock file detection
diff --git a/cli/src/internal/detector/detector_python.go b/cli/src/internal/detector/detector_python.go
index d99860e4d..5c7dcc728 100644
--- a/cli/src/internal/detector/detector_python.go
+++ b/cli/src/internal/detector/detector_python.go
@@ -48,7 +48,7 @@ func FindPythonProjects(rootDir string) ([]types.PythonProject, error) {
// Ensure we don't traverse outside rootDir
absPath, err := filepath.Abs(path)
if err != nil {
- return nil
+ return nil //nolint:nilerr // file not found is expected, means this detector doesn't match
}
relPath, err := filepath.Rel(rootDir, absPath)
if err != nil || strings.HasPrefix(relPath, "..") {
diff --git a/cli/src/internal/docker/exec.go b/cli/src/internal/docker/exec.go
index ffa921693..24fac7c1d 100644
--- a/cli/src/internal/docker/exec.go
+++ b/cli/src/internal/docker/exec.go
@@ -2,6 +2,7 @@ package docker
import (
"bytes"
+ "context"
"encoding/json"
"errors"
"fmt"
@@ -23,7 +24,7 @@ func NewClient() *ExecClient {
// IsAvailable checks if Docker is installed and running.
func (c *ExecClient) IsAvailable() bool {
- cmd := exec.Command("docker", "info")
+ cmd := exec.CommandContext(context.Background(), "docker", "info")
err := cmd.Run()
return err == nil
}
@@ -34,7 +35,7 @@ func (c *ExecClient) Pull(image string) error {
return err
}
- cmd := exec.Command("docker", "pull", image)
+ cmd := exec.CommandContext(context.Background(), "docker", "pull", image)
var stderr bytes.Buffer
cmd.Stderr = &stderr
@@ -56,7 +57,7 @@ func (c *ExecClient) Run(config ContainerConfig) (string, error) {
}
args := buildRunArgs(config)
- cmd := exec.Command("docker", args...)
+ cmd := exec.CommandContext(context.Background(), "docker", args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
@@ -133,7 +134,7 @@ func (c *ExecClient) Stop(containerID string, timeoutSeconds int) error {
}
args := []string{"stop", "-t", fmt.Sprintf("%d", timeoutSeconds), containerID}
- cmd := exec.Command("docker", args...)
+ cmd := exec.CommandContext(context.Background(), "docker", args...)
var stderr bytes.Buffer
cmd.Stderr = &stderr
@@ -155,7 +156,7 @@ func (c *ExecClient) Start(containerID string) error {
return errors.New(errEmptyContainerID)
}
- cmd := exec.Command("docker", "start", containerID)
+ cmd := exec.CommandContext(context.Background(), "docker", "start", containerID)
var stderr bytes.Buffer
cmd.Stderr = &stderr
@@ -176,7 +177,7 @@ func (c *ExecClient) Remove(containerID string) error {
return errors.New(errEmptyContainerID)
}
- cmd := exec.Command("docker", "rm", containerID)
+ cmd := exec.CommandContext(context.Background(), "docker", "rm", containerID)
var stderr bytes.Buffer
cmd.Stderr = &stderr
@@ -199,7 +200,7 @@ func (c *ExecClient) Logs(containerID string) (io.ReadCloser, error) {
return nil, errors.New(errEmptyContainerID)
}
- cmd := exec.Command("docker", "logs", "-f", containerID)
+ cmd := exec.CommandContext(context.Background(), "docker", "logs", "-f", containerID)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("failed to get stdout pipe: %w", err)
@@ -261,6 +262,7 @@ func (c *combinedReadCloser) Read(p []byte) (int, error) {
return c.reader.Read(p)
}
+// Close shuts down the combined reader by closing the pipe and terminating the underlying command.
func (c *combinedReadCloser) Close() error {
// Close the pipe writer to unblock any pending reads
if c.pipe != nil {
@@ -291,7 +293,7 @@ func (c *ExecClient) Inspect(containerID string) (*Container, error) {
return nil, errors.New(errEmptyContainerID)
}
- cmd := exec.Command("docker", "inspect", containerID)
+ cmd := exec.CommandContext(context.Background(), "docker", "inspect", containerID)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
@@ -331,7 +333,7 @@ func (c *ExecClient) IsRunning(containerID string) bool {
return false
}
- cmd := exec.Command("docker", "inspect", "-f", "{{.State.Running}}", containerID)
+ cmd := exec.CommandContext(context.Background(), "docker", "inspect", "-f", "{{.State.Running}}", containerID)
var stdout bytes.Buffer
cmd.Stdout = &stdout
@@ -364,7 +366,7 @@ func (c *ExecClient) Exec(containerName string, command []string) (int, string,
}
args := append([]string{"exec", containerName}, command...)
- cmd := exec.Command("docker", args...)
+ cmd := exec.CommandContext(context.Background(), "docker", args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
diff --git a/cli/src/internal/executor/executor.go b/cli/src/internal/executor/executor.go
index 4ced2ded0..2cef95d07 100644
--- a/cli/src/internal/executor/executor.go
+++ b/cli/src/internal/executor/executor.go
@@ -108,10 +108,12 @@ func StartCommandWithOutputMonitoring(ctx context.Context, name string, args []s
cliout.Newline()
// Wait for the command to complete (this blocks until the process exits or is killed)
- if err := cmd.Wait(); err != nil {
- // Ignore exit errors from Ctrl+C
+ waitErr := cmd.Wait()
+ if waitErr == nil {
return nil
}
-
- return nil
+ if ctx.Err() != nil {
+ return nil //nolint:nilerr // context cancellation from Ctrl+C is expected when stopping the command
+ }
+ return waitErr
}
diff --git a/cli/src/internal/healthcheck/checker.go b/cli/src/internal/healthcheck/checker.go
index 9c8318887..127d8d4d9 100644
--- a/cli/src/internal/healthcheck/checker.go
+++ b/cli/src/internal/healthcheck/checker.go
@@ -23,6 +23,11 @@ import (
"golang.org/x/time/rate"
)
+const (
+ statusError = "error"
+ statusCompleted = "completed"
+)
+
// HealthChecker performs individual health checks with circuit breaker and rate limiting.
type HealthChecker struct {
timeout time.Duration
@@ -68,12 +73,15 @@ func (c *HealthChecker) getOrCreateCircuitBreaker(serviceName string) *gobreaker
Interval: c.breakerTimeout,
Timeout: c.breakerTimeout,
ReadyToTrip: func(counts gobreaker.Counts) bool {
- // Validate positive breakerFailures to prevent underflow/overflow
- if c.breakerFailures < 0 {
+ // Validate breakerFailures to prevent underflow/overflow
+ if c.breakerFailures < 0 || int64(c.breakerFailures) > int64(^uint32(0)) {
+ return false
+ }
+ if counts.Requests == 0 {
return false
}
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
- return counts.Requests >= uint32(c.breakerFailures) && failureRatio >= 0.6
+ return counts.Requests >= uint32(c.breakerFailures) && failureRatio >= 0.6 //nolint:gosec // G115 - breakerFailures bounds checked above
},
OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
log.Info().
@@ -783,7 +791,7 @@ func (c *HealthChecker) parseHealthResponseBody(body []byte, result *httpHealthC
result.Status = HealthStatusHealthy
case "degraded", "warning":
result.Status = HealthStatusDegraded
- case "unhealthy", "down", "error":
+ case "unhealthy", "down", statusError:
result.Status = HealthStatusUnhealthy
}
}
@@ -791,7 +799,7 @@ func (c *HealthChecker) parseHealthResponseBody(body []byte, result *httpHealthC
}
// performProcessHealthCheck handles health checks for process-type services.
-func (c *HealthChecker) performProcessHealthCheck(ctx context.Context, svc serviceInfo, isInStartupGracePeriod bool) HealthCheckResult {
+func (c *HealthChecker) performProcessHealthCheck(_ context.Context, svc serviceInfo, isInStartupGracePeriod bool) HealthCheckResult {
result := HealthCheckResult{
ServiceName: svc.Name,
Timestamp: time.Now(),
@@ -873,7 +881,7 @@ func (c *HealthChecker) performBuildTaskHealthCheck(svc serviceInfo, isInStartup
if svc.Mode == ServiceModeBuild {
result.Details = map[string]interface{}{"state": "built", "exitCode": 0}
} else {
- result.Details = map[string]interface{}{"state": "completed", "exitCode": 0}
+ result.Details = map[string]interface{}{"state": statusCompleted, "exitCode": 0}
}
} else {
result.Status = HealthStatusUnhealthy
@@ -888,7 +896,7 @@ func (c *HealthChecker) performBuildTaskHealthCheck(svc serviceInfo, isInStartup
if svc.Mode == ServiceModeBuild {
result.Details = map[string]interface{}{"state": "built", "note": "exit code not captured"}
} else {
- result.Details = map[string]interface{}{"state": "completed", "note": "exit code not captured"}
+ result.Details = map[string]interface{}{"state": statusCompleted, "note": "exit code not captured"}
}
return result
}
@@ -916,7 +924,7 @@ func (c *HealthChecker) performOutputHealthCheck(svc serviceInfo, isInStartupGra
if svc.ExitCode != nil {
if *svc.ExitCode == 0 {
result.Status = HealthStatusHealthy
- result.Details["state"] = "completed"
+ result.Details["state"] = statusCompleted
return result
}
result.Status = HealthStatusUnhealthy
@@ -1004,7 +1012,7 @@ func suggestTCPErrorAction(err error, port int) string {
}
// suggestProcessErrorAction provides actionable suggestions for process check errors.
-func suggestProcessErrorAction(pid int, isRunning bool, mode string) string {
+func suggestProcessErrorAction(pid int, isRunning bool, _ string) string {
if !isRunning {
return fmt.Sprintf("Process %d not running. Check service logs and verify start command.", pid)
}
@@ -1051,7 +1059,7 @@ func parseErrorDetailsFromBody(body []byte) string {
var jsonData map[string]interface{}
if err := json.Unmarshal(body, &jsonData); err == nil {
// Look for common error fields
- for _, key := range []string{"error", "message", "detail", "details", "error_description"} {
+ for _, key := range []string{statusError, "message", "detail", "details", "error_description"} {
if val, ok := jsonData[key]; ok {
if str, ok := val.(string); ok && str != "" {
return str
diff --git a/cli/src/internal/healthcheck/metrics_test.go b/cli/src/internal/healthcheck/metrics_test.go
index 0f72b5e54..98f861777 100644
--- a/cli/src/internal/healthcheck/metrics_test.go
+++ b/cli/src/internal/healthcheck/metrics_test.go
@@ -1,6 +1,7 @@
package healthcheck
import (
+ "context"
"net/http"
"testing"
"time"
@@ -196,7 +197,7 @@ func TestCreateMetricsServer_HealthEndpoint(t *testing.T) {
server := CreateMetricsServer(0) // Use port 0 for testing
// Create a test request to /health
- req, err := http.NewRequest("GET", "/health", nil)
+ req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/health", nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
diff --git a/cli/src/internal/healthcheck/monitor.go b/cli/src/internal/healthcheck/monitor.go
index cb86e9a73..bfc20041c 100644
--- a/cli/src/internal/healthcheck/monitor.go
+++ b/cli/src/internal/healthcheck/monitor.go
@@ -23,6 +23,12 @@ import (
"gopkg.in/yaml.v3"
)
+const (
+ statusRunning = "running"
+ statusBuilt = "built"
+ statusStarting = "starting"
+)
+
var (
// metricsEnabled controls whether Prometheus metrics are recorded.
// Uses atomic.Bool for thread-safe concurrent access from multiple goroutines.
@@ -167,8 +173,12 @@ func (m *HealthMonitor) Check(ctx context.Context, serviceFilter []string) (*Hea
if m.cache != nil {
if cached, found := m.cache.Get(cacheKey); found {
+ report, ok := cached.(*HealthReport)
+ if !ok {
+ return nil, fmt.Errorf("cached health report for %q has unexpected type %T", cacheKey, cached)
+ }
log.Debug().Str("key", cacheKey).Msg("Returning cached health report")
- return cached.(*HealthReport), nil
+ return report, nil
}
}
@@ -207,7 +217,7 @@ func (m *HealthMonitor) Check(ctx context.Context, serviceFilter []string) (*Hea
ServiceName: svc.Name,
Timestamp: time.Now(),
Status: HealthStatusUnknown,
- Error: "context cancelled before check started",
+ Error: "context canceled before check started",
}}
return
}
@@ -222,7 +232,7 @@ func (m *HealthMonitor) Check(ctx context.Context, serviceFilter []string) (*Hea
ServiceName: svc.Name,
Timestamp: time.Now(),
Status: HealthStatusUnknown,
- Error: "context cancelled",
+ Error: "context canceled",
}}
return
}
@@ -237,7 +247,7 @@ func (m *HealthMonitor) Check(ctx context.Context, serviceFilter []string) (*Hea
ServiceName: svc.Name,
Timestamp: time.Now(),
Status: HealthStatusUnknown,
- Error: "context cancelled",
+ Error: "context canceled",
}}
return
}
@@ -344,7 +354,7 @@ func (m *HealthMonitor) buildServiceList(azureYaml *service.AzureYaml, registere
}
}
- var services []serviceInfo
+ services := make([]serviceInfo, 0, len(serviceMap))
for _, svc := range serviceMap {
services = append(services, svc)
}
@@ -445,8 +455,8 @@ func (m *HealthMonitor) trackFailure(result *HealthCheckResult) {
now := time.Now()
m.lastSuccessTime[serviceName] = now
result.LastSuccessTime = &now
- default:
- // For other statuses (degraded, starting, unknown), include current count without incrementing
+ case HealthStatusDegraded, HealthStatusStarting, HealthStatusUnknown:
+ // For non-terminal states, include current count without incrementing.
if count, exists := m.failureCount[serviceName]; exists {
result.ConsecutiveFailures = count
}
@@ -466,7 +476,7 @@ func (m *HealthMonitor) updateRegistry(results []HealthCheckResult) {
continue
}
- if exists && currentEntry.Status == "running" && result.Status == HealthStatusStarting {
+ if exists && currentEntry.Status == statusRunning && result.Status == HealthStatusStarting {
log.Debug().
Str("service", result.ServiceName).
Msg("Keeping running status during health check grace period")
@@ -480,36 +490,38 @@ func (m *HealthMonitor) updateRegistry(results []HealthCheckResult) {
case HealthStatusHealthy:
if details, ok := result.Details["state"].(string); ok {
switch details {
- case "built":
- status = "built"
- case "completed":
- status = "completed"
- case "building", "running":
- status = "running"
+ case statusBuilt:
+ status = statusBuilt
+ case statusCompleted:
+ status = statusCompleted
+ case "building", statusRunning:
+ status = statusRunning
case "failed":
- status = "error"
+ status = statusError
default:
- status = "running"
+ status = statusRunning
}
} else {
- status = "running"
+ status = statusRunning
}
case HealthStatusUnhealthy:
- status = "error"
+ status = statusError
case HealthStatusStarting:
- status = "starting"
- default:
- status = "running"
+ status = statusStarting
+ case HealthStatusDegraded, HealthStatusUnknown:
+ status = statusRunning
}
} else {
- status = "running"
+ status = statusRunning
switch result.Status {
case HealthStatusUnhealthy:
- status = "error"
+ status = statusError
case HealthStatusDegraded:
status = "degraded"
case HealthStatusStarting:
- status = "starting"
+ status = statusStarting
+ case HealthStatusHealthy, HealthStatusUnknown:
+ status = statusRunning
}
}
diff --git a/cli/src/internal/healthcheck/types.go b/cli/src/internal/healthcheck/types.go
index 9491d5dc7..8f4517f5c 100644
--- a/cli/src/internal/healthcheck/types.go
+++ b/cli/src/internal/healthcheck/types.go
@@ -9,16 +9,34 @@ import (
core "github.com/jongio/azd-core/healthcheck"
)
-// Re-export public types from azd-core/healthcheck.
+// HealthStatus re-exports the core health status enumeration for monitored services.
type HealthStatus = core.HealthStatus
+
+// HealthCheckType re-exports the core health check type enumeration.
type HealthCheckType = core.HealthCheckType
+
+// HealthCheckResult re-exports the core health check result for an individual monitored service.
type HealthCheckResult = core.HealthCheckResult
+
+// HealthReport re-exports the core health report returned by monitoring operations.
type HealthReport = core.HealthReport
+
+// HealthSummary re-exports the aggregate core health summary across monitored services.
type HealthSummary = core.HealthSummary
+
+// MonitorConfig re-exports the core monitor configuration used to set up health monitoring.
type MonitorConfig = core.MonitorConfig
+
+// ServiceInfo re-exports the core service metadata used during health checks.
type ServiceInfo = core.ServiceInfo
+
+// HealthCheckConfig re-exports the core probe configuration for a single health check.
type HealthCheckConfig = core.HealthCheckConfig
+
+// HealthProfile re-exports the core named health profile definition loaded from configuration.
type HealthProfile = core.HealthProfile
+
+// HealthProfiles re-exports the collection of named core health profiles.
type HealthProfiles = core.HealthProfiles
// Local aliases for backward compatibility with unexported references.
diff --git a/cli/src/internal/installer/error_formatting_test.go b/cli/src/internal/installer/error_formatting_test.go
index fb138d49c..e11d41e06 100644
--- a/cli/src/internal/installer/error_formatting_test.go
+++ b/cli/src/internal/installer/error_formatting_test.go
@@ -1,6 +1,7 @@
package installer
import (
+ "context"
"fmt"
"os/exec"
"strings"
@@ -71,7 +72,7 @@ func TestFormatNodeInstallError(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a mock command that will fail
- cmd := exec.Command("nonexistent-command-xyz-123")
+ cmd := exec.CommandContext(context.Background(), "nonexistent-command-xyz-123")
cmd.Args = []string{tt.packageManager, "install"}
// Create a mock error - we can't create ExitError directly,
@@ -129,7 +130,7 @@ func TestFormatPythonInstallError(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- cmd := exec.Command("nonexistent-command-xyz-123")
+ cmd := exec.CommandContext(context.Background(), "nonexistent-command-xyz-123")
cmd.Args = []string{tt.tool}
cmdErr := fmt.Errorf("exit status %d", tt.exitCode)
@@ -223,7 +224,7 @@ func TestFormatCommand(t *testing.T) {
}{
{
name: "normal_command",
- cmd: exec.Command("pnpm", "install", "--prefer-offline"),
+ cmd: exec.CommandContext(context.Background(), "pnpm", "install", "--prefer-offline"),
want: "pnpm install --prefer-offline",
},
{
diff --git a/cli/src/internal/installer/installer.go b/cli/src/internal/installer/installer.go
index 1f49b4353..a7fba5142 100644
--- a/cli/src/internal/installer/installer.go
+++ b/cli/src/internal/installer/installer.go
@@ -3,6 +3,7 @@ package installer
import (
"bytes"
+ "context"
"encoding/json"
"fmt"
"io"
@@ -86,9 +87,9 @@ func installNodeDependenciesWithWriter(project types.NodeProject, progressWriter
if runtime.GOOS == "windows" {
// Use cmd.exe /c to properly invoke .cmd files
cmdArgs := append([]string{"/c", project.PackageManager}, args...)
- cmd = exec.Command("cmd.exe", cmdArgs...)
+ cmd = exec.CommandContext(context.Background(), "cmd.exe", cmdArgs...)
} else {
- cmd = exec.Command(project.PackageManager, args...)
+ cmd = exec.CommandContext(context.Background(), project.PackageManager, args...)
}
cmd.Dir = project.Dir
@@ -148,7 +149,7 @@ func restoreDotnetProjectWithWriter(project types.DotnetProject, progressWriter
// Run restore with streaming output
dir := filepath.Dir(project.Path)
- cmd := exec.Command("dotnet", "restore", project.Path)
+ cmd := exec.CommandContext(context.Background(), "dotnet", "restore", project.Path)
cmd.Dir = dir
// Capture stderr for error reporting
@@ -213,7 +214,7 @@ func setupWithUv(projectDir string, progressWriter io.Writer) error {
cliout.Item("Installing dependencies into .venv (uv)...")
}
- cmd := exec.Command("uv", "sync", "--no-progress")
+ cmd := exec.CommandContext(context.Background(), "uv", "sync", "--no-progress")
cmd.Dir = projectDir
cmd.Env = os.Environ() // Inherit azd context (AZD_SERVER, AZD_ACCESS_TOKEN, AZURE_*)
@@ -236,7 +237,7 @@ func setupWithUv(projectDir string, progressWriter io.Writer) error {
if !cliout.IsJSON() && progressWriter == nil {
cliout.Item("Creating virtual environment at .venv (uv)...")
}
- venvCmd := exec.Command("uv", "venv")
+ venvCmd := exec.CommandContext(context.Background(), "uv", "venv")
venvCmd.Dir = projectDir
venvCmd.Env = os.Environ() // Inherit azd context (AZD_SERVER, AZD_ACCESS_TOKEN, AZURE_*)
@@ -260,7 +261,7 @@ func setupWithUv(projectDir string, progressWriter io.Writer) error {
if !cliout.IsJSON() && progressWriter == nil {
cliout.Item("Installing dependencies into .venv (uv pip)...")
}
- installCmd := exec.Command("uv", "pip", "install", "-r", "requirements.txt", "--no-progress")
+ installCmd := exec.CommandContext(context.Background(), "uv", "pip", "install", "-r", "requirements.txt", "--no-progress")
installCmd.Dir = projectDir
installCmd.Env = os.Environ() // Inherit azd context (AZD_SERVER, AZD_ACCESS_TOKEN, AZURE_*)
@@ -301,7 +302,7 @@ func setupWithPoetry(projectDir string, progressWriter io.Writer) error {
}
// Check if virtual environment exists
- checkCmd := exec.Command("poetry", "env", "info", "--path")
+ checkCmd := exec.CommandContext(context.Background(), "poetry", "env", "info", "--path")
checkCmd.Dir = projectDir
checkCmd.Env = os.Environ() // Inherit azd context (AZD_SERVER, AZD_ACCESS_TOKEN, AZURE_*)
cmdOutput, err := checkCmd.CombinedOutput()
@@ -319,7 +320,7 @@ func setupWithPoetry(projectDir string, progressWriter io.Writer) error {
}
// Install dependencies (use --no-root to avoid installing the package itself)
- cmd := exec.Command("poetry", "install", "--no-root")
+ cmd := exec.CommandContext(context.Background(), "poetry", "install", "--no-root")
cmd.Dir = projectDir
cmd.Env = os.Environ() // Inherit azd context (AZD_SERVER, AZD_ACCESS_TOKEN, AZURE_*)
@@ -356,7 +357,7 @@ func setupWithPip(projectDir string, progressWriter io.Writer) error {
}
// Create virtual environment
- cmd := exec.Command("python", "-m", "venv", ".venv")
+ cmd := exec.CommandContext(context.Background(), "python", "-m", "venv", ".venv")
cmd.Dir = projectDir
cmd.Env = os.Environ() // Inherit azd context (AZD_SERVER, AZD_ACCESS_TOKEN, AZURE_*)
@@ -396,7 +397,7 @@ func setupWithPip(projectDir string, progressWriter io.Writer) error {
}
// Run pip install with streaming output and optimizations
- pipCmd := exec.Command(pipPath, "install", "-r", "requirements.txt", "--disable-pip-version-check", "--prefer-binary")
+ pipCmd := exec.CommandContext(context.Background(), pipPath, "install", "-r", "requirements.txt", "--disable-pip-version-check", "--prefer-binary")
pipCmd.Dir = projectDir
var stderrBuf bytes.Buffer
@@ -499,7 +500,7 @@ type errorFormatter struct {
}
// formatInstallError creates a detailed error message using ecosystem-specific formatters
-func formatInstallError(tool, projectDir string, cmd *exec.Cmd, cmdErr error, stderr string, formatter errorFormatter) error {
+func formatInstallError(tool, _ string, cmd *exec.Cmd, cmdErr error, stderr string, formatter errorFormatter) error {
// Extract exit code
var exitCode int
if exitErr, ok := cmdErr.(*exec.ExitError); ok {
@@ -541,7 +542,7 @@ func formatInstallError(tool, projectDir string, cmd *exec.Cmd, cmdErr error, st
}
// nodeErrorFormatter provides Node.js-specific error formatting
-func nodeErrorFormatter(packageManager, projectDir string) errorFormatter {
+func nodeErrorFormatter(_ string, projectDir string) errorFormatter {
return errorFormatter{
baseMessage: func(tool string) string {
return fmt.Sprintf("failed to run %s install", tool)
@@ -602,7 +603,7 @@ func formatDotnetRestoreError(projectPath, dir string, cmd *exec.Cmd, cmdErr err
}
// extractErrorDetails extracts the most relevant error lines from stderr
-func extractErrorDetails(stderr, tool string) string {
+func extractErrorDetails(stderr, _ string) string {
if stderr == "" {
return ""
}
@@ -807,10 +808,13 @@ func runWithRetry(cmd *exec.Cmd, stderrBuf *bytes.Buffer, maxRetries int) error
// Calculate exponential backoff delay with bounds checking
// Limit shift to prevent overflow (max 2^5 = 32 seconds)
shiftAmount := attempt - 1
+ if shiftAmount < 0 {
+ shiftAmount = 0
+ }
if shiftAmount > 5 {
shiftAmount = 5
}
- delay := time.Duration(1<
/dev/null | grep ':%d ' || netstat -tlnp 2>/dev/null | grep ':%d '", port, port))
+ cmd = exec.CommandContext(context.Background(), "sh", "-c", fmt.Sprintf("ss -tlnp 2>/dev/null | grep ':%d ' || netstat -tlnp 2>/dev/null | grep ':%d '", port, port))
}
output, err := cmd.Output()
diff --git a/cli/src/internal/runner/aspire_test.go b/cli/src/internal/runner/aspire_test.go
index 2f82e471d..57a53fd12 100644
--- a/cli/src/internal/runner/aspire_test.go
+++ b/cli/src/internal/runner/aspire_test.go
@@ -232,5 +232,5 @@ func (w *testLineWriter) Write(p []byte) (n int, err error) {
}
}
- return n, nil
+ return n, nil //nolint:nilerr // partial line buffering is expected until a full line is available
}
diff --git a/cli/src/internal/runner/runner.go b/cli/src/internal/runner/runner.go
index fef9308aa..3a6aea955 100644
--- a/cli/src/internal/runner/runner.go
+++ b/cli/src/internal/runner/runner.go
@@ -243,13 +243,17 @@ func RunDotnet(ctx context.Context, project types.DotnetProject) error {
// For .sln files, we need to run from the directory
// For .csproj files, we can pass the path directly
args := []string{"run"}
- dir := ""
+ var dir string
if filepath.Ext(project.Path) == ".sln" {
dir = filepath.Dir(project.Path)
} else {
args = append(args, "--project", project.Path)
- dir, _ = os.Getwd()
+ cwd, err := os.Getwd()
+ if err != nil {
+ return fmt.Errorf("failed to get current working directory: %w", err)
+ }
+ dir = cwd
}
return executor.StartCommand(ctx, "dotnet", args, dir)
@@ -270,8 +274,7 @@ func RunFunctionApp(ctx context.Context, project types.FunctionAppProject, port
}
// Variant-specific validation
- switch project.Variant {
- case "logicapps":
+ if project.Variant == "logicapps" {
workflowsPath := filepath.Join(project.Dir, "workflows")
if info, err := os.Stat(workflowsPath); err != nil || !info.IsDir() {
return fmt.Errorf("logic Apps project missing workflows/ directory")
diff --git a/cli/src/internal/service/azure_logs_config.go b/cli/src/internal/service/azure_logs_config.go
index e4266f5fb..e74c39064 100644
--- a/cli/src/internal/service/azure_logs_config.go
+++ b/cli/src/internal/service/azure_logs_config.go
@@ -80,7 +80,9 @@ const (
)
// AzureStatus represents the Azure log streaming status.
-// DEPRECATED: This type is preserved for backward compatibility with old clients.
+//
+// Deprecated: This type is preserved for backward compatibility with old clients.
+//
// New code should use /api/azure/logs/health endpoint instead.
type AzureStatus struct {
Mode LogMode `json:"mode"`
diff --git a/cli/src/internal/service/config.go b/cli/src/internal/service/config.go
index 2dba3033f..acc547116 100644
--- a/cli/src/internal/service/config.go
+++ b/cli/src/internal/service/config.go
@@ -67,7 +67,7 @@ func validateURL(urlStr, fieldName, serviceName string) error {
}
// Protocol enforcement - only http and https allowed
- if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
+ if parsedURL.Scheme != ServiceTypeHTTP && parsedURL.Scheme != "https" {
return fmt.Errorf("%s for service '%s' must use http:// or https://, got %s://", fieldName, serviceName, parsedURL.Scheme)
}
diff --git a/cli/src/internal/service/constants.go b/cli/src/internal/service/constants.go
index e75b1444f..94bc3de99 100644
--- a/cli/src/internal/service/constants.go
+++ b/cli/src/internal/service/constants.go
@@ -57,8 +57,8 @@ const (
// DefaultContextLines is the default number of context lines when not specified.
DefaultContextLines = 3
- // Environment variable prefixes and patterns
- // EnvServiceURLPrefix is the prefix for service URL environment variables (e.g., SERVICE_WEB_URL)
+ // EnvServiceURLPrefix is the prefix for service URL environment variables (for example, SERVICE_WEB_URL).
EnvServiceURLPrefix = "SERVICE_"
+ // EnvServiceURLSuffix is the suffix appended to service URL environment variables.
EnvServiceURLSuffix = "_URL"
)
diff --git a/cli/src/internal/service/container_integration_test.go b/cli/src/internal/service/container_integration_test.go
index 40dcea8ed..b451d6733 100644
--- a/cli/src/internal/service/container_integration_test.go
+++ b/cli/src/internal/service/container_integration_test.go
@@ -3,6 +3,7 @@
package service
import (
+ "context"
"fmt"
"net"
"os/exec"
@@ -20,7 +21,7 @@ import (
func checkDockerAvailable(t *testing.T) {
t.Helper()
- cmd := exec.Command("docker", "version")
+ cmd := exec.CommandContext(context.Background(), "docker", "version")
if err := cmd.Run(); err != nil {
t.Skip("Docker not available, skipping integration tests")
}
@@ -310,7 +311,8 @@ func TestContainerService_Cleanup(t *testing.T) {
// checkTCPPort is a helper function to verify TCP port connectivity.
func checkTCPPort(host string, port int, timeout time.Duration) bool {
addr := fmt.Sprintf("%s:%d", host, port)
- conn, err := net.DialTimeout("tcp", addr, timeout)
+ dialer := net.Dialer{Timeout: timeout}
+ conn, err := dialer.DialContext(context.Background(), "tcp", addr)
if err != nil {
return false
}
diff --git a/cli/src/internal/service/detector.go b/cli/src/internal/service/detector.go
index 2c0b48cc3..316d11e50 100644
--- a/cli/src/internal/service/detector.go
+++ b/cli/src/internal/service/detector.go
@@ -51,9 +51,9 @@ func DetectServiceRuntime(serviceName string, service Service, usedPorts map[int
}
// Determine default health check type based on service configuration
- defaultHealthCheckType := "http"
+ defaultHealthCheckType := ServiceTypeHTTP
if service.IsHealthcheckDisabled() {
- defaultHealthCheckType = "none"
+ defaultHealthCheckType = watchModeNone
} else if service.Healthcheck != nil && service.Healthcheck.Type != "" {
defaultHealthCheckType = service.Healthcheck.Type
}
@@ -160,7 +160,7 @@ func detectContainerRuntime(serviceName string, service Service, usedPorts map[i
// Determine default health check type - TCP for containers (port connectivity)
defaultHealthCheckType := "tcp"
if service.IsHealthcheckDisabled() {
- defaultHealthCheckType = "none"
+ defaultHealthCheckType = watchModeNone
} else if service.Healthcheck != nil && service.Healthcheck.Type != "" {
defaultHealthCheckType = service.Healthcheck.Type
}
@@ -184,7 +184,7 @@ func detectContainerRuntime(serviceName string, service Service, usedPorts map[i
// Should add a dedicated Image field to ServiceRuntime struct for container-based services.
runtime.Command = image
runtime.Language = "container"
- runtime.Framework = "docker"
+ runtime.Framework = packageMgrDocker
// Copy environment variables from service config
for key, value := range service.GetEnvironment() {
@@ -293,13 +293,13 @@ func isBuildCommand(cmd string, language string) bool {
// Language-specific build commands
buildCommands := map[string][]string{
- "TypeScript": {"tsc", "npm run build", "pnpm build", "yarn build", "bun build"},
- "JavaScript": {"npm run build", "pnpm build", "yarn build", "bun build", "webpack", "rollup", "esbuild"},
- "Go": {"go build", "go install"},
- ".NET": {"dotnet build", "dotnet publish"},
- "Rust": {"cargo build"},
- "Java": {"mvn package", "mvn compile", "gradle build", "gradle assemble"},
- "Python": {"python setup.py build", "pip wheel"},
+ langTypeScript: {"tsc", "npm run build", "pnpm build", "yarn build", "bun build"},
+ langNameJavaScript: {"npm run build", "pnpm build", "yarn build", "bun build", "webpack", "rollup", "esbuild"},
+ "Go": {"go build", "go install"},
+ langNameDotNet: {"dotnet build", "dotnet publish"},
+ langNameRust: {"cargo build"},
+ langNameJava: {"mvn package", "mvn compile", "gradle build", "gradle assemble"},
+ langNamePython: {"python setup.py build", "pip wheel"},
}
// Check language-specific build commands
@@ -318,9 +318,9 @@ func isBuildCommand(cmd string, language string) bool {
}
// hasWatchModeIndicators checks project structure for watch mode configuration.
-func hasWatchModeIndicators(projectDir string, language string, packageManager string) bool {
+func hasWatchModeIndicators(projectDir string, language string, _ string) bool {
switch language {
- case "TypeScript", "JavaScript":
+ case langTypeScript, langNameJavaScript:
// Check package.json for watch scripts
return hasWatchScriptInPackageJSON(projectDir)
case "Go":
@@ -328,10 +328,10 @@ func hasWatchModeIndicators(projectDir string, language string, packageManager s
return fileExists(projectDir, "air.toml") ||
fileExists(projectDir, ".air.toml") ||
fileExists(projectDir, "reflex.conf")
- case ".NET":
+ case langNameDotNet:
// .NET watch is typically in command, not config
return false
- case "Python":
+ case langNamePython:
// Check for watchdog or watchfiles in requirements
return containsWatchDependency(projectDir)
default:
diff --git a/cli/src/internal/service/detector_commands.go b/cli/src/internal/service/detector_commands.go
index ba6f175c0..73e504368 100644
--- a/cli/src/internal/service/detector_commands.go
+++ b/cli/src/internal/service/detector_commands.go
@@ -10,6 +10,8 @@ import (
"github.com/jongio/azd-core/security"
)
+const frameworkSpringBoot = "Spring Boot"
+
// buildRunCommand builds the command and arguments to run the service.
//
// Priority:
@@ -60,7 +62,7 @@ func buildFrameworkCommand(runtime *ServiceRuntime, projectDir, runtimeMode stri
// Handle Python frameworks with venv support
pythonFrameworks := map[string]struct{}{
"Django": {}, "FastAPI": {}, "Flask": {},
- "Streamlit": {}, "Gradio": {}, "Python": {},
+ "Streamlit": {}, "Gradio": {}, langNamePython: {},
}
if _, isPython := pythonFrameworks[runtime.Framework]; isPython {
@@ -100,14 +102,14 @@ func buildFrameworkCommand(runtime *ServiceRuntime, projectDir, runtimeMode stri
case "Aspire":
return buildDotNetCommand(runtime, projectDir, runtimeMode, true)
- case "ASP.NET Core", ".NET":
+ case "ASP.NET Core", langNameDotNet:
return buildDotNetCommand(runtime, projectDir, runtimeMode, false)
- case "Spring Boot":
+ case frameworkSpringBoot:
buildJavaCommand(runtime, true)
return nil
- case "Java":
+ case langNameJava:
buildJavaCommand(runtime, false)
return nil
@@ -115,7 +117,7 @@ func buildFrameworkCommand(runtime *ServiceRuntime, projectDir, runtimeMode stri
runtime.Command = "go"
runtime.Args = []string{"run", "."}
- case "Rust":
+ case langNameRust:
runtime.Command = "cargo"
runtime.Args = []string{"run"}
@@ -123,7 +125,7 @@ func buildFrameworkCommand(runtime *ServiceRuntime, projectDir, runtimeMode stri
runtime.Command = "php"
runtime.Args = []string{"artisan", "serve", "--host=0.0.0.0", "--port=" + fmt.Sprintf("%d", runtime.Port)}
- case "PHP":
+ case langNamePHP:
runtime.Command = "php"
runtime.Args = []string{"-S", fmt.Sprintf("0.0.0.0:%d", runtime.Port)}
@@ -136,7 +138,7 @@ func buildFrameworkCommand(runtime *ServiceRuntime, projectDir, runtimeMode stri
// buildDotNetCommand configures a .NET service runtime command.
func buildDotNetCommand(runtime *ServiceRuntime, projectDir, runtimeMode string, isAspire bool) error {
- runtime.Command = "dotnet"
+ runtime.Command = langDotnet
csprojFiles, _ := filepath.Glob(filepath.Join(projectDir, "*.csproj"))
if len(csprojFiles) > 0 {
@@ -195,12 +197,9 @@ func getPythonVenvPath(projectDir string) string {
}
// resolvePythonEntrypoint resolves and validates the Python entrypoint file.
-// Returns the entrypoint filename, with fallback to auto-detection if not provided.
-func resolvePythonEntrypoint(projectDir, entrypoint string) (string, error) {
- appFile := entrypoint
- if appFile == "" {
- appFile = findPythonAppFile(projectDir)
- }
+// Returns the auto-detected entrypoint filename.
+func resolvePythonEntrypoint(projectDir, _ string) (string, error) {
+ appFile := findPythonAppFile(projectDir)
if err := validatePythonEntrypoint(projectDir, appFile); err != nil {
return "", err
}
@@ -254,7 +253,7 @@ func buildPythonDefaultCommand(runtime *ServiceRuntime, projectDir, pythonCmd st
runtime.Args = []string{"-m", "streamlit", "run", appFile + ".py", "--server.port", fmt.Sprintf("%d", runtime.Port)}
return nil
- case "Gradio", "Python":
+ case "Gradio", langNamePython:
appFile, err := resolvePythonEntrypoint(projectDir, "") // Auto-detect
if err != nil {
return fmt.Errorf("%s: %w", runtime.Framework, err)
@@ -278,7 +277,7 @@ func configureHealthCheck(runtime *ServiceRuntime) {
"Aspire": {"/", "Now listening on"},
"Next.js": {"/", "ready on"},
"Django": {"/", "Starting development server"},
- "Spring Boot": {"/actuator/health", "Started"},
+ frameworkSpringBoot: {"/actuator/health", "Started"},
"FastAPI": {"/docs", ""},
}
diff --git a/cli/src/internal/service/detector_frameworks.go b/cli/src/internal/service/detector_frameworks.go
index 659fcbe82..06cc507fe 100644
--- a/cli/src/internal/service/detector_frameworks.go
+++ b/cli/src/internal/service/detector_frameworks.go
@@ -7,6 +7,20 @@ import (
"github.com/jongio/azd-app/cli/src/internal/detector"
)
+const (
+ frameworkDocker = "Docker"
+ packageMgrDocker = "docker"
+ langNameJavaScript = "JavaScript"
+ langTypeScript = "TypeScript"
+ langNamePython = "Python"
+ langNameDotNet = ".NET"
+ langNameJava = "Java"
+ langNameRust = "Rust"
+ langNamePHP = "PHP"
+ watchModeNone = "none"
+ langDotnet = "dotnet"
+)
+
// detectLanguage determines the programming language used by the service.
func detectLanguage(projectDir string, host string) (string, error) {
// Define language detection rules in priority order
@@ -14,32 +28,32 @@ func detectLanguage(projectDir string, host string) (string, error) {
name string
checkFunc func() bool
}{
- {"TypeScript", func() bool {
+ {langTypeScript, func() bool {
return fileExists(projectDir, "package.json") && fileExists(projectDir, "tsconfig.json")
}},
- {"JavaScript", func() bool {
+ {langNameJavaScript, func() bool {
return fileExists(projectDir, "package.json")
}},
- {"Python", func() bool {
+ {langNamePython, func() bool {
return fileExists(projectDir, "requirements.txt") ||
fileExists(projectDir, "pyproject.toml") ||
fileExists(projectDir, "poetry.lock") ||
fileExists(projectDir, "uv.lock")
}},
- {".NET", func() bool {
+ {langNameDotNet, func() bool {
return hasFileWithExt(projectDir, ".csproj") ||
hasFileWithExt(projectDir, ".sln") ||
hasFileWithExt(projectDir, ".fsproj")
}},
- {"Java", func() bool {
+ {langNameJava, func() bool {
return fileExists(projectDir, "pom.xml") ||
fileExists(projectDir, "build.gradle") ||
fileExists(projectDir, "build.gradle.kts")
}},
{"Go", func() bool { return fileExists(projectDir, "go.mod") }},
- {"Rust", func() bool { return fileExists(projectDir, "Cargo.toml") }},
- {"PHP", func() bool { return fileExists(projectDir, "composer.json") }},
- {"Docker", func() bool {
+ {langNameRust, func() bool { return fileExists(projectDir, "Cargo.toml") }},
+ {langNamePHP, func() bool { return fileExists(projectDir, "composer.json") }},
+ {frameworkDocker, func() bool {
return fileExists(projectDir, "Dockerfile") || fileExists(projectDir, "docker-compose.yml")
}},
}
@@ -53,7 +67,7 @@ func detectLanguage(projectDir string, host string) (string, error) {
// Fallback: use host type as hint
if host == "containerapp" || host == "aks" {
- return "Docker", nil
+ return frameworkDocker, nil
}
return "", errCouldNotDetectLanguage(projectDir)
@@ -62,22 +76,22 @@ func detectLanguage(projectDir string, host string) (string, error) {
// detectFrameworkAndPackageManager detects the specific framework and package manager.
func detectFrameworkAndPackageManager(projectDir string, language string) (string, string, error) {
switch language {
- case "TypeScript", "JavaScript":
+ case langTypeScript, langNameJavaScript:
return detectNodeFramework(projectDir)
- case "Python":
+ case langNamePython:
return detectPythonFramework(projectDir)
- case ".NET":
+ case langNameDotNet:
return detectDotNetFramework(projectDir)
- case "Java":
+ case langNameJava:
return detectJavaFramework(projectDir)
case "Go":
return "Go", "go", nil
- case "Rust":
- return "Rust", "cargo", nil
- case "PHP":
+ case langNameRust:
+ return langNameRust, "cargo", nil
+ case langNamePHP:
return detectPHPFramework(projectDir)
- case "Docker":
- return "Docker", "docker", nil
+ case frameworkDocker:
+ return frameworkDocker, packageMgrDocker, nil
default:
return language, "", nil
}
@@ -150,14 +164,14 @@ func detectPythonFramework(projectDir string) (string, string, error) {
}
// Default to generic Python
- return "Python", packageManager, nil
+ return langNamePython, packageManager, nil
}
// detectDotNetFramework detects .NET framework.
func detectDotNetFramework(projectDir string) (string, string, error) {
// Check for Aspire
if fileExists(projectDir, "AppHost.cs") {
- return "Aspire", "dotnet", nil
+ return "Aspire", langDotnet, nil
}
// Check for ASP.NET Core
@@ -166,13 +180,13 @@ func detectDotNetFramework(projectDir string) (string, string, error) {
csprojFiles, _ := filepath.Glob(filepath.Join(projectDir, "*.csproj"))
for _, csprojFile := range csprojFiles {
if containsText(csprojFile, "Microsoft.NET.Sdk.Web") {
- return "ASP.NET Core", "dotnet", nil
+ return "ASP.NET Core", langDotnet, nil
}
}
}
// Default to generic .NET
- return ".NET", "dotnet", nil
+ return langNameDotNet, langDotnet, nil
}
// detectJavaFramework detects Java framework.
@@ -184,21 +198,21 @@ func detectJavaFramework(projectDir string) (string, string, error) {
// Check for Spring Boot in pom.xml
if fileExists(projectDir, "pom.xml") && containsText(filepath.Join(projectDir, "pom.xml"), "spring-boot") {
- return "Spring Boot", packageManager, nil
+ return frameworkSpringBoot, packageManager, nil
}
// Check for frameworks in build.gradle
if fileExists(projectDir, "build.gradle") {
buildGradle := filepath.Join(projectDir, "build.gradle")
if containsText(buildGradle, "spring-boot") {
- return "Spring Boot", packageManager, nil
+ return frameworkSpringBoot, packageManager, nil
}
if containsText(buildGradle, "quarkus") {
return "Quarkus", packageManager, nil
}
}
- return "Java", packageManager, nil
+ return langNameJava, packageManager, nil
}
// detectPHPFramework detects PHP framework.
@@ -207,5 +221,5 @@ func detectPHPFramework(projectDir string) (string, string, error) {
return "Laravel", "composer", nil
}
- return "PHP", "composer", nil
+ return langNamePHP, "composer", nil
}
diff --git a/cli/src/internal/service/detector_helpers.go b/cli/src/internal/service/detector_helpers.go
index 053739693..81b84e045 100644
--- a/cli/src/internal/service/detector_helpers.go
+++ b/cli/src/internal/service/detector_helpers.go
@@ -117,7 +117,7 @@ func normalizeLanguage(language string) string {
case "js", "javascript", "node", "nodejs", "node.js":
return "JavaScript"
case "ts", "typescript":
- return "TypeScript"
+ return langTypeScript
case "py", "python":
return "Python"
case "cs", "csharp", "c#":
diff --git a/cli/src/internal/service/executor.go b/cli/src/internal/service/executor.go
index f618dda71..d358960c5 100644
--- a/cli/src/internal/service/executor.go
+++ b/cli/src/internal/service/executor.go
@@ -53,9 +53,9 @@ func StartService(runtime *ServiceRuntime, env map[string]string, projectDir str
}
// createServiceCommand creates an exec.Cmd for the service.
-func createServiceCommand(runtime *ServiceRuntime, env map[string]string) (*exec.Cmd, error) {
+func createServiceCommand(runtime *ServiceRuntime, env map[string]string) (*exec.Cmd, error) { //nolint:unparam // return value kept for future use/interface conformance
// #nosec G204 -- Command and args come from azure.yaml service configuration, validated by service package
- cmd := exec.Command(runtime.Command, runtime.Args...)
+ cmd := exec.CommandContext(context.Background(), runtime.Command, runtime.Args...)
cmd.Dir = runtime.WorkingDir
// Build environment variable list ensuring azd context is preserved.
@@ -134,7 +134,7 @@ func StopServiceGraceful(process *ServiceProcess, timeout time.Duration) error {
// /T = Kill child processes (tree kill)
// /PID = Target process ID
// #nosec G204 -- PID is from os.Process which is a validated integer
- cmd := exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", process.Process.Pid))
+ cmd := exec.CommandContext(context.Background(), "taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", process.Process.Pid))
if err := cmd.Run(); err != nil {
// taskkill returns error if process already exited, which is fine
slog.Debug("taskkill completed with error (process may have already exited)",
diff --git a/cli/src/internal/service/functions.go b/cli/src/internal/service/functions.go
index b08e6e5e1..ddc28b8fd 100644
--- a/cli/src/internal/service/functions.go
+++ b/cli/src/internal/service/functions.go
@@ -284,7 +284,7 @@ func detectFunctionsLanguage(variant FunctionsVariant, projectDir string) (strin
case FunctionsVariantNodeJS:
// Check if it's TypeScript
if fileExists(projectDir, "tsconfig.json") {
- return "TypeScript", nil
+ return langTypeScript, nil
}
return "JavaScript", nil
diff --git a/cli/src/internal/service/health.go b/cli/src/internal/service/health.go
index 76182740d..e2cb59c0a 100644
--- a/cli/src/internal/service/health.go
+++ b/cli/src/internal/service/health.go
@@ -2,6 +2,7 @@
package service
import (
+ "context"
"errors"
"fmt"
"log/slog"
@@ -54,7 +55,7 @@ func PerformHealthCheck(process *ServiceProcess) error {
var err error
switch config.Type {
- case "http":
+ case ServiceTypeHTTP:
err = HTTPHealthCheck(process.Port, config.Path)
case "tcp":
err = PortHealthCheck(process.Port)
@@ -131,8 +132,14 @@ func HTTPHealthCheck(port int, path string) error {
},
}
+ ctx := context.Background()
+
// Try HEAD request first (lightweight)
- resp, err := client.Head(url)
+ req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
+ if err != nil {
+ return fmt.Errorf("failed to create HTTP HEAD request: %w", err)
+ }
+ resp, err := client.Do(req)
if err == nil {
defer SafeClose(resp.Body, "HEAD response body")
// Accept any 2xx or 3xx status code
@@ -142,7 +149,11 @@ func HTTPHealthCheck(port int, path string) error {
}
// If HEAD fails or returns error code, try GET
- resp, err = client.Get(url)
+ req, err = http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+ if err != nil {
+ return fmt.Errorf("failed to create HTTP GET request: %w", err)
+ }
+ resp, err = client.Do(req)
if err != nil {
return fmt.Errorf("HTTP request failed: %w", err)
}
@@ -159,7 +170,8 @@ func HTTPHealthCheck(port int, path string) error {
// PortHealthCheck verifies that a port is listening.
func PortHealthCheck(port int) error {
address := fmt.Sprintf("localhost:%d", port)
- conn, err := net.DialTimeout("tcp", address, ConnectionTimeout)
+ dialer := net.Dialer{Timeout: ConnectionTimeout}
+ conn, err := dialer.DialContext(context.Background(), "tcp", address)
if err != nil {
return fmt.Errorf("port %d not listening: %w", port, err)
}
@@ -216,7 +228,8 @@ func TryHTTPHealthCheck(port int, path string) bool {
// IsPortListening checks if a port is currently listening.
func IsPortListening(port int) bool {
address := fmt.Sprintf("localhost:%d", port)
- conn, err := net.DialTimeout("tcp", address, PortCheckTimeout)
+ dialer := net.Dialer{Timeout: PortCheckTimeout}
+ conn, err := dialer.DialContext(context.Background(), "tcp", address)
if err != nil {
return false
}
diff --git a/cli/src/internal/service/health_test.go b/cli/src/internal/service/health_test.go
index c4bf96677..d80d6b9b7 100644
--- a/cli/src/internal/service/health_test.go
+++ b/cli/src/internal/service/health_test.go
@@ -1,6 +1,7 @@
package service
import (
+ "context"
"net"
"net/http"
"net/http/httptest"
@@ -333,10 +334,10 @@ func TestPerformHealthCheck_ProcessType(t *testing.T) {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
// On Windows, use ping with count to wait
- cmd = exec.Command("ping", "-n", "10", "127.0.0.1")
+ cmd = exec.CommandContext(context.Background(), "ping", "-n", "10", "127.0.0.1")
} else {
// On Unix, use sleep
- cmd = exec.Command("sleep", "5")
+ cmd = exec.CommandContext(context.Background(), "sleep", "5")
}
if err := cmd.Start(); err != nil {
diff --git a/cli/src/internal/service/health_windows.go b/cli/src/internal/service/health_windows.go
index 8ae8762fe..ea361d0ed 100644
--- a/cli/src/internal/service/health_windows.go
+++ b/cli/src/internal/service/health_windows.go
@@ -40,7 +40,7 @@ func processIsRunning(pid int) error {
ret, _, err := getExitCodeProcess.Call(
uintptr(handle),
- uintptr(unsafe.Pointer(&exitCode)),
+ uintptr(unsafe.Pointer(&exitCode)), //nolint:gosec // G103 - unsafe required for Windows process handle operations
)
if ret == 0 {
return fmt.Errorf("failed to get exit code for process %d: %w", pid, err)
diff --git a/cli/src/internal/service/logbuffer.go b/cli/src/internal/service/logbuffer.go
index 2840a6af8..3b909f8e3 100644
--- a/cli/src/internal/service/logbuffer.go
+++ b/cli/src/internal/service/logbuffer.go
@@ -354,7 +354,9 @@ func (lb *LogBuffer) GetLogsWithContext(level LogLevel, limit int, contextLines
}
// GetErrors is deprecated: use GetLogsWithContext instead.
+//
// Deprecated: This method exists for backward compatibility.
+//
// Parameters:
// - limit: maximum number of error entries to return (0 = no limit)
// - contextLines: number of log lines before and after each error (0-10)
diff --git a/cli/src/internal/service/logmanager.go b/cli/src/internal/service/logmanager.go
index f18eefd77..62bc58333 100644
--- a/cli/src/internal/service/logmanager.go
+++ b/cli/src/internal/service/logmanager.go
@@ -203,7 +203,9 @@ func (lm *LogManager) GetAllLogsWithContext(serviceName string, level LogLevel,
}
// GetAllErrors is deprecated: use GetAllLogsWithContext instead.
+//
// Deprecated: This method exists for backward compatibility.
+//
// Parameters:
// - serviceName: filter to specific service (empty = all services)
// - limit: maximum total errors to return (0 = default 50)
diff --git a/cli/src/internal/service/logmanager_test.go b/cli/src/internal/service/logmanager_test.go
index 7515e9562..d0b2159f0 100644
--- a/cli/src/internal/service/logmanager_test.go
+++ b/cli/src/internal/service/logmanager_test.go
@@ -327,7 +327,7 @@ func TestLogManagerConcurrency(t *testing.T) {
// Test concurrent buffer creation
done := make(chan bool)
for i := 0; i < 10; i++ {
- go func(n int) {
+ go func(_ int) {
_, err := lm.CreateBuffer("concurrent-test", 1000, false)
if err != nil {
t.Errorf("concurrent CreateBuffer failed: %v", err)
diff --git a/cli/src/internal/service/operation_manager.go b/cli/src/internal/service/operation_manager.go
index bb0e8fb2b..26cf28e4b 100644
--- a/cli/src/internal/service/operation_manager.go
+++ b/cli/src/internal/service/operation_manager.go
@@ -26,6 +26,7 @@ const (
// OperationType represents the type of service operation.
type OperationType string
+// OpStart and related constants identify the service operations coordinated by the operation manager.
const (
OpStart OperationType = "start"
OpStop OperationType = "stop"
diff --git a/cli/src/internal/service/port.go b/cli/src/internal/service/port.go
index 18ae2f80c..6955dd607 100644
--- a/cli/src/internal/service/port.go
+++ b/cli/src/internal/service/port.go
@@ -2,6 +2,7 @@
package service
import (
+ "context"
"encoding/json"
"fmt"
"net"
@@ -481,7 +482,8 @@ func findAvailablePort(startPort int, usedPorts map[int]bool) (int, error) {
}
// Try to bind to the port to check if it's available
- listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
+ lc := net.ListenConfig{}
+ listener, err := lc.Listen(context.Background(), "tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err == nil {
if closeErr := listener.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close listener: %v\n", closeErr)
@@ -495,7 +497,8 @@ func findAvailablePort(startPort int, usedPorts map[int]bool) (int, error) {
// IsPortAvailable checks if a port is available.
func IsPortAvailable(port int) bool {
- listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
+ lc := net.ListenConfig{}
+ listener, err := lc.Listen(context.Background(), "tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
return false
}
diff --git a/cli/src/internal/service/types.go b/cli/src/internal/service/types.go
index 510ce2ab7..4fc776c36 100644
--- a/cli/src/internal/service/types.go
+++ b/cli/src/internal/service/types.go
@@ -620,6 +620,7 @@ const (
// LogLevel represents the severity of a log message.
type LogLevel int
+// LogLevelInfo and related constants define the severity levels used for service log entries.
const (
LogLevelInfo LogLevel = iota
LogLevelWarn
@@ -662,10 +663,12 @@ type LogEntryWithContext struct {
}
// ErrorEntry is deprecated: use LogEntryWithContext instead.
+//
// Deprecated: This type alias exists for backward compatibility.
type ErrorEntry = LogEntryWithContext
// ErrorContext is deprecated: use LogContext instead.
+//
// Deprecated: This type alias exists for backward compatibility.
type ErrorContext = LogContext
diff --git a/cli/src/internal/service/venv_integration_test.go b/cli/src/internal/service/venv_integration_test.go
index e150b6537..d7cee7f2d 100644
--- a/cli/src/internal/service/venv_integration_test.go
+++ b/cli/src/internal/service/venv_integration_test.go
@@ -3,6 +3,7 @@
package service_test
import (
+ "context"
"os"
"os/exec"
"path/filepath"
@@ -31,7 +32,7 @@ func TestPythonVenvIntegration(t *testing.T) {
} else if _, err := exec.LookPath("python"); err == nil {
pythonCmd = "python"
// Verify it's actually Python and not a Windows Store stub
- verifyCmd := exec.Command(pythonCmd, "--version")
+ verifyCmd := exec.CommandContext(context.Background(), pythonCmd, "--version")
if err := verifyCmd.Run(); err != nil {
t.Skipf("Python found but not functional: %v", err)
}
@@ -134,7 +135,7 @@ services:
// Create real venv
t.Logf("Creating virtual environment in %s", tmpDir)
- venvCmd := exec.Command(pythonCmd, "-m", "venv", ".venv")
+ venvCmd := exec.CommandContext(context.Background(), pythonCmd, "-m", "venv", ".venv")
venvCmd.Dir = tmpDir
venvCmd.Stdout = os.Stdout
venvCmd.Stderr = os.Stderr
@@ -156,7 +157,7 @@ services:
// Install dependencies
t.Logf("Installing dependencies: %v", tt.dependencies)
installArgs := append([]string{"install", "--quiet"}, tt.dependencies...)
- installCmd := exec.Command(pipPath, installArgs...)
+ installCmd := exec.CommandContext(context.Background(), pipPath, installArgs...)
installCmd.Dir = tmpDir
installCmd.Stdout = os.Stdout
installCmd.Stderr = os.Stderr
@@ -226,7 +227,7 @@ services:
// Verify the module is actually installed in venv
t.Logf("Verifying %s is installed in venv", tt.expectedModule)
venvPython := runtimeInfo.Command
- checkCmd := exec.Command(venvPython, "-c", "import "+tt.expectedModule)
+ checkCmd := exec.CommandContext(context.Background(), venvPython, "-c", "import "+tt.expectedModule)
checkCmd.Dir = tmpDir
if err := checkCmd.Run(); err != nil {
t.Errorf("Module %s not importable from venv Python: %v", tt.expectedModule, err)
@@ -251,7 +252,7 @@ func TestPythonVenvFallback(t *testing.T) {
pythonCmd = "python3"
}
- verifyCmd := exec.Command(pythonCmd, "--version")
+ verifyCmd := exec.CommandContext(context.Background(), pythonCmd, "--version")
if err := verifyCmd.Run(); err != nil {
t.Skipf("Python not available or not functional: %v", err)
}
diff --git a/cli/src/internal/serviceinfo/environment_test.go b/cli/src/internal/serviceinfo/environment_test.go
index ee40651d6..2067aef45 100644
--- a/cli/src/internal/serviceinfo/environment_test.go
+++ b/cli/src/internal/serviceinfo/environment_test.go
@@ -109,7 +109,7 @@ func TestRefreshEnvironmentCache_ConcurrentAccess(t *testing.T) {
// Concurrent refreshes
for i := 0; i < 10; i++ {
wg.Add(1)
- go func(id int) {
+ go func(_ int) {
defer wg.Done()
for j := 0; j < iterations; j++ {
RefreshEnvironmentCache()
@@ -120,7 +120,7 @@ func TestRefreshEnvironmentCache_ConcurrentAccess(t *testing.T) {
// Concurrent reads
for i := 0; i < 10; i++ {
wg.Add(1)
- go func(id int) {
+ go func(_ int) {
defer wg.Done()
for j := 0; j < iterations; j++ {
getAzureEnvironmentValues()
diff --git a/cli/src/internal/serviceinfo/serviceinfo.go b/cli/src/internal/serviceinfo/serviceinfo.go
index c132f4344..ca7145cf0 100644
--- a/cli/src/internal/serviceinfo/serviceinfo.go
+++ b/cli/src/internal/serviceinfo/serviceinfo.go
@@ -398,7 +398,7 @@ func mergeServiceInfo(azureYaml *service.AzureYaml, runningServices []*registry.
}
// Convert map to slice
- var result []*ServiceInfo
+ result := make([]*ServiceInfo, 0, len(serviceMap))
for _, svc := range serviceMap {
result = append(result, svc)
}
diff --git a/cli/src/internal/testing/config_writer.go b/cli/src/internal/testing/config_writer.go
index ceabf5e89..ad23e6553 100644
--- a/cli/src/internal/testing/config_writer.go
+++ b/cli/src/internal/testing/config_writer.go
@@ -11,6 +11,8 @@ import (
"gopkg.in/yaml.v3"
)
+const configTest = "test"
+
// GenerateTestConfigYAML generates a YAML snippet for discovered test configurations.
// Only includes services that were auto-detected (had no config in azure.yaml).
// Returns an empty string if no auto-detected services are found.
@@ -167,7 +169,7 @@ func addTestConfigToServices(root *yaml.Node, autoDetected map[string]string) er
// Check if test config already exists
hasTestConfig := false
for j := 0; j < len(serviceNode.Content)-1; j += 2 {
- if serviceNode.Content[j].Value == "test" {
+ if serviceNode.Content[j].Value == configTest {
hasTestConfig = true
break
}
@@ -181,7 +183,7 @@ func addTestConfigToServices(root *yaml.Node, autoDetected map[string]string) er
testKeyNode := &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
- Value: "test",
+ Value: configTest,
}
testValueNode := &yaml.Node{
diff --git a/cli/src/internal/testing/coverage.go b/cli/src/internal/testing/coverage.go
index d82f7759b..37cc6e0b9 100644
--- a/cli/src/internal/testing/coverage.go
+++ b/cli/src/internal/testing/coverage.go
@@ -14,6 +14,8 @@ import (
"github.com/jongio/azd-core/security"
)
+const formatJSON = "json"
+
// CoverageAggregator collects and merges coverage data from multiple services
type CoverageAggregator struct {
serviceCoverage map[string]*CoverageData
@@ -139,13 +141,13 @@ func (a *CoverageAggregator) GenerateReport(format string) error {
if a.outputDir != "" {
// Ensure output directory exists
- if err := os.MkdirAll(a.outputDir, 0o755); err != nil {
+ if err := os.MkdirAll(a.outputDir, 0o750); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
}
switch strings.ToLower(format) {
- case "json":
+ case formatJSON:
return a.generateJSONReport(aggregate)
case "cobertura", "xml":
return a.generateCoberturaReport(aggregate)
@@ -158,7 +160,7 @@ func (a *CoverageAggregator) GenerateReport(format string) error {
// GenerateAllReports generates all coverage report formats
func (a *CoverageAggregator) GenerateAllReports() error {
- formats := []string{"json", "cobertura", "html"}
+ formats := []string{formatJSON, "cobertura", "html"}
for _, format := range formats {
if err := a.GenerateReport(format); err != nil {
return err
@@ -263,7 +265,7 @@ func (a *CoverageAggregator) generateJSONReport(aggregate *AggregateCoverage) er
return fmt.Errorf("failed to marshal coverage data: %w", err)
}
- if err := os.WriteFile(outputPath, data, 0o644); err != nil {
+ if err := os.WriteFile(outputPath, data, 0o600); err != nil {
return fmt.Errorf("failed to write JSON report: %w", err)
}
@@ -381,7 +383,7 @@ func (a *CoverageAggregator) generateCoberturaReport(aggregate *AggregateCoverag
// Add XML header
xmlData := append([]byte(xml.Header), data...)
- if err := os.WriteFile(outputPath, xmlData, 0o644); err != nil {
+ if err := os.WriteFile(outputPath, xmlData, 0o600); err != nil {
return fmt.Errorf("failed to write Cobertura report: %w", err)
}
@@ -560,7 +562,7 @@ func (a *CoverageAggregator) generateHTMLIndex(aggregate *AggregateCoverage) err