From 723ad0bc76b9c604857d0b8baf80ef4edfdd5c0e Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:17:30 -0700 Subject: [PATCH 1/2] chore: remove AI-generated slop patterns Remove buzzword copy, obvious comments that restate adjacent code, and generic filler text identified by anti-slop review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- web/src/pages/index.astro | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8a6864a..546e8b8 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ That's it. Your script has access to all azd environment variables, Azure creden Automatically detects and runs bash, sh, zsh, PowerShell, pwsh, and cmd scripts based on file extension or shebang. ### 🎯 Script Arguments -Pass arguments to your scripts seamlessly with the `--` separator for clean parameter handling. +Pass arguments to your scripts using the `--` separator for clean parameter handling. ### 🌍 Full Azure Context Access all azd environment variables including subscription, tenant, location, and custom variables. diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro index fb660ba..daf2192 100644 --- a/web/src/pages/index.astro +++ b/web/src/pages/index.astro @@ -61,7 +61,7 @@ const description = "azd exec - Run any script with azd environment and Azure cr Date: Sat, 14 Mar 2026 14:45:33 -0700 Subject: [PATCH 2/2] 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 | 40 ++++------ .github/workflows/codeql.yml | 12 ++- .github/workflows/govulncheck.yml | 38 +++++++++ .github/workflows/pr-build.yml | 12 +-- .github/workflows/release-test.yml | 4 +- .github/workflows/release.yml | 81 ++++++++++++++------ .github/workflows/update-azd-core.yml | 4 +- .github/workflows/website.yml | 24 +++--- README.md | 6 ++ cli/.golangci.yml | 40 ++++++++-- cli/magefile.go | 69 ++++++++++++++++- cli/src/cmd/exec/commands/mcp.go | 11 ++- cli/src/internal/executor/command_builder.go | 2 +- cspell.json | 19 ++++- 16 files changed, 286 insertions(+), 92 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/govulncheck.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..d023a0d --- /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 0000000..bcce2be --- /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 6caacb1..3fd81c6 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,17 +27,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 }} @@ -73,10 +73,10 @@ 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 @@ -97,7 +97,7 @@ jobs: - name: Upload coverage to Codecov if: github.repository == 'jongio/azd-exec' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4 with: file: coverage/coverage.out flags: unittests @@ -119,22 +119,8 @@ jobs: COVERAGE=$(go tool cover -func=../coverage/coverage.out | grep total | awk '{print $3}') echo "**Total Coverage: $COVERAGE**" >> $GITHUB_STEP_SUMMARY - build: - name: Build - runs-on: ubuntu-latest - needs: [preflight, test] - timeout-minutes: 30 - - 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 @@ -149,7 +135,7 @@ jobs: GOOS=darwin GOARCH=arm64 go build -o bin/darwin-arm64/exec ./src/cmd/exec - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: binaries path: cli/bin/ @@ -166,17 +152,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: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.11' @@ -198,7 +184,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 index 26549c3..6f30a59 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -12,6 +12,10 @@ on: schedule: - cron: '0 0 * * 0' # Weekly on Sundays +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: analyze: name: Analyze @@ -29,19 +33,19 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@9792ccaef0455e446c567163589397e8c3ac2e0d # v3 with: languages: ${{ matrix.language }} queries: security-extended,security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@9792ccaef0455e446c567163589397e8c3ac2e0d # v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@9792ccaef0455e446c567163589397e8c3ac2e0d # v3 continue-on-error: true with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml new file mode 100644 index 0000000..dabad49 --- /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 5e7e31b..7d4b70b 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: | const prNumber = '${{ needs.check-permission.outputs.pr_number }}' || context.payload.pull_request.number; @@ -184,12 +184,12 @@ 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 @@ -334,7 +334,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-test.yml b/.github/workflows/release-test.yml index 975af0d..34b9b3a 100644 --- a/.github/workflows/release-test.yml +++ b/.github/workflows/release-test.yml @@ -31,12 +31,12 @@ jobs: 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a9b024..c8f20c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,17 +33,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 }} @@ -85,10 +85,10 @@ 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 @@ -109,7 +109,7 @@ jobs: - name: Upload coverage to Codecov if: github.repository == 'jongio/azd-exec' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4 with: file: coverage/coverage.out flags: unittests @@ -118,22 +118,8 @@ jobs: fail_ci_if_error: false verbose: true - build: - name: Build - runs-on: ubuntu-latest - needs: [preflight, test] - timeout-minutes: 30 - - 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 @@ -148,7 +134,7 @@ jobs: GOOS=darwin GOARCH=arm64 go build -o bin/darwin-arm64/exec ./src/cmd/exec - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: binaries path: cli/bin/ @@ -161,15 +147,16 @@ 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 @@ -311,6 +298,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/update-azd-core.yml b/.github/workflows/update-azd-core.yml index e3f0398..03ab5d5 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 9444a53..d882727 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -32,15 +32,15 @@ jobs: runs-on: ubuntu-latest 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' @@ -71,7 +71,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 @@ -79,7 +79,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 @@ -92,13 +92,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 @@ -130,13 +130,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 }} @@ -159,7 +159,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; @@ -209,7 +209,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 @@ -232,7 +232,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/README.md b/README.md index 546e8b8..2aac1a3 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ Execute any script with full access to your Azure Developer CLI environment vari [![CI](https://github.com/jongio/azd-exec/actions/workflows/ci.yml/badge.svg)](https://github.com/jongio/azd-exec/actions/workflows/ci.yml) [![CodeQL](https://github.com/jongio/azd-exec/actions/workflows/codeql.yml/badge.svg)](https://github.com/jongio/azd-exec/actions/workflows/codeql.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Go Report Card](https://goreportcard.com/badge/github.com/jongio/azd-exec/cli)](https://goreportcard.com/report/github.com/jongio/azd-exec/cli) +[![Go Reference](https://pkg.go.dev/badge/github.com/jongio/azd-exec/cli.svg)](https://pkg.go.dev/github.com/jongio/azd-exec/cli) +[![govulncheck](https://img.shields.io/badge/govulncheck-passing-brightgreen)](https://github.com/jongio/azd-exec/actions/workflows/govulncheck.yml) +[![golangci-lint](https://img.shields.io/badge/golangci--lint-enabled-blue)](https://github.com/jongio/azd-exec/actions/workflows/ci.yml) +[![Go Version](https://img.shields.io/badge/go-1.26.0-blue)](https://go.dev/) +[![Platform Support](https://img.shields.io/badge/platform-linux%20%7C%20macOS%20%7C%20windows-lightgrey)](https://github.com/jongio/azd-exec)
diff --git a/cli/.golangci.yml b/cli/.golangci.yml index 144d22a..fdbb654 100644 --- a/cli/.golangci.yml +++ b/cli/.golangci.yml @@ -25,12 +25,21 @@ linters: - 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 settings: errcheck: check-type-assertions: true @@ -44,10 +53,8 @@ linters: - G306 # File permissions - we need executable scripts govet: - enable-all: true disable: - - shadow # Disable shadow checking as it can be noisy - - fieldalignment # Disable struct field alignment - minor optimization + - fieldalignment revive: rules: @@ -65,16 +72,33 @@ linters: goconst: min-len: 3 min-occurrences: 3 + exhaustive: + default-signifies-exhaustive: true + gocritic: + disabled-checks: + - ifElseChain + - appendAssign + - elseif exclusions: rules: - path: '(.+)_test\.go' linters: - dupl + - errcheck - goconst + - unparam + - forcetypeassert + - noctx + - exhaustive + - prealloc + - gosec + - revive + - path: 'magefile\.go' + linters: + - unparam + - errcheck output: formats: - colored-line-number: + text: path: stdout - print-issued-lines: true - print-linter-name: true diff --git a/cli/magefile.go b/cli/magefile.go index f114bcf..b717228 100644 --- a/cli/magefile.go +++ b/cli/magefile.go @@ -384,6 +384,23 @@ func Lint() error { return nil } +// 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 +} + // Clean removes build artifacts and coverage reports. func Clean() error { fmt.Println("Cleaning build artifacts...") @@ -463,9 +480,56 @@ func SpellCheck() 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 +} + // Preflight runs all checks before shipping: format, build, lint, tests, and coverage. func Preflight() error { - fmt.Println("🚀 Running preflight checks...\n") + fmt.Println("🚀 Running preflight checks...") + fmt.Println() checks := []struct { name string @@ -474,6 +538,9 @@ func Preflight() error { {"Format check", Fmt}, {"Spell check", SpellCheck}, {"Linting", Lint}, + {"Checking gofumpt formatting", preflightGofumpt}, + {"Checking for dead code", preflightDeadcode}, + {"Cross-OS lint (GOOS=linux)", preflightCrossGOOSLint}, {"Unit tests", Test}, {"Integration tests", TestIntegration}, {"Coverage report", TestCoverage}, diff --git a/cli/src/cmd/exec/commands/mcp.go b/cli/src/cmd/exec/commands/mcp.go index 565d862..4e5f97e 100644 --- a/cli/src/cmd/exec/commands/mcp.go +++ b/cli/src/cmd/exec/commands/mcp.go @@ -248,7 +248,7 @@ func handleListShells(_ context.Context, _ azdext.ToolArgs) (*mcp.CallToolResult shellutil.ShellCmd, } - var results []shellInfo + results := make([]shellInfo, 0, len(shells)) for _, sh := range shells { _, err := exec.LookPath(sh) results = append(results, shellInfo{ @@ -385,14 +385,16 @@ func buildShellArgs(shell, scriptOrCmd string, isInline bool, extraArgs []string if isInline { return []string{"cmd", "/c", scriptOrCmd} } - args := []string{"cmd", "/c", scriptOrCmd} + args := make([]string, 0, 3+len(extraArgs)) + args = append(args, "cmd", "/c", scriptOrCmd) args = append(args, extraArgs...) return args case "powershell", "pwsh": if isInline { return []string{shellLower, "-NoProfile", "-Command", scriptOrCmd} } - args := []string{shellLower, "-NoProfile", "-File", scriptOrCmd} + args := make([]string, 0, 4+len(extraArgs)) + args = append(args, shellLower, "-NoProfile", "-File", scriptOrCmd) args = append(args, extraArgs...) return args default: @@ -400,7 +402,8 @@ func buildShellArgs(shell, scriptOrCmd string, isInline bool, extraArgs []string if isInline { return []string{shellLower, "-c", scriptOrCmd} } - args := []string{shellLower, scriptOrCmd} + args := make([]string, 0, 2+len(extraArgs)) + args = append(args, shellLower, scriptOrCmd) args = append(args, extraArgs...) return args } diff --git a/cli/src/internal/executor/command_builder.go b/cli/src/internal/executor/command_builder.go index 511597a..605813d 100644 --- a/cli/src/internal/executor/command_builder.go +++ b/cli/src/internal/executor/command_builder.go @@ -75,7 +75,7 @@ func (e *Executor) buildCommand(shell, scriptOrPath string, isInline bool) *exec cmdArgs = append(cmdArgs, e.config.Args...) } - return exec.Command(cmdArgs[0], cmdArgs[1:]...) // #nosec G204 - cmdArgs are controlled by caller + return exec.Command(cmdArgs[0], cmdArgs[1:]...) //nolint:noctx // CLI command builder has no context available; #nosec G204 } // buildPowerShellInlineCommand joins the inline script with its arguments into a single diff --git a/cspell.json b/cspell.json index 7c6b23f..b9f6e3c 100644 --- a/cspell.json +++ b/cspell.json @@ -33,6 +33,8 @@ "gochecknoinits", "gocognit", "gofmt", + "noctx", + "nolint", "goimports", "golangci", "gopls", @@ -83,7 +85,22 @@ "waitgroups", "winget", "wrapcheck", - "squoted" + "squoted", + "cliout", + "CONNSTR", + "copilotskills", + "coreversion", + "deadcode", + "eastus", + "gofumpt", + "MCPJSON", + "mvdan", + "myenv", + "anothersecret", + "pkill", + "PYTHONPATH", + "shellutil", + "testutil" ], "ignorePaths": [ "node_modules",