diff --git a/AGENTS.md b/AGENTS.md index ece3b8d68..6f5a79b6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,16 +1,16 @@ -# AGENTS.md — jongio/azd-app +# AGENTS.md: jongio/azd-app ## Overview -**azd-app** is an Azure Developer CLI (azd) extension that orchestrates multi-service application development. It provides service discovery, lifecycle management, health checks, log streaming, and a real-time dashboard — all driven from a single `azd app run` command. +**azd-app** is an Azure Developer CLI (azd) extension that orchestrates multi-service application development. It provides service discovery, lifecycle management, health checks, log streaming, and a real-time dashboard, all driven from a single `azd app run` command. ## Architecture Monorepo with three major components: -- **cli/** — Go CLI extension (the core product) -- **web/** — Astro 6 documentation site -- **proto/** — Protobuf service definitions (Connect-RPC v2) +- **cli/**: Go CLI extension (the core product) +- **web/**: Astro 6 documentation site +- **proto/**: Protobuf service definitions (Connect-RPC v2) ### CLI (Go) @@ -20,11 +20,11 @@ Monorepo with three major components: - **Core dependency**: `github.com/jongio/azd-core` (shared extension SDK) - **Build tool**: Mage (`magefile.go` at `cli/magefile.go`) - **Package structure**: - - `cli/src/cmd/app/` — Entry point - - `cli/src/cmd/app/commands/` — Command implementations (run, logs, health, test, etc.) - - `cli/src/internal/` — Domain packages (service, detector, executor, orchestrator, portmanager, etc.) - - `cli/src/gen/proto/` — Generated protobuf Go code -- **Dashboard**: `cli/dashboard/` — Vite + React 19 SPA, communicates via Connect-RPC + - `cli/src/cmd/app/`: Entry point + - `cli/src/cmd/app/commands/`: Command implementations (run, logs, health, test, etc.) + - `cli/src/internal/`: Domain packages (service, detector, executor, orchestrator, portmanager, etc.) + - `cli/src/gen/proto/`: Generated protobuf Go code +- **Dashboard**: `cli/dashboard/`, a Vite + React 19 SPA that communicates via Connect-RPC ### Web (Astro) @@ -50,7 +50,7 @@ Conventional Commits strictly enforced: ### Go Code Style -- **Error handling**: `fmt.Errorf` with `%w` wrapping — always add context +- **Error handling**: `fmt.Errorf` with `%w` wrapping, always add context - **Logging**: slog-based via logutil, component-scoped: `NewLogger("component-name")` - **Naming**: PascalCase exports, camelCase unexported, descriptive domain package names - **Interfaces**: Suffix with role (e.g., `*Credential`, `*Logger`, `*Provider`) @@ -66,15 +66,15 @@ Conventional Commits strictly enforced: ### Linting -- **Config**: `.golangci.yml` — 24 linters enabled, 5-minute timeout +- **Config**: `.golangci.yml`, 24 linters enabled, 5-minute timeout - **Key linters**: errcheck, govet, staticcheck, gosec, revive, dupl, exhaustive - **Security**: gosec enabled (G204/G304 excluded for CLI exec patterns) -- **Test exclusions**: Broad — most linters disabled for `_test.go` files +- **Test exclusions**: broad, most linters disabled for `_test.go` files - **Run**: `mage preflight` (runs format, imports, security, lint) ## CI/CD -- **Main CI**: `.github/workflows/ci.yml` — preflight, lint, test on ubuntu/windows/macos matrix +- **Main CI**: `.github/workflows/ci.yml`, running preflight, lint, and test on ubuntu/windows/macos matrix - **Go version**: 1.26.5, Node: 22, pnpm: 9 - **Race detector**: Enabled on Linux/Windows, disabled on macOS - **Coverage**: codecov integration with threshold enforcement diff --git a/README.md b/README.md index 9030f4071..065cc06cc 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Monitor all your services in one place with live status updates and health check ![Dashboard Resources](web/public/screenshots/dashboard-resources-cards.png) ### 📝 Unified Logs -Stream and filter logs from all services—both local and Azure. Search, highlight, and export with ease. Switch between local and cloud logs with a single click. +Stream and filter logs from all services, both local and Azure. Search, highlight, and export with ease. Switch between local and cloud logs with a single click. ![Console Logs](web/public/screenshots/dashboard-console.png) @@ -252,7 +252,7 @@ retired and being archived; it still resolves for existing installs but is no lo maintained. **azd exec** is no longer needed at all, because `azd exec` shipped as a built-in azd command in v1.25.1. -🌐 **Extension Hub**: [jongio.github.io/azd-extensions](https://jongio.github.io/azd-extensions/) — Browse all extensions, quick install, and registry info. +🌐 **Extension Hub**: [jongio.github.io/azd-extensions](https://jongio.github.io/azd-extensions/). Browse all extensions, quick install, and registry info. --- diff --git a/cli/dashboard/e2e/helpers/connect-mock.ts b/cli/dashboard/e2e/helpers/connect-mock.ts index 4c80b2c91..d344f9173 100644 --- a/cli/dashboard/e2e/helpers/connect-mock.ts +++ b/cli/dashboard/e2e/helpers/connect-mock.ts @@ -14,12 +14,12 @@ * does. * * Wire format: - * - Unary calls ride `application/json` with a raw message body — easy. + * - Unary calls ride `application/json` with a raw message body: easy. * - Server-streaming rides `application/connect+json` with length-prefixed * envelopes: one 5-byte header (1 flag byte + 4-byte big-endian length) * per frame, terminated by an end-stream envelope (flag 0x02). Request * bodies for streams are a single data envelope, which is why we parse - * with `postDataBuffer()` rather than `postData()` — the 5-byte prefix + * with `postDataBuffer()` rather than `postData()`: the 5-byte prefix * is not UTF-8-safe. * * Enum encoding: @@ -72,7 +72,7 @@ function encodeStreamBody(messages: unknown[], endPayload: unknown = {}): Buffer * Build a single data-envelope (flag 0x00) for a JSON message. Used by * callers that construct a never-closing ReadableStream inside the page * (via addInitScript) and need raw bytes to enqueue. Returns a plain - * Uint8Array — Node's Buffer doesn't survive the structured-clone serde + * Uint8Array: Node's Buffer doesn't survive the structured-clone serde * boundary into the page context. */ export function encodeStreamEnvelopeNoEnd(message: unknown): Uint8Array { diff --git a/cli/dashboard/e2e/helpers/test-setup.ts b/cli/dashboard/e2e/helpers/test-setup.ts index 700bfdf60..fee79abee 100644 --- a/cli/dashboard/e2e/helpers/test-setup.ts +++ b/cli/dashboard/e2e/helpers/test-setup.ts @@ -1032,7 +1032,7 @@ export async function mockConnectRoutes(page: Page, options: MockConnectOptions // close the connection, because `useHealthStream` flips `connected` to // false on stream close and schedules a reconnect. React batches state // updates, so a setConnected(true) + setConnected(false) within the - // same tick collapses to false — tests never observe the "connected" + // same tick collapses to false; tests never observe the "connected" // state and downstream hooks gated on it (useLogsStream for Azure) // never fire their first fetch. // diff --git a/cli/dashboard/src/App.tsx b/cli/dashboard/src/App.tsx index b57410e9a..4029aa7a2 100644 --- a/cli/dashboard/src/App.tsx +++ b/cli/dashboard/src/App.tsx @@ -38,7 +38,7 @@ function App() { // Update document title whenever the project name resolves. Keeping // this side-effect in App.tsx (vs. inside useProject) preserves the - // hook's purity — useProject is reused-safe and shouldn't mutate the + // hook's purity: useProject is reused-safe and shouldn't mutate the // browser document just by being called. useEffect(() => { if (projectName) { diff --git a/cli/dashboard/src/components/App.tsx b/cli/dashboard/src/components/App.tsx index 143c7510e..b533e862d 100644 --- a/cli/dashboard/src/components/App.tsx +++ b/cli/dashboard/src/components/App.tsx @@ -173,7 +173,7 @@ export function App({ ) const hasServiceSearch = serviceSearch.trim().length > 0 - // Sync selected service with services list (in case it updates) — render-time reset + // Sync selected service with services list (in case it updates), render-time reset const [prevServices, setPrevServices] = React.useState(services) if (services !== prevServices) { setPrevServices(services) @@ -329,8 +329,8 @@ export function App({ * * The full-screen blocking overlay only renders when the backend is * genuinely unreachable (reconnect attempts exhausted). Transient - * reconnect cycles — `Connection lost. Reconnecting in Ns...` and - * bare `Backend connection lost` during attempts 4-5 — must not + * reconnect cycles: `Connection lost. Reconnecting in Ns...` and + * bare `Backend connection lost` during attempts 4-5: must not * block the UI; the page stays interactive and recovers silently * when the stream re-attaches. The exhaustion signal is the single * `Click to reconnect` substring that useHealthStream sets exactly diff --git a/cli/dashboard/src/components/DiagnosticSettingsStep.test.tsx b/cli/dashboard/src/components/DiagnosticSettingsStep.test.tsx index 10ce4f9be..ef092f06b 100644 --- a/cli/dashboard/src/components/DiagnosticSettingsStep.test.tsx +++ b/cli/dashboard/src/components/DiagnosticSettingsStep.test.tsx @@ -11,7 +11,7 @@ import userEvent from '@testing-library/user-event' // Replace the Connect-backed hook with a tiny shim that still consumes // `globalThis.fetch` so the pre-existing fetch-mock staged payloads keep // driving the UI. The shim maps the legacy `/api/azure/diagnostic-settings/ -// check` JSON payload onto the hook's result shape verbatim — which +// check` JSON payload onto the hook's result shape verbatim, which // matches what the real hook surfaces once it's decoded the proto. vi.mock('@/hooks/useDiagnosticSettings', async () => { const React = await import('react') diff --git a/cli/dashboard/src/components/DiagnosticsModal.test.tsx b/cli/dashboard/src/components/DiagnosticsModal.test.tsx index ad522baa9..8e95b1943 100644 --- a/cli/dashboard/src/components/DiagnosticsModal.test.tsx +++ b/cli/dashboard/src/components/DiagnosticsModal.test.tsx @@ -7,7 +7,7 @@ * stage a proto response (built by `buildHealthResponse`) or a thrown * error. The assertion surface (rendered status, names, fix-setup * routing, etc.) is unchanged because the component still renders a - * `HealthCheckResponse` internally — only the wire shape differs. + * `HealthCheckResponse` internally: only the wire shape differs. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { render, screen, waitFor, cleanup } from '@testing-library/react' diff --git a/cli/dashboard/src/components/HistoricalLogPanel.tsx b/cli/dashboard/src/components/HistoricalLogPanel.tsx index c35b5a29e..36d2ce7f0 100644 --- a/cli/dashboard/src/components/HistoricalLogPanel.tsx +++ b/cli/dashboard/src/components/HistoricalLogPanel.tsx @@ -274,7 +274,7 @@ export function HistoricalLogPanel({ } }, [isOpen]) - // Reset state when panel opens with new service — render-time reset + // Reset state when panel opens with new service, render-time reset const [prevOpenKey, setPrevOpenKey] = React.useState(() => `${isOpen}:${serviceName}:${defaultTimeRange}`) const openKey = `${isOpen}:${serviceName}:${defaultTimeRange}` if (openKey !== prevOpenKey) { diff --git a/cli/dashboard/src/components/LogConfigPanel.tsx b/cli/dashboard/src/components/LogConfigPanel.tsx index 4525865d7..b429dfffe 100644 --- a/cli/dashboard/src/components/LogConfigPanel.tsx +++ b/cli/dashboard/src/components/LogConfigPanel.tsx @@ -76,7 +76,7 @@ export function LogConfigPanel({ // Close on Escape useEscapeKey(onClose, isOpen) - // Fetch data when panel opens — render-time reset + // Fetch data when panel opens, render-time reset const [prevIsOpen, setPrevIsOpen] = React.useState(isOpen) if (isOpen !== prevIsOpen) { setPrevIsOpen(isOpen) @@ -87,7 +87,7 @@ export function LogConfigPanel({ } } - // Sync local state with fetched config — render-time reset + // Sync local state with fetched config, render-time reset const [prevConfig, setPrevConfig] = React.useState(config) if (config !== prevConfig) { setPrevConfig(config) diff --git a/cli/dashboard/src/components/LogsPane.tsx b/cli/dashboard/src/components/LogsPane.tsx index a2fe843c6..7979c5267 100644 --- a/cli/dashboard/src/components/LogsPane.tsx +++ b/cli/dashboard/src/components/LogsPane.tsx @@ -99,7 +99,7 @@ export function LogsPane({ return `azure:${resolvedTimeRange.preset}:${end}:${azureRealtime ? 'realtime' : 'poll'}` }, [logMode, resolvedTimeRange.preset, resolvedTimeRange.end, azureRealtime]) - // Reset state when fetchKey changes — render-time reset + // Reset state when fetchKey changes, render-time reset const [prevFetchKey, setPrevFetchKey] = useState(fetchKey) if (fetchKey !== prevFetchKey) { setPrevFetchKey(fetchKey) diff --git a/cli/dashboard/src/components/ServiceDetailPanel.tsx b/cli/dashboard/src/components/ServiceDetailPanel.tsx index 0467c433d..9aaf8f794 100644 --- a/cli/dashboard/src/components/ServiceDetailPanel.tsx +++ b/cli/dashboard/src/components/ServiceDetailPanel.tsx @@ -783,7 +783,7 @@ export function ServiceDetailPanel({ useEscapeKey(onClose, isOpen) - // Reset tab when service changes — render-time reset + // Reset tab when service changes, render-time reset const [prevService, setPrevService] = React.useState(service) if (service !== prevService) { setPrevService(service) diff --git a/cli/dashboard/src/components/SettingsDialog.tsx b/cli/dashboard/src/components/SettingsDialog.tsx index aecb3874e..76cc0589e 100644 --- a/cli/dashboard/src/components/SettingsDialog.tsx +++ b/cli/dashboard/src/components/SettingsDialog.tsx @@ -79,7 +79,7 @@ export function SettingsDialog({ } }, []) - // Reset pending changes when dialog opens — render-time reset + // Reset pending changes when dialog opens, render-time reset const [prevIsOpen, setPrevIsOpen] = React.useState(isOpen) if (isOpen !== prevIsOpen) { setPrevIsOpen(isOpen) diff --git a/cli/dashboard/src/components/TableSelector.tsx b/cli/dashboard/src/components/TableSelector.tsx index 2d4c68ee1..073d43373 100644 --- a/cli/dashboard/src/components/TableSelector.tsx +++ b/cli/dashboard/src/components/TableSelector.tsx @@ -95,7 +95,7 @@ export function TableSelector({ new Set() ) - // Initialize expanded categories once categories are available — render-time reset + // Initialize expanded categories once categories are available, render-time reset const [prevSafeCategories, setPrevSafeCategories] = React.useState(safeCategories) if (safeCategories !== prevSafeCategories) { setPrevSafeCategories(safeCategories) diff --git a/cli/dashboard/src/components/TimeRangeSelector.tsx b/cli/dashboard/src/components/TimeRangeSelector.tsx index daf865667..74ed07886 100644 --- a/cli/dashboard/src/components/TimeRangeSelector.tsx +++ b/cli/dashboard/src/components/TimeRangeSelector.tsx @@ -96,7 +96,7 @@ export function TimeRangeSelector({ formatDateTimeLocal(value.end ?? getDefaultEnd()) ) - // Update local state when value changes externally — render-time reset + // Update local state when value changes externally, render-time reset const [prevValue, setPrevValue] = React.useState(value) if (value !== prevValue) { setPrevValue(value) diff --git a/cli/dashboard/src/gen/proto/azdapp/v1/common_pb.ts b/cli/dashboard/src/gen/proto/azdapp/v1/common_pb.ts index db437e4e7..f38188f26 100644 --- a/cli/dashboard/src/gen/proto/azdapp/v1/common_pb.ts +++ b/cli/dashboard/src/gen/proto/azdapp/v1/common_pb.ts @@ -505,7 +505,7 @@ export const ServiceStatusSchema: GenEnum = /*@__PURE__*/ * HealthState mirrors internal/healthcheck health states. * * Wire stability: enum values are append-only. DEGRADED was added after the - * initial draft when wiring HealthService — the existing dashboard summary + * initial draft when wiring HealthService; the existing dashboard summary * distinguishes degraded from unhealthy, and dropping that distinction would * silently lose information. Older clients that don't recognise the value * will see HEALTH_STATE_UNSPECIFIED (proto3 unknown-enum semantics) rather diff --git a/cli/dashboard/src/hooks/useAzureConnectionStatus.test.ts b/cli/dashboard/src/hooks/useAzureConnectionStatus.test.ts index 0ff9efda6..ae49818e1 100644 --- a/cli/dashboard/src/hooks/useAzureConnectionStatus.test.ts +++ b/cli/dashboard/src/hooks/useAzureConnectionStatus.test.ts @@ -34,7 +34,7 @@ interface RouterOverrides { /** * Build an in-memory router serving ModeService. Each test passes the * scenario it cares about; unimplemented methods raise CodeUnimplemented - * automatically — exactly what we want when a test should not hit a + * automatically: exactly what we want when a test should not hit a * given RPC. */ function makeTransport(overrides: RouterOverrides = {}) { @@ -78,7 +78,7 @@ describe('useAzureConnectionStatus (Connect)', () => { }) describe('initial render', () => { - it('does not auto-fetch — fetchAzureStatus must be called explicitly', async () => { + it('does not auto-fetch; fetchAzureStatus must be called explicitly', async () => { let calls = 0 const transport = makeTransport({ getMode: () => { @@ -186,13 +186,13 @@ describe('useAzureConnectionStatus (Connect)', () => { }) await waitFor(() => expect(calls).toBe(1)) - // Now issue concurrent calls — they must all bail out at the + // Now issue concurrent calls; they must all bail out at the // abortControllerRef guard before reaching the transport. act(() => { void result.current.fetchAzureStatus() void result.current.fetchAzureStatus() }) - // Give microtasks a chance — calls must stay at 1. + // Give microtasks a chance; calls must stay at 1. await Promise.resolve() expect(calls).toBe(1) @@ -248,7 +248,7 @@ describe('useAzureConnectionStatus (Connect)', () => { const { result } = renderHook(() => useAzureConnectionStatus({ transport })) - // Initial state is 'local' — switching to 'local' should be a no-op. + // Initial state is 'local'; switching to 'local' should be a no-op. await act(async () => { await result.current.handleLogModeChange('local') }) @@ -303,7 +303,7 @@ describe('useAzureConnectionStatus (Connect)', () => { // Switching flips on synchronously. expect(result.current.isModeSwitching).toBe(true) - // Resolve the SetMode promise — switching stays true until the + // Resolve the SetMode promise; switching stays true until the // 1500ms cleanup timeout fires. await act(async () => { resolveSet(create(SetModeResponseSchema, { diff --git a/cli/dashboard/src/hooks/useAzureConnectionStatus.ts b/cli/dashboard/src/hooks/useAzureConnectionStatus.ts index 6ffab84d9..772e1fd16 100644 --- a/cli/dashboard/src/hooks/useAzureConnectionStatus.ts +++ b/cli/dashboard/src/hooks/useAzureConnectionStatus.ts @@ -84,7 +84,7 @@ function logModeToProto(m: LogMode): ProtoLogMode { /** * Project a Get/SetMode response onto the local state shape. Keeps the - * two response handlers in sync — both messages share the same fields, + * two response handlers in sync: both messages share the same fields, * so a divergence here would silently desync the UI. */ interface NormalizedModeSnapshot { @@ -132,7 +132,7 @@ export interface UseAzureConnectionStatusResult { export interface UseAzureConnectionStatusOptions { onAzureRealtimeConfig?: (azureRealtime: boolean | undefined) => void - /** Test seam — inject a Connect transport (e.g. createRouterTransport). */ + /** Test seam, inject a Connect transport (e.g. createRouterTransport). */ transport?: Transport } diff --git a/cli/dashboard/src/hooks/useConsoleFilters.ts b/cli/dashboard/src/hooks/useConsoleFilters.ts index e85b7bf47..7b0919311 100644 --- a/cli/dashboard/src/hooks/useConsoleFilters.ts +++ b/cli/dashboard/src/hooks/useConsoleFilters.ts @@ -151,7 +151,7 @@ export function useConsoleFilters(services: Service[]): UseConsoleFiltersResult () => new Set(savedFilters?.healthFilter?.length ? savedFilters.healthFilter : ['healthy', 'degraded', 'unhealthy', 'unknown']) ) - // Sync selected services with available services — render-time reset + // Sync selected services with available services, render-time reset const [prevSyncKey, setPrevSyncKey] = React.useState(() => `${services.map(s => s.name).join(',')}:${serviceSelectionMode}`) const syncKey = `${services.map(s => s.name).join(',')}:${serviceSelectionMode}` if (syncKey !== prevSyncKey) { diff --git a/cli/dashboard/src/hooks/useHealthStream.test.ts b/cli/dashboard/src/hooks/useHealthStream.test.ts index 77831db7a..9d31544ce 100644 --- a/cli/dashboard/src/hooks/useHealthStream.test.ts +++ b/cli/dashboard/src/hooks/useHealthStream.test.ts @@ -51,7 +51,7 @@ interface Harness { /** * Build a transport whose StreamHealth handler yields events queued via * the returned controller. The queue uses an inner promise per pending - * event so the handler can suspend until tests push something — this is + * event so the handler can suspend until tests push something: this is * the same shape `cli/src/internal/rpc/health.go` produces (one async * source -> one yield per source event) but in TS test land. */ diff --git a/cli/dashboard/src/hooks/useHealthStream.ts b/cli/dashboard/src/hooks/useHealthStream.ts index e157f686b..6916a626b 100644 --- a/cli/dashboard/src/hooks/useHealthStream.ts +++ b/cli/dashboard/src/hooks/useHealthStream.ts @@ -1,5 +1,5 @@ /** - * useHealthStream — subscribes to the HealthService.StreamHealth Connect + * useHealthStream: subscribes to the HealthService.StreamHealth Connect * server-streaming RPC and exposes the same React hook surface the * dashboard already consumes. * @@ -9,8 +9,8 @@ * moves: instead of named SSE events (`message`, `health-change`, * `heartbeat`) we consume a `HealthEvent` oneof and translate each * variant back into the legacy event shape so downstream UI never sees - * a proto type. Doing that translation in this single place — rather - * than threading proto types through to App.tsx — keeps the migration + * a proto type. Doing that translation in this single place, rather + * than threading proto types through to App.tsx, keeps the migration * contained to the transport layer. * * The `summary` field on legacy `HealthReportEvent` was computed by the @@ -118,7 +118,7 @@ function healthStateToStatus(state: HealthState): HealthStatus { /** * Convert google.protobuf.Timestamp (seconds + nanos as bigint) to ISO - * string. Falls back to "now" when the server omitted the field — the + * string. Falls back to "now" when the server omitted the field: the * legacy SSE handler always populated it, so omission is a server bug * rather than a normal path, but a missing timestamp must never crash * the UI. diff --git a/cli/dashboard/src/hooks/useProject.test.tsx b/cli/dashboard/src/hooks/useProject.test.tsx index fade11536..a15df5e78 100644 --- a/cli/dashboard/src/hooks/useProject.test.tsx +++ b/cli/dashboard/src/hooks/useProject.test.tsx @@ -1,6 +1,6 @@ /** * Tests for useProject against an in-memory Connect router transport. - * Mirrors useCodespaceEnv.test.tsx — no fetch mocking, no client mocking; + * Mirrors useCodespaceEnv.test.tsx: no fetch mocking, no client mocking; * the production hook code path runs unchanged with an injected transport. */ import { renderHook, waitFor } from '@testing-library/react' diff --git a/cli/dashboard/src/hooks/useProject.ts b/cli/dashboard/src/hooks/useProject.ts index 16e14aebd..803bd8c6e 100644 --- a/cli/dashboard/src/hooks/useProject.ts +++ b/cli/dashboard/src/hooks/useProject.ts @@ -1,5 +1,5 @@ /** - * useProject — fetches azure.yaml-derived project metadata via the + * useProject: fetches azure.yaml-derived project metadata via the * ProjectService Connect handler. * * Wire migration note: replaces a one-shot `fetch('/api/project')` that diff --git a/cli/dashboard/src/hooks/useServiceErrors.ts b/cli/dashboard/src/hooks/useServiceErrors.ts index f080dba1c..b4706fc22 100644 --- a/cli/dashboard/src/hooks/useServiceErrors.ts +++ b/cli/dashboard/src/hooks/useServiceErrors.ts @@ -1,5 +1,5 @@ /** - * useServiceErrors — fan out N parallel Connect log streams (one per + * useServiceErrors: fan out N parallel Connect log streams (one per * service) and surface a boolean for whether ANY service has produced * an error-level entry in the last 30 seconds. * diff --git a/cli/dashboard/src/hooks/useSharedLogStream.ts b/cli/dashboard/src/hooks/useSharedLogStream.ts index 6aea9c9b6..80a25017a 100644 --- a/cli/dashboard/src/hooks/useSharedLogStream.ts +++ b/cli/dashboard/src/hooks/useSharedLogStream.ts @@ -1,5 +1,5 @@ /** - * useSharedLogStream — singleton multiplexer over the live local-log + * useSharedLogStream: singleton multiplexer over the live local-log * stream so a dashboard with N panes opens one upstream connection * instead of N. Each pane subscribes for one service (or "all"); the * manager fans incoming entries out to every matching subscriber. diff --git a/cli/dashboard/src/lib/connectClient.test.ts b/cli/dashboard/src/lib/connectClient.test.ts index cfc7586fe..f24a4e470 100644 --- a/cli/dashboard/src/lib/connectClient.test.ts +++ b/cli/dashboard/src/lib/connectClient.test.ts @@ -103,8 +103,8 @@ describe('connectClient factories', () => { __setDefaultTransportForTesting(null) // ensure fresh construction getDefaultTransport() - // The token selector must have been queried exactly once — at - // construction time — and must not be re-queried per request. + // The token selector must have been queried exactly once, at + // construction time, and must not be re-queried per request. const tokenQueryCount = querySpy.mock.calls.filter( ([selector]) => selector === 'meta[name="azd-session-token"]' ).length diff --git a/cli/dashboard/src/lib/connectClient.ts b/cli/dashboard/src/lib/connectClient.ts index 6c220affb..6921db990 100644 --- a/cli/dashboard/src/lib/connectClient.ts +++ b/cli/dashboard/src/lib/connectClient.ts @@ -17,7 +17,7 @@ * * Adding a new service: add an `import { FooService } from '@/gen/...'` * plus a `createFooClient(transport?)` factory that mirrors the pattern - * below. Do NOT cache the client at module scope — the caller (typically + * below. Do NOT cache the client at module scope: the caller (typically * a hook with a stable transport reference) is responsible for memoising. */ import { createClient, type Client, type Transport } from '@connectrpc/connect' @@ -65,7 +65,7 @@ let cachedDefaultTransport: Transport | null = null * exactly once and never mutated, so reading it at transport-construction time * is sufficient and avoids a redundant DOM query on every RPC call. * - * Keeping the value inside a closure — rather than a module-level variable — + * Keeping the value inside a closure (rather than a module-level variable) * prevents it from being visible via `window` inspection in browser DevTools * or becoming reachable through any future module-scope leak. */ @@ -109,7 +109,7 @@ export function getDefaultTransport(): Transport { /** * Test-only hook: replace the default transport for the duration of a * test. Pass `null` to fall back to the real transport. Production code - * MUST NOT call this — the only call sites should be vitest specs that + * MUST NOT call this: the only call sites should be vitest specs that * wire a `createRouterTransport` against an in-memory service handler. * * Exposed as a named export rather than a plain assignment because TS diff --git a/cli/dashboard/src/lib/log-utils.test.ts b/cli/dashboard/src/lib/log-utils.test.ts index 448fbf583..0617f04d6 100644 --- a/cli/dashboard/src/lib/log-utils.test.ts +++ b/cli/dashboard/src/lib/log-utils.test.ts @@ -30,7 +30,7 @@ describe('log-utils', () => { }) // ----------------------------------------------------------------------- - // SEC-016: XSS bypass vectors — verify the upstream security boundary + // SEC-016: XSS bypass vectors, verify the upstream security boundary // // The previous sanitizeHtml() regex blocklist was bypassable (CWE-184). // Security now relies solely on ansi-to-html's escapeXML:true option, @@ -42,11 +42,11 @@ describe('log-utils', () => { it('SEC-016 bypass: whitespace-in-handler (on\\tmouseover=) is not injected as an HTML attribute', () => { // The old regex `on\w+=` would not match onmouseover= because \t is not \w. // With escapeXML:true there are no < > to form new tags, so the text stays - // as inert text content inside a span — event handlers in text content never fire. + // as inert text content inside a span; event handlers in text content never fire. const result = convertAnsiToHtml('click on\tmouseover=alert(1)') // Must not appear as an attribute inside any HTML element expect(result).not.toMatch(/<[^>]+on[\s\t]+mouseover\s*=/i) - // The text itself appears as content — that is safe + // The text itself appears as content; that is safe expect(result).toContain('on\tmouseover=alert(1)') }) @@ -154,7 +154,7 @@ describe('log-utils', () => { }) it('should produce an href value that contains no raw double-quotes (CWE-79 regression)', () => { - // Normal localhost URL — verify the attribute value itself is quote-free + // Normal localhost URL, verify the attribute value itself is quote-free const result = convertAnsiToHtml('Server at http://localhost:3000/') const hrefMatch = result.match(/href="([^"]*)"/) expect(hrefMatch).toBeTruthy() diff --git a/cli/dashboard/src/lib/log-utils.ts b/cli/dashboard/src/lib/log-utils.ts index a1c85c16a..e4da10773 100644 --- a/cli/dashboard/src/lib/log-utils.ts +++ b/cli/dashboard/src/lib/log-utils.ts @@ -42,7 +42,7 @@ const ansiConverterDark = new AnsiConverter({ // entities.encodeXML() on every text token, converting all user-supplied // < > & " to < > & " before any HTML is assembled. // This is the sole XSS defence. No post-processing step is needed or used. - // Do NOT set to false — doing so would expose raw user content as HTML. + // Do NOT set to false; doing so would expose raw user content as HTML. escapeXML: true, stream: false, }) @@ -78,7 +78,7 @@ function stripAnsi(text: string): string { * passed to the AnsiConverter instances above. That option causes the library to call * entities.encodeXML() on every text token, so all user-supplied `< > & "` characters * are HTML-entity-encoded before any span tags are assembled. No further sanitization - * pass is applied or needed — one correct defence beats two flawed ones. + * pass is applied or needed: one correct defence beats two flawed ones. * * URL linkification only wraps `http://` / `https://` URLs (see URL_PATTERN), so * `javascript:` schemes cannot be injected into href attributes. diff --git a/cli/dashboard/src/lib/service-url-utils.test.ts b/cli/dashboard/src/lib/service-url-utils.test.ts index d21db595f..c299dfddb 100644 --- a/cli/dashboard/src/lib/service-url-utils.test.ts +++ b/cli/dashboard/src/lib/service-url-utils.test.ts @@ -28,7 +28,7 @@ function localInfo(overrides: Partial = {}): LocalServiceInfo } // ============================================================================= -// isValidUrl — scheme allowlist (CWE-79 regression suite) +// isValidUrl: scheme allowlist (CWE-79 regression suite) // ============================================================================= describe('isValidUrl', () => { @@ -59,7 +59,7 @@ describe('isValidUrl', () => { expect(isValidUrl('javascript:alert(document.cookie)')).toBe(false) }) - it('rejects javascript: URLs with encoded colon (%3A) — URL constructor normalises it', () => { + it('rejects javascript: URLs with encoded colon (%3A); URL constructor normalises it', () => { // new URL('javascript%3Aalert(1)') throws → returns false, which is also correct expect(isValidUrl('javascript%3Aalert(1)')).toBe(false) }) @@ -116,7 +116,7 @@ describe('isValidUrl', () => { it('rejects URLs with unbound port 0', () => { // port :0 is technically a valid URL but not a reachable endpoint; - // isValidUrl only validates scheme — unbound-port filtering is + // isValidUrl only validates scheme; unbound-port filtering is // done separately by hasUnboundPort() in getEffectiveLocalUrl expect(isValidUrl('http://localhost:0')).toBe(true) }) @@ -124,7 +124,7 @@ describe('isValidUrl', () => { }) // ============================================================================= -// getEffectiveLocalUrl — precedence + scheme filtering +// getEffectiveLocalUrl: precedence + scheme filtering // ============================================================================= describe('getEffectiveLocalUrl', () => { @@ -147,7 +147,7 @@ describe('getEffectiveLocalUrl', () => { expect(result.defaultUrl).toBe('http://localhost:3000') }) - it('blocks javascript: customUrl — falls back to url', () => { + it('blocks javascript: customUrl, falls back to url', () => { const result = getEffectiveLocalUrl(localInfo({ url: 'http://localhost:3000', customUrl: 'javascript:alert(document.cookie)', @@ -156,7 +156,7 @@ describe('getEffectiveLocalUrl', () => { expect(result.source).toBe('url') }) - it('blocks data: customUrl — falls back to url', () => { + it('blocks data: customUrl, falls back to url', () => { const result = getEffectiveLocalUrl(localInfo({ url: 'http://localhost:3000', customUrl: 'data:text/html,', @@ -165,7 +165,7 @@ describe('getEffectiveLocalUrl', () => { expect(result.source).toBe('url') }) - it('blocks javascript: customUrl and invalid url — returns null', () => { + it('blocks javascript: customUrl and invalid url and returns null', () => { const result = getEffectiveLocalUrl(localInfo({ customUrl: 'javascript:alert(1)', })) @@ -196,7 +196,7 @@ describe('getEffectiveLocalUrl', () => { }) // ============================================================================= -// getEffectiveAzureUrl — precedence + scheme filtering +// getEffectiveAzureUrl: precedence + scheme filtering // ============================================================================= describe('getEffectiveAzureUrl', () => { diff --git a/cli/docs/commands/init.md b/cli/docs/commands/init.md index 91246ff73..1874486a7 100644 --- a/cli/docs/commands/init.md +++ b/cli/docs/commands/init.md @@ -18,7 +18,7 @@ azd app init [flags] Creates a complete `azure.yaml` from scratch based on detected project structure, including services, ports, commands, prerequisites, and infrastructure dependencies. ### Existing Project (azure.yaml exists) -Non-destructively enriches the existing file — adds missing `ports`, `command`, `language`, and `uses` fields to services without overwriting anything already configured. +Non-destructively enriches the existing file, adds missing `ports`, `command`, `language`, and `uses` fields to services without overwriting anything already configured. ## Examples @@ -178,7 +178,7 @@ azd app run ## See Also - [azure.yaml Schema Reference](../schema/azure.yaml.md) -- [`azd app reqs`](reqs.md) — Check prerequisites -- [`azd app deps`](deps.md) — Install dependencies -- [`azd app run`](run.md) — Start services -- [`azd app add`](add.md) — Add container services +- [`azd app reqs`](reqs.md): Check prerequisites +- [`azd app deps`](deps.md): Install dependencies +- [`azd app run`](run.md): Start services +- [`azd app add`](add.md): Add container services diff --git a/cli/docs/commands/stop.md b/cli/docs/commands/stop.md index d6588eb33..a099e7136 100644 --- a/cli/docs/commands/stop.md +++ b/cli/docs/commands/stop.md @@ -10,7 +10,7 @@ azd app stop ## Description -Sends a shutdown signal to the running `azd app run` process. This triggers graceful shutdown including prestop/poststop hooks, port release, and process cleanup — identical to pressing Ctrl+C in the run terminal. +Sends a shutdown signal to the running `azd app run` process. This triggers graceful shutdown including prestop/poststop hooks, port release, and process cleanup, identical to pressing Ctrl+C in the run terminal. Run this from **any terminal** in the project directory while `azd app run` is active in another terminal. @@ -25,8 +25,8 @@ Run this from **any terminal** in the project directory while `azd app run` is a The stop command supports `prestop` and `poststop` hooks defined in `azure.yaml`: -- **`prestop`** — Runs before services are stopped (e.g., drain connections, flush caches) -- **`poststop`** — Runs after all services are stopped (e.g., cleanup temp files, remove tunnels) +- **`prestop`**: Runs before services are stopped (e.g., drain connections, flush caches) +- **`poststop`**: Runs after all services are stopped (e.g., cleanup temp files, remove tunnels) Hook failures are non-fatal: services will still be stopped even if a hook fails. diff --git a/cli/docs/commands/validate.md b/cli/docs/commands/validate.md index 10e019be5..3dd4f80d2 100644 --- a/cli/docs/commands/validate.md +++ b/cli/docs/commands/validate.md @@ -22,7 +22,7 @@ azd app validate |------|-------|------|---------|-------------| | `--output` | `-o` | string | `default` | Output format: 'default' or 'json' (inherited from parent) | -The command takes no positional arguments. `azure.yaml` is located the same way `azd app run` locates it — by searching the current directory and its parents. +The command takes no positional arguments. `azure.yaml` is located the same way `azd app run` locates it, by searching the current directory and its parents. ## Execution Flow diff --git a/cli/docs/features/local-services.md b/cli/docs/features/local-services.md index 09a1aab92..15e162632 100644 --- a/cli/docs/features/local-services.md +++ b/cli/docs/features/local-services.md @@ -70,15 +70,15 @@ services: - ./eventhubs-config.json:/Eventhubs_Emulator/ConfigFiles/Config.json # bind mount ``` -- **`volumes`** — named volumes and bind mounts (bind paths resolve relative to +- **`volumes`**: named volumes and bind mounts (bind paths resolve relative to the project directory). -- **`command`** — string or array; overrides the image's default command. -- **`pull_policy`** — `missing` / `always` / `never`. -- **Multi-port** — every port in `ports` is published. -- **Networking** — all container services share a project network and resolve +- **`command`**: string or array; overrides the image's default command. +- **`pull_policy`**: `missing` / `always` / `never`. +- **Multi-port**: every port in `ports` is published. +- **Networking**: all container services share a project network and resolve each other by service name (`BLOB_SERVER: azurite`). See the [Container Networking](../schema/azure.yaml.md#container-networking) reference. -- **`uses`** — health-gated startup ordering (Compose `depends_on: service_healthy`). +- **`uses`**: health-gated startup ordering (Compose `depends_on: service_healthy`). ## Behavior diff --git a/cli/docs/schema/azure.yaml.md b/cli/docs/schema/azure.yaml.md index 978bfa862..5a27378c1 100644 --- a/cli/docs/schema/azure.yaml.md +++ b/cli/docs/schema/azure.yaml.md @@ -12,7 +12,7 @@ This document describes the `azd app` extensions to the standard `azure.yaml` co `azd app` extends the standard `azd` azure.yaml with local development features: - **`ports`**: Explicit port mappings (Docker Compose style) -- **`volumes`**: Container volume mounts — named volumes and bind mounts (Docker Compose style) +- **`volumes`**: Container volume mounts, both named volumes and bind mounts (Docker Compose style) - **`environment`**: Environment variables (Docker Compose compatible formats) - **`entrypoint`**: Custom entry point files for Python/Node services - **`command`**: Override auto-detected run commands (string or array) @@ -127,14 +127,14 @@ services: Container services support these properties: - **`image`**: Docker image name (triggers container mode) -- **`ports`**: Port mappings (`["5432"]` or `["5432:5432"]`) — **all** listed ports are published +- **`ports`**: Port mappings (`["5432"]` or `["5432:5432"]`). **All** listed ports are published - **`volumes`**: Named volumes and bind mounts (`["pgdata:/var/lib/postgresql/data", "./init.sql:/init.sql:ro"]`) - **`environment`**: Environment variables for the container - **`command`**: Override the container's default command (string or array) - **`pull_policy`**: When to pull the image (`missing`, `always`, `never`) - **`healthcheck`**: Health check configuration - **`type`**: Auto-detected as `container` when `image` is set -- **`uses`**: Start ordering — a container waits for its dependencies to be healthy first +- **`uses`**: Start ordering, so a container waits for its dependencies to be healthy first Container services in a project also share a per-project Docker network so they can resolve each other by service name (see [Container Networking](#container-networking)). @@ -143,7 +143,7 @@ can resolve each other by service name (see [Container Networking](#container-ne All container services in a project are automatically attached to a shared, per-project Docker network. Each container is registered on that network under -its **service name**, so one container can reach another by that name — exactly +its **service name**, so one container can reach another by that name, exactly like Docker Compose. ```yaml @@ -172,12 +172,12 @@ Notes: - **DNS by service name.** `BLOB_SERVER: azurite` resolves to the azurite container over the shared network. No `container_name` or host IP is needed. - **Startup ordering via `uses`.** Listing `uses: ["azurite"]` makes `eventhubs` - start only after `azurite` reports healthy — the equivalent of Docker Compose + start only after `azurite` reports healthy; the equivalent of Docker Compose `depends_on` with `condition: service_healthy`. - **Persistent containers.** Container services keep running across `azd app run` sessions (stopped with the app's shutdown but reused on the next run). The project network persists with them and is reused. -- **Single-container projects** are unaffected — they still publish their ports +- **Single-container projects** are unaffected: they still publish their ports to the host as before. ## Root Properties @@ -295,7 +295,7 @@ services: project: ./worker command: "npm run worker:start" - # Array form — useful for container services with many flags + # Array form, useful for container services with many flags postgres: image: postgres:16-alpine command: ["postgres", "-c", "max_connections=200", "-c", "log_statement=all"] @@ -408,8 +408,8 @@ services: Docker Compose-style volume mounts for **container services**. Supports: -- **Named volumes** — `name:/container/path` (Docker-managed, persist across runs) -- **Bind mounts** — `./host/path:/container/path[:mode]` (host path is resolved +- **Named volumes**: `name:/container/path` (managed by Docker and persistent across runs) +- **Bind mounts**: `./host/path:/container/path[:mode]` (host path is resolved **relative to the project directory**; absolute host paths are also accepted) Relative bind-mount paths that escape the project directory are rejected. @@ -428,11 +428,11 @@ services: Controls when a **container service** image is pulled before it runs: -- `missing` — pull only when the image is not present locally (recommended for +- `missing`: pull only when the image is not present locally (recommended for pinned emulator images to avoid re-pulling on every run) -- `always` — always attempt to pull -- `never` — never pull; fail if the image is absent locally -- *(unset)* — best-effort pull (default) +- `always`: always attempt to pull +- `never`: never pull; fail if the image is absent locally +- *(unset)*: best-effort pull (default) ```yaml services: @@ -478,11 +478,11 @@ services: #### `uses` **Type:** `array` of `string` (optional) -Service dependencies — defines startup order and infrastructure connections. +Service dependencies: defines startup order and infrastructure connections. **Two use cases:** -1. **Service dependencies** — reference other services by name for startup ordering -2. **Infrastructure dependencies** — reference infrastructure services (databases, caches, queues) for connection string injection +1. **Service dependencies**: reference other services by name for startup ordering +2. **Infrastructure dependencies**: reference infrastructure services (databases, caches, queues) for connection string injection ```yaml services: @@ -1407,4 +1407,3 @@ services: - [Port Configuration Guide](../features/ports.md) - [Port Management Design](../design/ports.md) - [Azure Functions Support](../features/azure-functions.md) - Comprehensive Azure Functions documentation - diff --git a/cli/evals/.vally.yaml b/cli/evals/.vally.yaml index da5615f39..d1cd66c12 100644 --- a/cli/evals/.vally.yaml +++ b/cli/evals/.vally.yaml @@ -10,12 +10,12 @@ paths: suites: smoke: - description: "Quick routing checks — 1 stimulus per skill, runs: 1" + description: "Quick routing checks, 1 stimulus per skill, runs: 1" filter: tier: smoke pr: - description: "PR gate — smoke routing + basic generation" + description: "PR gate, smoke routing + basic generation" filter: tier: smoke @@ -30,5 +30,5 @@ suites: type: integration full: - description: "All evals — nightly" + description: "All evals, nightly" filter: {} diff --git a/cli/evals/README.md b/cli/evals/README.md index 501773c45..9909c57dd 100644 --- a/cli/evals/README.md +++ b/cli/evals/README.md @@ -6,7 +6,7 @@ Evaluation suites for azd-app Copilot skills, powered by [@microsoft/vally](http ``` cli/evals/ -├── .vally.yaml # Root config — suite definitions +├── .vally.yaml # Root config, suite definitions ├── package.json # Dev dependency + npm scripts ├── .gitignore # Ignores results/ and node_modules/ └── azd-app-onboard/ @@ -21,7 +21,7 @@ cd cli/evals # Install Vally CLI npm install -# Run smoke suite (fast — routing checks only) +# Run smoke suite (fast: routing checks only) npm run eval:smoke # Run a specific skill's eval @@ -38,11 +38,11 @@ npm run grade -- azd-app-onboard/eval.yaml < results/results.jsonl | Suite | Filter | Use Case | |-------|--------|----------| -| `smoke` | `tier: smoke` | PR gate — fast routing checks | +| `smoke` | `tier: smoke` | PR gate, fast routing checks | | `pr` | `tier: smoke` | Same as smoke (alias for clarity) | | `routing` | `type: routing` | All routing/trigger evals | | `integration` | `type: integration` | Full behavior tests (LLM-backed) | -| `full` | (none) | All evals — nightly CI | +| `full` | (none) | All evals, nightly CI | ## CI Integration diff --git a/cli/evals/azd-app-onboard/eval.yaml b/cli/evals/azd-app-onboard/eval.yaml index 0376f37f3..b37c485dc 100644 --- a/cli/evals/azd-app-onboard/eval.yaml +++ b/cli/evals/azd-app-onboard/eval.yaml @@ -1,4 +1,4 @@ -# Vally eval suite — azd-app-onboard skill +# Vally eval suite: azd-app-onboard skill # Validates that the onboarding skill is correctly invoked and produces # quality guidance for various project onboarding scenarios. @@ -26,7 +26,7 @@ scoring: stimuli: # ═══════════════════════════════════════════════════════════ - # ROUTING — Does the agent invoke this skill for onboarding? + # ROUTING: Does the agent invoke this skill for onboarding? # ═══════════════════════════════════════════════════════════ - name: "New project onboarding request" @@ -80,7 +80,7 @@ stimuli: config: pattern: "(?i)fatal error|unhandled exception|stack trace" - - name: "Negative routing — running services (should NOT invoke onboard)" + - name: "Negative routing: running services (should NOT invoke onboard)" prompt: "Run my services with azd app" tags: type: routing @@ -101,7 +101,7 @@ stimuli: pattern: "(?i)fatal error|unhandled exception|stack trace" # ═══════════════════════════════════════════════════════════ - # GENERATION — Correct azure.yaml output for common projects + # GENERATION: Correct azure.yaml output for common projects # ═══════════════════════════════════════════════════════════ - name: "Node.js Express API azure.yaml generation" @@ -200,7 +200,7 @@ stimuli: pattern: "(?i)fatal error|unhandled exception|stack trace" # ═══════════════════════════════════════════════════════════ - # GUIDANCE — Step-by-step quality + # GUIDANCE: Step-by-step quality # ═══════════════════════════════════════════════════════════ - name: "Complete onboarding guidance includes verification steps" @@ -287,7 +287,7 @@ stimuli: pattern: "(?i)fatal error|unhandled exception|stack trace" # ═══════════════════════════════════════════════════════════ - # ERROR HANDLING — Invalid project structures + # ERROR HANDLING: Invalid project structures # ═══════════════════════════════════════════════════════════ - name: "Handles empty project gracefully" @@ -335,7 +335,7 @@ stimuli: pattern: "(?i)(custom|command|docker|run\\.command|manual)" # ═══════════════════════════════════════════════════════════ - # COMPLETENESS — All services detected + # COMPLETENESS: All services detected # ═══════════════════════════════════════════════════════════ - name: "Detects all services in complex project" diff --git a/cli/magefile.go b/cli/magefile.go index 73c3bc4c4..7a3363f6c 100644 --- a/cli/magefile.go +++ b/cli/magefile.go @@ -115,7 +115,7 @@ func getVersion() (string, error) { // For dev builds, append git metadata. sha, err := sh.Output("git", "rev-parse", "--short", "HEAD") if err != nil { - // No git available — fall back to base version with +dev suffix. + // No git available; fall back to base version with +dev suffix. return base + "+dev", nil } @@ -801,7 +801,7 @@ func ModTidy() error { // In workspace mode, use GOWORK=off so tidy resolves against the module proxy env := os.Environ() if _, err := os.Stat("../go.work"); err == nil { - fmt.Println(" (workspace detected — running with GOWORK=off)") + fmt.Println(" (workspace detected, running with GOWORK=off)") env = append(env, "GOWORK=off") } @@ -1512,7 +1512,7 @@ func fmtCheck() error { // 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(" ⚠️ gofumpt not installed, skipping strict format check") fmt.Println(" Install with: go install mvdan.cc/gofumpt@latest") return nil } @@ -1534,7 +1534,7 @@ func preflightGofumpt() error { // 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(" ⚠️ deadcode not installed, skipping dead code check") fmt.Println(" Install with: go install golang.org/x/tools/cmd/deadcode@latest") return nil } @@ -1542,7 +1542,7 @@ func preflightDeadcode() error { if err != nil { fmt.Println(" ⚠️ Dead code found:") fmt.Println(output) - // Non-fatal for now — report but don't fail + // Non-fatal for now: report but don't fail fmt.Println(" ⚠️ Dead code check completed with findings (non-fatal)") return nil } @@ -1589,7 +1589,7 @@ func quietLint() error { // 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") + fmt.Println(" ⏭️ Already on Linux, skipping cross-OS lint") return nil } cmd := exec.Command("golangci-lint", "run", "./...") @@ -1687,7 +1687,7 @@ func quietTestCoverage() error { // quietTestOnly runs tests without coverage profiling for maximum speed. // Skipping -coverprofile eliminates code instrumentation overhead. // Uses -vet=off because golangci-lint (which includes vet) runs as a separate -// parallel step — no need to run vet twice. +// parallel step, no need to run vet twice. // Use 'mage testCoverage' when you need coverage reports. func quietTestOnly() error { pkgPath := goSrcPattern @@ -1770,7 +1770,7 @@ func playwrightInstallBrowsers() error { return fmt.Errorf("failed to get absolute website path: %w", err) } - // Install from both dirs concurrently — they may pin different Playwright versions. + // Install from both dirs concurrently; they may pin different Playwright versions. var wg sync.WaitGroup var mu sync.Mutex var errs []error @@ -1919,7 +1919,7 @@ func websiteTestE2EDevServer() error { // Clean up stale dev servers from interrupted runs. killProcessOnPort(4321) - // Use dev server — avoids the full Astro production build. + // Use dev server, avoids the full Astro production build. // Pages compile on first request; startup is faster than build+preview under contention. serverCmd := exec.Command("npx", "astro", "dev", "--host", "127.0.0.1", "--port", "4321") serverCmd.Dir = absWebsiteDir diff --git a/cli/src/cmd/app/commands/core_deps.go b/cli/src/cmd/app/commands/core_deps.go index 139e9d8f7..d6498d560 100644 --- a/cli/src/cmd/app/commands/core_deps.go +++ b/cli/src/cmd/app/commands/core_deps.go @@ -385,8 +385,8 @@ func detectProjectsFromAzureYaml(searchRoot string) ([]types.NodeProject, []type // Dedupe by resolved project directory: a monorepo often points several // services at one directory (e.g. `project: .` on each), and each would - // otherwise be collected — then installed, and rendered as its own progress - // bar — once per service. Collapsing to the unique directory installs it once. + // otherwise be collected, then installed, and rendered as its own progress + // bar, once per service. Collapsing to the unique directory installs it once. seenDirs := make(map[string]bool) for _, svc := range azureYaml.Services { diff --git a/cli/src/cmd/app/commands/core_helpers.go b/cli/src/cmd/app/commands/core_helpers.go index 29af566f0..fd35ee3de 100644 --- a/cli/src/cmd/app/commands/core_helpers.go +++ b/cli/src/cmd/app/commands/core_helpers.go @@ -54,7 +54,7 @@ func loadAzureYaml() (string, *AzureYaml, error) { return "", nil, newProjectNotFoundError() } - // Validate path to azure.yaml — enforce containment within cwd so a + // Validate path to azure.yaml, enforce containment within cwd so a // crafted detector result cannot point outside the project tree (CWE-22). if _, err := internalsec.ValidatePathContainment(azureYamlPath, cwd); err != nil { return "", nil, fmt.Errorf("invalid path: %w", err) diff --git a/cli/src/cmd/app/commands/core_helpers_security_test.go b/cli/src/cmd/app/commands/core_helpers_security_test.go index c9dbb7421..775cd6620 100644 --- a/cli/src/cmd/app/commands/core_helpers_security_test.go +++ b/cli/src/cmd/app/commands/core_helpers_security_test.go @@ -33,7 +33,7 @@ func TestValidatePathContainment_TraversalRejected(t *testing.T) { // TestValidatePathContainment_AbsoluteOutsideRejected mirrors acceptance criterion 5. func TestValidatePathContainment_AbsoluteOutsideRejected(t *testing.T) { root := t.TempDir() - outside := filepath.Dir(root) // parent directory — always exists + outside := filepath.Dir(root) // The parent directory always exists. _, err := internalsec.ValidatePathContainment(outside, root) if err == nil { diff --git a/cli/src/cmd/app/commands/doctor.go b/cli/src/cmd/app/commands/doctor.go index 4379cb84c..6861d50d2 100644 --- a/cli/src/cmd/app/commands/doctor.go +++ b/cli/src/cmd/app/commands/doctor.go @@ -32,7 +32,7 @@ type doctorCheck struct { } // doctorToolRequirement describes a required executable. Candidates holds the -// acceptable executable names — the requirement is satisfied when any one of +// acceptable executable names; the requirement is satisfied when any one of // them resolves on PATH. An empty Candidates means the requirement name is the // only accepted executable. type doctorToolRequirement struct { diff --git a/cli/src/cmd/app/commands/init.go b/cli/src/cmd/app/commands/init.go index 6189cccc8..4ec18d6eb 100644 --- a/cli/src/cmd/app/commands/init.go +++ b/cli/src/cmd/app/commands/init.go @@ -121,7 +121,7 @@ func runInit(cmd *cobra.Command, _ []string) error { if dryRun { cliout.Newline() - cliout.Info("Dry run — no files modified") + cliout.Info("Dry run: no files modified") cliout.Newline() cliout.Item("Generated azure.yaml would contain:") cliout.Newline() @@ -963,7 +963,7 @@ func enrichAzureYaml(azureYamlPath string, services []DetectedService) error { } if !modified { - cliout.Info("No changes needed — azure.yaml already has complete service configuration") + cliout.Info("No changes needed: azure.yaml already has complete service configuration") return nil } diff --git a/cli/src/cmd/app/commands/init_test.go b/cli/src/cmd/app/commands/init_test.go index 77a4a7879..1a7a89ad1 100644 --- a/cli/src/cmd/app/commands/init_test.go +++ b/cli/src/cmd/app/commands/init_test.go @@ -318,6 +318,49 @@ services: assert.Contains(t, content, "5173") } +func TestEnrichAzureYaml_CompleteServiceNeedsNoChanges(t *testing.T) { + dir := t.TempDir() + yamlPath := filepath.Join(dir, "azure.yaml") + original := `name: myapp +services: + api: + language: ts + project: ./api + ports: + - "3000" +` + require.NoError(t, os.WriteFile(yamlPath, []byte(original), 0o644)) + + services := []DetectedService{{ + Name: "api", + Language: "ts", + Project: "./api", + Ports: []string{"3000"}, + }} + + require.NoError(t, enrichAzureYaml(yamlPath, services)) + + actual, err := os.ReadFile(yamlPath) + require.NoError(t, err) + assert.Equal(t, original, string(actual)) +} + +func TestInitCommand_DryRunDoesNotWriteAzureYaml(t *testing.T) { + dir := t.TempDir() + t.Chdir(dir) + require.NoError(t, os.WriteFile( + filepath.Join(dir, "package.json"), + []byte(`{"scripts":{"dev":"vite"},"devDependencies":{"vite":"^5.0.0"}}`), + 0o644, + )) + + cmd := NewInitCommand() + cmd.SetArgs([]string{"--dry-run"}) + + require.NoError(t, cmd.Execute()) + assert.NoFileExists(t, filepath.Join(dir, "azure.yaml")) +} + func TestDetectServiceDependencies_NoFalsePositives(t *testing.T) { dir := t.TempDir() // package.json with "pg" appearing in description/URLs but NOT as a dependency diff --git a/cli/src/cmd/app/commands/logs.go b/cli/src/cmd/app/commands/logs.go index af1733ece..a25e19526 100644 --- a/cli/src/cmd/app/commands/logs.go +++ b/cli/src/cmd/app/commands/logs.go @@ -374,7 +374,7 @@ func (e *logsExecutor) execute(ctx context.Context, args []string) error { } // CLI-specific: emit informational messages based on collected status. - // In follow mode, skip this gate — follow mode creates its own dashboard + // In follow mode, skip this gate; follow mode creates its own dashboard // connection and can wait for services to produce logs. if e.opts.source == string(LogSourceLocal) && !e.opts.follow { if !collected.DashboardAvailable || collected.ServiceCount == 0 { diff --git a/cli/src/cmd/app/commands/mcp.go b/cli/src/cmd/app/commands/mcp.go index 9a349ad59..b2627f950 100644 --- a/cli/src/cmd/app/commands/mcp.go +++ b/cli/src/cmd/app/commands/mcp.go @@ -130,7 +130,7 @@ This server complements azd's core MCP capabilities: newServiceConfigResource(), ) - // Register all tools via AddTool — builder handles rate limiting + ToolArgs parsing + // Register all tools via AddTool, builder handles rate limiting + ToolArgs parsing registerAllTools(builder) s := builder.Build() @@ -215,7 +215,7 @@ func extractValidatedProjectDir(args azdext.ToolArgs) (string, error) { } // marshalToolResult marshals data to JSON, sanitizes all string values for LLM -// safety (stripping ANSI/control characters — CWE-150/117), and returns an MCP +// safety (stripping ANSI/control characters, CWE-150/117), and returns an MCP // tool result with both structured content and a text fallback. // // The JSON round-trip (Marshal → Unmarshal → sanitizeAny → Marshal) is diff --git a/cli/src/cmd/app/commands/mcp_sanitize.go b/cli/src/cmd/app/commands/mcp_sanitize.go index a886f62b5..9aff2897d 100644 --- a/cli/src/cmd/app/commands/mcp_sanitize.go +++ b/cli/src/cmd/app/commands/mcp_sanitize.go @@ -33,7 +33,7 @@ var ansiStripper = regexp.MustCompile( // sanitizeForLLM strips ANSI escape sequences and dangerous C0/C1 control // characters from s before it is included in an MCP tool response. // -// Preserved: \t (0x09), \n (0x0A), \r (0x0D) — safe for LLM consumption. +// Preserved: \t (0x09), \n (0x0A), \r (0x0D), safe for LLM consumption. // Stripped: ANSI CSI/OSC/charset sequences; C0 control chars 0x00-0x08, // 0x0B, 0x0C, 0x0E-0x1F (includes bare ESC); C1 control chars 0x80-0x9F. func sanitizeForLLM(s string) string { @@ -41,7 +41,7 @@ func sanitizeForLLM(s string) string { s = ansiStripper.ReplaceAllString(s, "") // Second pass: strip any remaining dangerous control characters. - // Allocate the same capacity as the input — common case is no stripping. + // Allocate the same capacity as the input; the common case is no stripping. var b strings.Builder b.Grow(len(s)) for _, r := range s { @@ -119,7 +119,7 @@ func sanitizeAny(v any) any { } return result default: - return val // nil, bool, float64 — no string content + return val // nil, bool, float64, no string content } } @@ -143,7 +143,7 @@ var sensitivePrefixes = []string{ // pattern; otherwise it returns value unchanged. // // This function is used exclusively in MCP tool responses where the result -// reaches an LLM context window. Full redaction is intentional — even a +// reaches an LLM context window. Full redaction is intentional, even a // partial leak (e.g. first/last two chars) is unacceptable for LLM output. // // For CLI display, see redactSecretValue in core_helpers.go which applies @@ -157,7 +157,7 @@ var sensitivePrefixes = []string{ func redactEnvVarForMCP(key, value string) string { upper := strings.ToUpper(key) - // Exact matches first — cheap O(1) check. + // Exact matches first, cheap O(1) check. if upper == "PASSWORD" || upper == "SECRET" { return redacted } diff --git a/cli/src/cmd/app/commands/mcp_sanitize_test.go b/cli/src/cmd/app/commands/mcp_sanitize_test.go index 25e1e7ecf..565dd53e2 100644 --- a/cli/src/cmd/app/commands/mcp_sanitize_test.go +++ b/cli/src/cmd/app/commands/mcp_sanitize_test.go @@ -403,7 +403,7 @@ func TestRedactEnvVarForMCP_NonSensitiveKeys(t *testing.T) { } // TestRedactEnvVarForMCP_CaseInsensitive verifies that matching is -// case-insensitive (AC1 — lowercase and mixed-case keys are caught). +// case-insensitive (AC1, lowercase and mixed-case keys are caught). func TestRedactEnvVarForMCP_CaseInsensitive(t *testing.T) { cases := []struct { key string diff --git a/cli/src/cmd/app/commands/mcp_test.go b/cli/src/cmd/app/commands/mcp_test.go index a46f4a5da..329a77e71 100644 --- a/cli/src/cmd/app/commands/mcp_test.go +++ b/cli/src/cmd/app/commands/mcp_test.go @@ -1144,8 +1144,8 @@ func TestSetEnvironmentVariableToolValidation(t *testing.T) { // TestSetEnvironmentVariableRedactsSecrets verifies that // handleSetEnvironmentVariable (CWE-684 / SEC-015) never echoes sensitive -// values — keys matching TOKEN, SECRET, KEY, PASSWORD, CREDENTIAL, or -// CONNECTION_STRING patterns — back in the MCP tool response. +// values, keys matching TOKEN, SECRET, KEY, PASSWORD, CREDENTIAL, or +// CONNECTION_STRING patterns, back in the MCP tool response. // // Acceptance criteria: // @@ -1181,7 +1181,7 @@ func TestSetEnvironmentVariableRedactsSecrets(t *testing.T) { {"DB_PASSWORD", "supersecret123", "supersecret123"}, // SECRET pattern {"CLIENT_SECRET", "my-very-secret-value", "my-very-secret-value"}, - // CREDENTIAL pattern — AC3: newly added pattern + // CREDENTIAL pattern, AC3: newly added pattern {"APP_CREDENTIAL", "cred_value_abc", "cred_value_abc"}, // CONNECTION_STRING pattern {"DB_CONNECTION_STRING", "Server=host;Password=pw;", "Server=host;Password=pw;"}, diff --git a/cli/src/cmd/app/commands/mcp_tools.go b/cli/src/cmd/app/commands/mcp_tools.go index 19bac6be5..2632535ef 100644 --- a/cli/src/cmd/app/commands/mcp_tools.go +++ b/cli/src/cmd/app/commands/mcp_tools.go @@ -746,7 +746,7 @@ func handleInstallDependencies(ctx context.Context, args azdext.ToolArgs) (*mcp. return mcpErrorResult("Error searching for azure.yaml: %v", err), nil } if azureYamlPath == "" { - return mcpErrorResult("install_dependencies requires an azure.yaml project — run from a project directory"), nil + return mcpErrorResult("install_dependencies requires an azure.yaml project; run from a project directory"), nil } if ctxErr := ctx.Err(); ctxErr != nil { @@ -885,7 +885,7 @@ func addSetEnvironmentVariableTool(b *azdext.MCPServerBuilder) { azdext.MCPToolOptions{ Title: "Set Environment Variable", Description: "Provides guidance on how to set an environment variable for services. " + - "This tool does NOT modify any files or system state — it returns instructions " + + "This tool does NOT modify any files or system state; it returns instructions " + "for configuring the variable in azure.yaml, .env files, or the shell. " + "Secret-pattern values (keys containing TOKEN, SECRET, KEY, PASSWORD, CREDENTIAL, " + "CONNECTION_STRING) are redacted in the response.", diff --git a/cli/src/cmd/app/commands/run.go b/cli/src/cmd/app/commands/run.go index d7d78a3e9..a76e2b5d3 100644 --- a/cli/src/cmd/app/commands/run.go +++ b/cli/src/cmd/app/commands/run.go @@ -230,7 +230,7 @@ func ensureWorkspaceTrusted(azureYamlPath string) error { response = strings.ToLower(strings.TrimSpace(response)) if response == "n" || response == "no" { - return fmt.Errorf("workspace not trusted — exiting without executing any commands") + return fmt.Errorf("workspace not trusted, exiting without executing any commands") } if err := store.TrustWorkspace(projectDir); err != nil { diff --git a/cli/src/gen/proto/azdapp/v1/common.pb.go b/cli/src/gen/proto/azdapp/v1/common.pb.go index 25ecb6a81..0a795040d 100644 --- a/cli/src/gen/proto/azdapp/v1/common.pb.go +++ b/cli/src/gen/proto/azdapp/v1/common.pb.go @@ -319,7 +319,7 @@ func (ServiceStatus) EnumDescriptor() ([]byte, []int) { // HealthState mirrors internal/healthcheck health states. // // Wire stability: enum values are append-only. DEGRADED was added after the -// initial draft when wiring HealthService — the existing dashboard summary +// initial draft when wiring HealthService; the existing dashboard summary // distinguishes degraded from unhealthy, and dropping that distinction would // silently lose information. Older clients that don't recognise the value // will see HEALTH_STATE_UNSPECIFIED (proto3 unknown-enum semantics) rather diff --git a/cli/src/internal/azure/standalone_logs.go b/cli/src/internal/azure/standalone_logs.go index c061e6f68..8d6d7aae3 100644 --- a/cli/src/internal/azure/standalone_logs.go +++ b/cli/src/internal/azure/standalone_logs.go @@ -127,7 +127,7 @@ func getServiceNameMap(_ string) map[string]string { // Reject compound suffixes like IMAGE_NAME or RESOURCE_NAME by ensuring // the captured group does not itself end with a known qualifier. svcPart := m[1] - // Only exclude _IMAGE suffix – this is the only known compound property + // Only exclude _IMAGE suffix; this is the only known compound property // that produces a SERVICE_*_NAME env var (SERVICE__IMAGE_NAME). // Broader filtering would reject legitimate service names like "my-resource". // See: serviceinfo/serviceinfo.go, dashboard/azure_logs_config.go for consistency. diff --git a/cli/src/internal/dashboard/dashboard_test.go b/cli/src/internal/dashboard/dashboard_test.go index 6163769d6..c943ce281 100644 --- a/cli/src/internal/dashboard/dashboard_test.go +++ b/cli/src/internal/dashboard/dashboard_test.go @@ -118,7 +118,7 @@ func TestSecurityHeaders_CSPTokens(t *testing.T) { t.Fatal("Content-Security-Policy header must be set") } - // script-src must be 'self' only — no eval, no inline scripts. + // script-src must be 'self' only, no eval, no inline scripts. if !strings.Contains(csp, "script-src 'self'") { t.Errorf("CSP must contain \"script-src 'self'\"; got: %s", csp) } @@ -340,7 +340,7 @@ func TestHostAllow_ViaBuiltHandler(t *testing.T) { t.Run("no port in Host header works correctly", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Host = "localhost" // no port — SplitHostPort fails, raw host used + req.Host = "localhost" // No port; SplitHostPort fails and the raw host is used. rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { @@ -372,10 +372,10 @@ func TestTimeoutContext(t *testing.T) { // // Acceptance criteria: // -// AC1 — /api/shutdown receives Cache-Control: no-store -// AC2 — Connect-RPC path (/azdapp.v1.*) receives Cache-Control: no-store -// AC3 — root path (/) does NOT receive Cache-Control: no-store -// AC4 — static asset paths do NOT receive Cache-Control: no-store +// AC1, /api/shutdown receives Cache-Control: no-store +// AC2, Connect-RPC path (/azdapp.v1.*) receives Cache-Control: no-store +// AC3, root path (/) does NOT receive Cache-Control: no-store +// AC4, static asset paths do NOT receive Cache-Control: no-store func TestSecurityHeaders_CacheControl(t *testing.T) { tests := []struct { name string @@ -389,9 +389,9 @@ func TestSecurityHeaders_CacheControl(t *testing.T) { // AC2: Connect-RPC paths (/azdapp.v1./). {"connect_rpc_lifecycle_sets_no_store", "/azdapp.v1.LifecycleService/Ping", true}, {"connect_rpc_health_sets_no_store", "/azdapp.v1.HealthService/GetHealth", true}, - // AC3: SPA root — must NOT get no-store (serveIndex handles it separately). + // AC3: SPA root, must NOT get no-store (serveIndex handles it separately). {"root_does_not_set_no_store", "/", false}, - // AC4: static assets — must NOT get no-store so they remain cacheable. + // AC4: static assets must NOT get no-store so they remain cacheable. {"app_js_does_not_set_no_store", "/app.js", false}, {"favicon_does_not_set_no_store", "/favicon.ico", false}, } diff --git a/cli/src/internal/dashboard/envinfo/envinfo.go b/cli/src/internal/dashboard/envinfo/envinfo.go index 4d7e10daf..a416674d6 100644 --- a/cli/src/internal/dashboard/envinfo/envinfo.go +++ b/cli/src/internal/dashboard/envinfo/envinfo.go @@ -107,7 +107,7 @@ func runningOnVsCodeDesktop(ctx context.Context) bool { if err != nil { // Failure to spawn `code` (missing CLI, sandbox, etc.) is treated // as "not desktop" so the dashboard falls back to URL rewriting. - // That is the safer default — it produces working URLs in browser + // That is the safer default; it produces working URLs in browser // Codespaces at the cost of an unnecessary rewrite when the CLI // is just absent. return false diff --git a/cli/src/internal/dashboard/httputil.go b/cli/src/internal/dashboard/httputil.go index b09d58d01..a6909da98 100644 --- a/cli/src/internal/dashboard/httputil.go +++ b/cli/src/internal/dashboard/httputil.go @@ -10,13 +10,13 @@ import ( // These headers provide defense-in-depth against common web attacks (CWE-693). // // CSP notes: -// - script-src: 'self' only — no eval, no inline scripts. +// - script-src: 'self' only, no eval, no inline scripts. // - style-src: 'unsafe-inline' is retained because multiple React components // use style={} props (e.g. progress bars, panel widths, animation hints). // Those props render as HTML style= attributes, which the browser blocks // without this token. Removing it requires refactoring ~10 components; // that work is tracked separately and out of scope for this patch. -// - connect-src: 'self' only — the legacy WebSocket endpoint +// - connect-src: 'self' only; the legacy WebSocket endpoint // (ws://localhost:* / wss://localhost:*) was removed; Connect-RPC uses HTTP. // - object-src, base-uri, form-action: 'none' for defence-in-depth. // @@ -66,7 +66,7 @@ func hostAllow(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { host, _, err := net.SplitHostPort(r.Host) if err != nil { - // No port present (or other parse error) — use the raw Host value. + // No port present (or other parse error); use the raw Host value. host = r.Host } switch host { diff --git a/cli/src/internal/dashboard/server_core.go b/cli/src/internal/dashboard/server_core.go index a9c5cf4aa..1eb325ba8 100644 --- a/cli/src/internal/dashboard/server_core.go +++ b/cli/src/internal/dashboard/server_core.go @@ -153,8 +153,8 @@ func (s *Server) Start() (string, error) { ReadHeaderTimeout: 10 * time.Second, // WriteTimeout must be 0 (disabled) because Connect-RPC server-streaming // handlers (StreamHealth, StreamLocalLogs, StreamBroadcast) are long-lived. - // Go's WriteTimeout is an absolute deadline from request header read — not - // a per-write idle timeout — so any non-zero value kills streams after that + // Go's WriteTimeout is an absolute deadline from request header read, not + // a per-write idle timeout, so any non-zero value kills streams after that // duration, causing ERR_INCOMPLETE_CHUNKED_ENCODING on the client. // Security: this server binds to 127.0.0.1 only (not internet-facing). // ReadHeaderTimeout guards against slowloris; IdleTimeout reclaims idle @@ -275,7 +275,7 @@ func (s *Server) Stop() error { // so any in-flight stream handlers have already started exiting. s.broadcast.StopAll() - // Now safe — no more handlers running + // Now safe, no more handlers running if s.configClient != nil { s.configClient.Close() s.configClient = nil diff --git a/cli/src/internal/dashboard/server_port_mgmt.go b/cli/src/internal/dashboard/server_port_mgmt.go index 234582d65..0dd138479 100644 --- a/cli/src/internal/dashboard/server_port_mgmt.go +++ b/cli/src/internal/dashboard/server_port_mgmt.go @@ -98,7 +98,7 @@ func loadOrCreateNonce(projectHash string) (string, error) { if nonce := strings.TrimSpace(string(data)); len(nonce) == 32 { return nonce, nil } - // Corrupt or truncated — fall through to regenerate. + // Corrupt or truncated; fall through to regenerate. } // Generate 128 bits of randomness. diff --git a/cli/src/internal/dashboard/server_port_test.go b/cli/src/internal/dashboard/server_port_test.go index 962c60019..d2633ef3d 100644 --- a/cli/src/internal/dashboard/server_port_test.go +++ b/cli/src/internal/dashboard/server_port_test.go @@ -240,7 +240,7 @@ func TestPersistentDashboardPort_PortConflictFallback(t *testing.T) { // ── Nonce tests ─────────────────────────────────────────────────────────────── // TestPortFileNonce_PathNotPredictableFromDir verifies that the port file path -// cannot be derived from the project directory alone — it must contain a nonce +// cannot be derived from the project directory alone; it must contain a nonce // beyond the deterministic project hash (CWE-340). func TestPortFileNonce_PathNotPredictableFromDir(t *testing.T) { nonceDirBase = t.TempDir() @@ -249,7 +249,7 @@ func TestPortFileNonce_PathNotPredictableFromDir(t *testing.T) { projectDir := t.TempDir() hash := azdconfig.ProjectHash(projectDir) - // "predictable" path — the old, deterministic format with no nonce. + // "predictable" path; the old, deterministic format with no nonce. predictedPath := filepath.Join(os.TempDir(), fmt.Sprintf(".azd-app-dashboard-%s.port", hash)) path, err := portFilePath(projectDir) diff --git a/cli/src/internal/dashboard/server_routes.go b/cli/src/internal/dashboard/server_routes.go index 6d96e4676..6fe4f762c 100644 --- a/cli/src/internal/dashboard/server_routes.go +++ b/cli/src/internal/dashboard/server_routes.go @@ -31,7 +31,7 @@ const sessionTokenPlaceholder = `` // // Safety: rpc.GenerateSessionToken returns a hex-encoded random string // (characters 0-9 and a-f only). Those characters are unconditionally safe -// inside an HTML attribute value — no further encoding is needed. If the +// inside an HTML attribute value; no further encoding is needed. If the // placeholder is absent the original slice is returned unchanged; the client // will read an empty string and every RPC call will be rejected by the // server-side auth interceptor, which is the safe failure mode (CWE-306). @@ -61,7 +61,7 @@ func (s *Server) shutdownOriginAllowed(r *http.Request) bool { if fetchSite := r.Header.Get("Sec-Fetch-Site"); fetchSite != "" { return fetchSite == "same-origin" } - // Sec-Fetch-Site absent — fall back to the Origin header. + // Sec-Fetch-Site absent; fall back to the Origin header. origin := r.Header.Get("Origin") if origin == "" { return false @@ -133,7 +133,7 @@ func (s *Server) setupRoutes() { // azure.yaml writes with the parallel REST handlers. Azure: newAzureStoreFuncs(s), }); err != nil { - // An empty SessionToken is a programming error — the server always + // An empty SessionToken is a programming error; the server always // generates one at startup. Panicking here surfaces the misconfiguration // immediately rather than serving every request unauthenticated. panic("rpc.Mount: " + err.Error()) @@ -143,12 +143,12 @@ func (s *Server) setupRoutes() { // Used by `azd app stop` from a separate terminal. // // Two-factor check (CWE-352): - // 1. Origin proof — shutdownOriginAllowed rejects cross-origin callers. - // 2. Session token — ConstantTimeCompare rejects unauthenticated callers. + // 1. Origin proof: shutdownOriginAllowed rejects cross-origin callers. + // 2. Session token: ConstantTimeCompare rejects unauthenticated callers. // // Origin proof is evaluated first so that cross-origin requests are rejected // even if the attacker somehow obtains a valid token (e.g. via MITM on the - // loopback — mitigated by hostAllow, but defence-in-depth applies). + // loopback, mitigated by hostAllow, but defence-in-depth applies). s.mux.HandleFunc("POST /api/shutdown", func(w http.ResponseWriter, r *http.Request) { if !s.shutdownOriginAllowed(r) { http.Error(w, "forbidden", http.StatusForbidden) @@ -206,7 +206,7 @@ func (s *Server) setupRoutes() { // Try to open the file from the embedded FS. f, err := distFS.Open(strings.TrimPrefix(path, "/")) if err != nil { - // File doesn't exist — serve index.html for client-side routing. + // File doesn't exist, serve index.html for client-side routing. // This handles routes like /console, /services, /environment, /metrics. if indexReadErr != nil { http.NotFound(w, r) diff --git a/cli/src/internal/dashboard/server_routes_test.go b/cli/src/internal/dashboard/server_routes_test.go index 235eecda7..2dfd0962f 100644 --- a/cli/src/internal/dashboard/server_routes_test.go +++ b/cli/src/internal/dashboard/server_routes_test.go @@ -157,7 +157,7 @@ func TestShutdownOriginCheck(t *testing.T) { }) t.Run("AC4_OriginFallback_Localhost_Returns200", func(t *testing.T) { - // Criterion 4: Origin fallback — http://localhost: accepted + // Criterion 4: Origin fallback, http://localhost: accepted s := newSrv() req := httptest.NewRequest(http.MethodPost, "/api/shutdown", nil) req.Header.Set("X-Session-Token", validToken) @@ -170,7 +170,7 @@ func TestShutdownOriginCheck(t *testing.T) { }) t.Run("AC5_OriginFallback_127001_Returns200", func(t *testing.T) { - // Criterion 5: Origin fallback — http://127.0.0.1: accepted + // Criterion 5: Origin fallback, http://127.0.0.1: accepted s := newSrv() req := httptest.NewRequest(http.MethodPost, "/api/shutdown", nil) req.Header.Set("X-Session-Token", validToken) @@ -183,7 +183,7 @@ func TestShutdownOriginCheck(t *testing.T) { }) t.Run("AC6_OriginFallback_CrossSite_Returns403", func(t *testing.T) { - // Criterion 6: Origin fallback — cross-site Origin rejected + // Criterion 6: Origin fallback, cross-site Origin rejected s := newSrv() req := httptest.NewRequest(http.MethodPost, "/api/shutdown", nil) req.Header.Set("X-Session-Token", validToken) @@ -274,7 +274,7 @@ func TestShutdownOriginCheck(t *testing.T) { func TestIndexHTMLInjectsSessionToken(t *testing.T) { const token = "cafebabe12345678cafebabe12345678" - // Minimal index.html with the placeholder — mirrors the real template. + // Minimal index.html with the placeholder, mirrors the real template. indexHTML := []byte(``) tokenized := injectSessionToken(indexHTML, token) diff --git a/cli/src/internal/dashboard/service_ops_rpc.go b/cli/src/internal/dashboard/service_ops_rpc.go index 63ca2feff..84f30eefb 100644 --- a/cli/src/internal/dashboard/service_ops_rpc.go +++ b/cli/src/internal/dashboard/service_ops_rpc.go @@ -130,7 +130,7 @@ func (s *Server) RunServiceOperation(ctx context.Context, op serviceOperation, n // one broadcast update afterward (matching legacy behavior). Per-service // failures are folded into the return string; the Go error return is // reserved for catastrophic failures that prevented the run from happening -// at all (currently none — the bulk runner cannot itself fail). +// at all (currently none; the bulk runner cannot itself fail). func (s *Server) runBulk( ctx context.Context, handler *serviceOperationHandler, diff --git a/cli/src/internal/docker/exec.go b/cli/src/internal/docker/exec.go index 76fe7fea3..a5e8ca7ea 100644 --- a/cli/src/internal/docker/exec.go +++ b/cli/src/internal/docker/exec.go @@ -161,7 +161,7 @@ func (c *ExecClient) ConnectNetwork(network, container string, aliases []string) cmd.Stderr = &stderr if err := cmd.Run(); err != nil { stderrStr := strings.TrimSpace(stderr.String()) - // Already attached — treat as success (idempotent). + // Already attached, treat as success (idempotent). if isAlreadyConnectedError(stderrStr) { return nil } diff --git a/cli/src/internal/executor/executor.go b/cli/src/internal/executor/executor.go index 77110f8b3..c79565b77 100644 --- a/cli/src/internal/executor/executor.go +++ b/cli/src/internal/executor/executor.go @@ -27,7 +27,7 @@ func RunWithContext(ctx context.Context, name string, args []string, dir string) cmd.Stdout = io.Discard cmd.Stderr = io.Discard cmd.Stdin = nil - // cmd.Env is nil — child inherits full parent environment (including azd env values) + // cmd.Env is nil, child inherits full parent environment (including azd env values) return cmd.Run() } diff --git a/cli/src/internal/healthcheck/checker_process.go b/cli/src/internal/healthcheck/checker_process.go index 2e2920d6c..7c85b1fe7 100644 --- a/cli/src/internal/healthcheck/checker_process.go +++ b/cli/src/internal/healthcheck/checker_process.go @@ -310,6 +310,6 @@ func parseErrorDetailsFromBody(body []byte) string { } } - // JSON parsing failed or no recognised error field — sanitize and return body text + // JSON parsing failed or no recognised error field; sanitize and return body text return sanitizeResponseBody(string(body), 200) } diff --git a/cli/src/internal/rpc/azure_stores.go b/cli/src/internal/rpc/azure_stores.go index 822770e11..6312fd485 100644 --- a/cli/src/internal/rpc/azure_stores.go +++ b/cli/src/internal/rpc/azure_stores.go @@ -15,13 +15,13 @@ import ( // without faking the whole Azure stack. // // Sub-store rationale (per ADR-0001 commit-B-1 plan): -// - AzureConfigStore — azure.yaml read / write helpers, all guarded by +// - AzureConfigStore: azure.yaml read / write helpers, all guarded by // dashboard's azureYamlMu in production. -// - AzureCatalog — read-only metadata about workspaces, tables, default +// - AzureCatalog: read-only metadata about workspaces, tables, default // queries; no I/O against Azure. -// - AzureLogsClient — Azure-side reads (fetch logs, verify workspace, +// - AzureLogsClient: Azure-side reads (fetch logs, verify workspace, // create realtime streamer / credentials). -// - AzureDiagnostics — multi-step diagnostics + setup probes that return +// - AzureDiagnostics: multi-step diagnostics + setup probes that return // opaque JSON (passed through *structpb.Struct on the wire). type AzureService interface { AzureConfigStore diff --git a/cli/src/internal/rpc/lifecycle_test.go b/cli/src/internal/rpc/lifecycle_test.go index f751a28ca..95ed0995e 100644 --- a/cli/src/internal/rpc/lifecycle_test.go +++ b/cli/src/internal/rpc/lifecycle_test.go @@ -214,7 +214,7 @@ func TestStreamBroadcastClientCancelExitsCleanly(t *testing.T) { // Cancel from a goroutine once the subscription is active. Cancel // before the first Send aborts the in-flight HTTP request, which // connect-go surfaces as either an error from StreamBroadcast or - // from stream.Receive — both are valid client-cancel paths. + // from stream.Receive; both are valid client-cancel paths. go func() { deadline := time.Now().Add(3 * time.Second) for time.Now().Before(deadline) && mgr.Count() == 0 { diff --git a/cli/src/internal/rpc/mode.go b/cli/src/internal/rpc/mode.go index 121243bf2..3c726cc88 100644 --- a/cli/src/internal/rpc/mode.go +++ b/cli/src/internal/rpc/mode.go @@ -15,7 +15,7 @@ import ( // access to the current log source mode. dashboard.Server satisfies it // via its modeMu/currentMode pair; tests inject an in-memory stub. // -// Get/Set semantics are intentionally synchronous — callers must hold +// Get/Set semantics are intentionally synchronous; callers must hold // no other locks. The store implementation is responsible for its own // concurrency control (Server uses a sync.RWMutex). type ModeStore interface { @@ -146,7 +146,7 @@ type azureConfigSnapshot struct { // probeAzureConfig parses azure.yaml and projects the result into the // snapshot the wire types want. Errors are encoded into the snapshot // (not returned) because both RPCs need to surface a partial answer -// even when the manifest is unreadable — telling the user "I can't +// even when the manifest is unreadable, telling the user "I can't // read azure.yaml" is more useful than a generic Internal error. func (h *ModeHandler) probeAzureConfig() azureConfigSnapshot { snap := azureConfigSnapshot{ @@ -186,7 +186,7 @@ func logModeToProto(m service.LogMode) v1.LogMode { // protoToLogMode converts a proto LogMode to the internal string-based // LogMode. UNSPECIFIED is rejected (returns an error) because every -// SetMode caller must declare a concrete intent — silently treating +// SetMode caller must declare a concrete intent, silently treating // UNSPECIFIED as a default would mask client bugs. func protoToLogMode(m v1.LogMode) (service.LogMode, error) { switch m { diff --git a/cli/src/internal/rpc/project.go b/cli/src/internal/rpc/project.go index 765772439..656b35d78 100644 --- a/cli/src/internal/rpc/project.go +++ b/cli/src/internal/rpc/project.go @@ -71,7 +71,7 @@ func (h *ProjectHandler) GetProject( // dashboard's project context is broken (missing azure.yaml, // malformed manifest, IO error). A more granular code (e.g. // FailedPrecondition for "no azure.yaml in this tree") would be - // a behavior change vs. the legacy 500 — defer that until a + // a behavior change vs. the legacy 500, defer that until a // concrete consumer needs it. return nil, connect.NewError(connect.CodeInternal, err) } diff --git a/cli/src/internal/rpc/validation.go b/cli/src/internal/rpc/validation.go index 712837084..c5363cbd4 100644 --- a/cli/src/internal/rpc/validation.go +++ b/cli/src/internal/rpc/validation.go @@ -36,9 +36,9 @@ func init() { // connect.CodeInvalidArgument. // // Two invariants it enforces: -// 1. Size ≤ maxQueryBytes — prevents gigantic payloads from bloating or +// 1. Size ≤ maxQueryBytes, prevents gigantic payloads from bloating or // corrupting azure.yaml. -// 2. No non-printable bytes (except \n, \r, \t) — prevents null-byte +// 2. No non-printable bytes (except \n, \r, \t), prevents null-byte // injection and other control-code tricks that could confuse YAML // parsers or downstream KQL evaluation (CWE-94). func validateQuery(query string) error { diff --git a/cli/src/internal/rpc/validation_test.go b/cli/src/internal/rpc/validation_test.go index dccd5b089..ca38a2134 100644 --- a/cli/src/internal/rpc/validation_test.go +++ b/cli/src/internal/rpc/validation_test.go @@ -170,7 +170,7 @@ func TestSaveAzureLogConfig_ValidationGateBlocksStore(t *testing.T) { } h := newStubHandler(funcs) - // Unknown table — store must not be called. + // Unknown table; store must not be called. _, err := h.SaveAzureLogConfig(context.Background(), connect.NewRequest(&v1.SaveAzureLogConfigRequest{ Service: "api", @@ -223,7 +223,7 @@ func TestSaveServiceQuery_NonPrintableByteRejected(t *testing.T) { } // ============================================================================= -// auditMutation — smoke: must not panic or error; slog output verified +// auditMutation, smoke: must not panic or error; slog output verified // structurally by the slog default handler. // ============================================================================= diff --git a/cli/src/internal/security/containment.go b/cli/src/internal/security/containment.go index 75adcc54e..3ce1d3717 100644 --- a/cli/src/internal/security/containment.go +++ b/cli/src/internal/security/containment.go @@ -69,14 +69,14 @@ func ValidatePathContainment(path, baseDir string) (string, error) { // Follow symlinks when the path already exists so that symlink-based // escapes (e.g. a symlink inside baseDir pointing outside it) are caught. // For paths that do not yet exist (service project dirs to be created), - // resolve symlinks on the parent directory only — this handles the macOS + // resolve symlinks on the parent directory only; this handles the macOS // /var → /private/var symlink that would otherwise cause a false mismatch. if realPath, symlinkErr := filepath.EvalSymlinks(absPath); symlinkErr == nil { absPath = realPath } else if !os.IsNotExist(symlinkErr) { return "", fmt.Errorf("cannot resolve symbolic links for %q: %w", path, symlinkErr) } else { - // Path doesn't exist — resolve parent to normalise platform symlinks. + // Path doesn't exist, so resolve parent to normalise platform symlinks. parent := filepath.Dir(absPath) if realParent, parentErr := filepath.EvalSymlinks(parent); parentErr == nil { absPath = filepath.Join(realParent, filepath.Base(absPath)) @@ -86,7 +86,7 @@ func ValidatePathContainment(path, baseDir string) (string, error) { // --- Containment check via filepath.Rel --- // filepath.Rel(base, path) returns ".." or a string starting with "../" // ("..\" on Windows) whenever path is outside base. This is the - // authoritative check — it works correctly even when no ".." literals + // authoritative check; it works correctly even when no ".." literals // appear in the individual inputs. rel, relErr := filepath.Rel(absBase, absPath) if relErr != nil { diff --git a/cli/src/internal/security/containment_test.go b/cli/src/internal/security/containment_test.go index ada7e3b1c..936bbf71d 100644 --- a/cli/src/internal/security/containment_test.go +++ b/cli/src/internal/security/containment_test.go @@ -61,7 +61,7 @@ func TestValidatePathContainment_AcceptedPaths(t *testing.T) { baseDir: root, }, { - // Non-existent child path — creation is allowed inside root + // Non-existent child path; creation is allowed inside root name: "non-existent child allowed", path: filepath.Join(root, "new-service"), baseDir: root, @@ -123,7 +123,7 @@ func TestValidatePathContainment_RejectedPaths(t *testing.T) { errMsg: "escapes project root", }, { - // Single step up — catches ../sibling style + // Single step up, catches ../sibling style name: "one level up from root", path: filepath.Clean(filepath.Join(root, "..")), baseDir: root, diff --git a/cli/src/internal/service/container_config.go b/cli/src/internal/service/container_config.go index 4bbbc8e99..415c6e5c4 100644 --- a/cli/src/internal/service/container_config.go +++ b/cli/src/internal/service/container_config.go @@ -125,12 +125,12 @@ func resolveVolumeSpec(spec, projectDir string) (string, error) { source, rest, found := splitVolumeSource(trimmed) if !found { - // Anonymous volume (container path only) — pass through. + // Anonymous volume (container path only), pass through. return trimmed, nil } if isNamedVolume(source) { - // Docker-managed named volume — pass through. + // Docker-managed named volume, pass through. return trimmed, nil } diff --git a/cli/src/internal/service/container_topology_integration_test.go b/cli/src/internal/service/container_topology_integration_test.go index c93654f5b..86fe4603c 100644 --- a/cli/src/internal/service/container_topology_integration_test.go +++ b/cli/src/internal/service/container_topology_integration_test.go @@ -27,8 +27,8 @@ func dockerInspectField(t *testing.T, id, format string) string { // TestStartContainerService_WebsiteStyleTopology is the AC8 goal-challenge: it // runs an azurite-like container through the FULL runtime path // (DetectServiceRuntime -> StartContainerService -> docker run) exercising the -// website's mechanisms together — a string command, THREE published ports, a -// named volume, and attachment to the shared project network — and verifies via +// website's mechanisms together, a string command, THREE published ports, a +// named volume, and attachment to the shared project network, and verifies via // `docker inspect` that they all took effect. func TestStartContainerService_WebsiteStyleTopology(t *testing.T) { checkDockerAvailable(t) diff --git a/cli/src/internal/service/detector.go b/cli/src/internal/service/detector.go index 769e807ed..62b3994d4 100644 --- a/cli/src/internal/service/detector.go +++ b/cli/src/internal/service/detector.go @@ -27,7 +27,7 @@ const ( // DetectServiceRuntime determines how to run a service based on its configuration and project structure. func DetectServiceRuntime(serviceName string, service Service, usedPorts map[int]bool, azureYamlDir string, runtimeMode string) (*ServiceRuntime, error) { - // Container services (image/docker.image) run as containers — UNLESS the + // Container services (image/docker.image) run as containers, UNLESS the // service opts into running as a local process via an explicit local // `command` or `type: process`. In that case its `docker.*`/image is // deploy-only and `azd app run` runs the command as a process. @@ -170,8 +170,8 @@ func DetectServiceRuntime(serviceName string, service Service, usedPorts map[int } // Copy environment variables from service config. A service run as a local - // process — including a docker/appservice/containerapp service routed to a - // local `command` or `type: process` (RunsAsLocalProcess) — must still + // process, including a docker/appservice/containerapp service routed to a + // local `command` or `type: process` (RunsAsLocalProcess), must still // receive its azure.yaml `environment:` block. Orchestration resolves each // service's effective env from runtime.Env (see orchestrator.go), so without // this the block (e.g. APP_BASE_URL, NODE_ENV, POSTGRES_URL) is silently diff --git a/cli/src/internal/service/detector_container_test.go b/cli/src/internal/service/detector_container_test.go index 9eee97de2..877052d83 100644 --- a/cli/src/internal/service/detector_container_test.go +++ b/cli/src/internal/service/detector_container_test.go @@ -114,7 +114,7 @@ func TestDetectServiceRuntime_DockerServiceWithCommandRunsAsProcess(t *testing.T func TestDetectServiceRuntime_DockerServiceWithoutCommandStaysContainer(t *testing.T) { // Without a local command, a docker.image service keeps the (deploy default) - // container behavior — unchanged. + // container behavior, unchanged. svc := Service{ Host: "containerapp", Language: "docker", diff --git a/cli/src/internal/service/detector_test.go b/cli/src/internal/service/detector_test.go index 81d2e4db9..5c45e8b49 100644 --- a/cli/src/internal/service/detector_test.go +++ b/cli/src/internal/service/detector_test.go @@ -162,8 +162,8 @@ services: } // TestDetectServiceRuntime_LocalProcessCopiesEnvironment verifies that a service -// which runs as a local process — including a docker/appservice service routed to -// a local `command` (RunsAsLocalProcess) — receives its azure.yaml `environment:` +// which runs as a local process, including a docker/appservice service routed to +// a local `command` (RunsAsLocalProcess), receives its azure.yaml `environment:` // block in runtime.Env. Regression test: the env copy previously existed only in // the container path, so local-process services silently dropped their env (e.g. // APP_BASE_URL), which surfaced as a masked 500 in the running application. @@ -180,7 +180,7 @@ func TestDetectServiceRuntime_LocalProcessCopiesEnvironment(t *testing.T) { } // A docker/appservice service with an explicit local `command` runs as a - // local process (RunsAsLocalProcess), not a container — so it takes the + // local process (RunsAsLocalProcess), not a container, so it takes the // non-container path in DetectServiceRuntime. azureYamlContent := `name: test-app services: diff --git a/cli/src/internal/service/environment.go b/cli/src/internal/service/environment.go index 46b0aaa92..28c3a00fa 100644 --- a/cli/src/internal/service/environment.go +++ b/cli/src/internal/service/environment.go @@ -76,7 +76,7 @@ func resolveEnvironmentWithSources(ctx context.Context, service Service, azureEn env[key] = value } - // Start with full OS environment — child processes inherit everything from the parent. + // Start with full OS environment, child processes inherit everything from the parent. // When running as an azd extension, the parent azd process injects all environment // values (AZD_SERVER, AZD_ACCESS_TOKEN, AZURE_*, SERVICE_*, custom outputs) into this // process's environment. Services need these to function correctly. diff --git a/cli/src/internal/service/logbuffer.go b/cli/src/internal/service/logbuffer.go index 449b4eeac..557b938bc 100644 --- a/cli/src/internal/service/logbuffer.go +++ b/cli/src/internal/service/logbuffer.go @@ -620,7 +620,7 @@ func (lb *LogBuffer) Close() error { // inferLogLevel attempts to infer the log level from a log message. func inferLogLevel(message string, isStderr bool) LogLevel { - // Structured logs (JSON with an explicit level/severity) are authoritative — + // Structured logs (JSON with an explicit level/severity) are authoritative, so // honor the emitter's own level instead of guessing from the text. This is // what keeps a line like {"level":"info","role":"trace-worker"} at INFO // rather than matching the substring "trace" and dropping it to DEBUG. diff --git a/cli/src/internal/service/logmanager_test.go b/cli/src/internal/service/logmanager_test.go index 8fe0d41f9..68e3b22a1 100644 --- a/cli/src/internal/service/logmanager_test.go +++ b/cli/src/internal/service/logmanager_test.go @@ -363,7 +363,7 @@ func TestLogManagerOnBufferAdded(t *testing.T) { ch := lm.OnBufferAdded() defer lm.RemoveBufferListener(ch) - // Create a buffer — the listener should receive the service name. + // Create a buffer; the listener should receive the service name. _, err = lm.CreateBuffer("svc-alpha", 100, false) if err != nil { t.Fatalf("CreateBuffer() error = %v", err) @@ -387,7 +387,7 @@ func TestLogManagerOnBufferAdded(t *testing.T) { // expected: no notification } - // Create a second buffer — should notify. + // Create a second buffer; should notify. _, err = lm.CreateBuffer("svc-beta", 100, false) if err != nil { t.Fatalf("CreateBuffer() error = %v", err) @@ -411,7 +411,7 @@ func TestLogManagerRemoveBufferListener(t *testing.T) { ch := lm.OnBufferAdded() - // Remove listener, then create a buffer — should NOT receive. + // Remove listener, then create a buffer; should NOT receive. lm.RemoveBufferListener(ch) _, _ = lm.CreateBuffer("orphan-svc", 100, false) diff --git a/cli/src/internal/service/parser.go b/cli/src/internal/service/parser.go index 04ef78425..d7867fdb6 100644 --- a/cli/src/internal/service/parser.go +++ b/cli/src/internal/service/parser.go @@ -48,7 +48,7 @@ func ParseAzureYaml(workingDir string) (*AzureYaml, error) { // silently removes ".." components from the string, so security.ValidatePath // (which searches for ".." literals) would pass even when the resolved path // escapes the project root. We use ValidatePathContainment instead, which - // uses filepath.Rel on the fully-resolved absolute paths — the only correct + // uses filepath.Rel on the fully-resolved absolute paths; the only correct // approach. azureYamlDir := filepath.Dir(azureYamlPath) // Resolve symlinks on azureYamlDir so that paths joined below are in the diff --git a/cli/src/internal/service/parser_security_test.go b/cli/src/internal/service/parser_security_test.go index ff07fbb2e..61655b88c 100644 --- a/cli/src/internal/service/parser_security_test.go +++ b/cli/src/internal/service/parser_security_test.go @@ -51,7 +51,7 @@ services: // a service project set to an absolute path outside the project root must be rejected. func TestParseAzureYaml_ValidatePath_AbsoluteOutsideRootRejected(t *testing.T) { root := t.TempDir() - // Use the parent of root as the escape target — it always exists. + // Use the parent of root as the escape target; it always exists. outsideDir := filepath.ToSlash(filepath.Dir(root)) content := "name: test\nservices:\n api:\n project: " + outsideDir + "\n host: appservice\n" diff --git a/cli/src/internal/service/types.go b/cli/src/internal/service/types.go index 9b77608e7..42dadfc03 100644 --- a/cli/src/internal/service/types.go +++ b/cli/src/internal/service/types.go @@ -463,7 +463,7 @@ func (s *Service) GetContainerImage() string { // container (a prebuilt image/emulator, whose `command` is a container command // override). A service that only builds an image for deployment (`docker.*`) // runs locally as a process when an explicit local `command` or `type: process` -// is configured — its `docker.*`/image is then used only by `azd deploy`. +// is configured, its `docker.*`/image is then used only by `azd deploy`. func (s *Service) RunsAsLocalProcess() bool { if s.Image != "" { return false diff --git a/cli/src/internal/skills/azd-app-onboard/SKILL.md b/cli/src/internal/skills/azd-app-onboard/SKILL.md index 4d3268b05..e71cba824 100644 --- a/cli/src/internal/skills/azd-app-onboard/SKILL.md +++ b/cli/src/internal/skills/azd-app-onboard/SKILL.md @@ -336,7 +336,7 @@ azd app run --service frontend | Issue | Solution | |-------|----------| -| Port conflict | Use `{port}` placeholder in command — azd app manages ports automatically | +| Port conflict | Use `{port}` placeholder in command, azd app manages ports automatically | | Service not detected | Ensure service has a recognizable entry point (package.json, main.py, go.mod, etc.) | | Health check failing | Verify the endpoint returns HTTP 200 and the path matches `healthcheck.path` | | Docker service won't start | Check `docker` is running with `azd app reqs` | diff --git a/cli/src/internal/skills/azd-app/SKILL.md b/cli/src/internal/skills/azd-app/SKILL.md index a585320d0..0cb010709 100644 --- a/cli/src/internal/skills/azd-app/SKILL.md +++ b/cli/src/internal/skills/azd-app/SKILL.md @@ -140,12 +140,12 @@ Environment variables are resolved at service startup and injected into each ser azd-app supports lifecycle hooks defined in `azure.yaml`: -- **prerun** — runs before services start -- **postrun** — runs after services are ready -- **prestop** — runs before services are stopped (e.g., drain connections, flush caches) -- **poststop** — runs after all services are stopped (e.g., cleanup temp files, remove tunnels) +- **prerun**: runs before services start +- **postrun**: runs after services are ready +- **prestop**: runs before services are stopped (e.g., drain connections, flush caches) +- **poststop**: runs after all services are stopped (e.g., cleanup temp files, remove tunnels) -Hooks can be defined at the project level. Hook failures in `prestop`/`poststop` are non-fatal — services will still be stopped. +Hooks can be defined at the project level. Hook failures in `prestop`/`poststop` are non-fatal; services will still be stopped. ## Supported Languages @@ -158,7 +158,7 @@ Hooks can be defined at the project level. Hook failures in `prestop`/`poststop` | Go | go mod download | go test | go run | | Rust | cargo build | cargo test | cargo run | | PHP | composer install | PHPUnit | php | -| Docker | docker build | — | docker compose | +| Docker | docker build | n/a | docker compose | ## MCP Tools diff --git a/cli/src/internal/trust/workspace_trust.go b/cli/src/internal/trust/workspace_trust.go index 746bb4ec8..bab0d0afe 100644 --- a/cli/src/internal/trust/workspace_trust.go +++ b/cli/src/internal/trust/workspace_trust.go @@ -27,7 +27,7 @@ const ( storeFileName = "trusted-workspaces.json" // storeFileMode is the permission bits applied to the trust-store file. - // Owner-read/write only — trust records contain local paths that should + // Owner-read/write only, trust records contain local paths that should // not be world-readable. storeFileMode = 0o600 @@ -79,10 +79,10 @@ func newTrustStoreAt(storePath string) *TrustStore { // azure.yaml content matches the stored hash. // // Return semantics: -// - (true, nil) — trusted and azure.yaml unchanged -// - (false, nil) — workspace not in the store (never trusted) -// - (false, ErrHashChanged) — in the store but azure.yaml has changed -// - (false, other) — I/O or parse error +// - (true, nil): trusted and azure.yaml unchanged +// - (false, nil): workspace not in the store (never trusted) +// - (false, ErrHashChanged): in the store but azure.yaml has changed +// - (false, other): I/O or parse error func (ts *TrustStore) IsWorkspaceTrusted(projectRoot string) (bool, error) { root, err := normalizePath(projectRoot) if err != nil { diff --git a/cli/tests/projects/integration/containers-test/api/server.js b/cli/tests/projects/integration/containers-test/api/server.js index f8345f58f..92c814dd5 100644 --- a/cli/tests/projects/integration/containers-test/api/server.js +++ b/cli/tests/projects/integration/containers-test/api/server.js @@ -101,7 +101,7 @@ async function testAzurite() { // ========== Cosmos DB ========== async function testCosmos() { try { - // Cosmos emulator uses a self-signed cert — scope TLS bypass to this client only (CWE-295) + // Cosmos emulator uses a self-signed cert, so scope TLS bypass to this client only (CWE-295) const agent = new https.Agent({ rejectUnauthorized: false }); const client = new CosmosClient({ diff --git a/docs/adr/0001-connect-go-transport.md b/docs/adr/0001-connect-go-transport.md index d16aabc20..420a3cb9b 100644 --- a/docs/adr/0001-connect-go-transport.md +++ b/docs/adr/0001-connect-go-transport.md @@ -10,16 +10,16 @@ deciders: jongio ## Status -Implemented. All four PRs have landed: PR 1 (foundation/proto), PR 2 (Connect handlers mounted in parallel), Stage 3 (CLI/MCP converged on typed Connect client), Stage 3.5 (dashboard TS migrated to Connect-ES), and PR 4 (REST + WebSocket surface deleted). The dashboard, CLI cobra commands, and MCP tools all talk to the same Connect handlers. `handleCheckRequirements` remains a subprocess path — no `RequirementsService` exists in the proto yet, so the MCP `reqs` tool still shells out to `azd app reqs`; adding that service is a follow-up ADR. +Implemented. All four PRs have landed: PR 1 (foundation/proto), PR 2 (Connect handlers mounted in parallel), Stage 3 (CLI/MCP converged on typed Connect client), Stage 3.5 (dashboard TS migrated to Connect-ES), and PR 4 (REST + WebSocket surface deleted). The dashboard, CLI cobra commands, and MCP tools all talk to the same Connect handlers. `handleCheckRequirements` remains a subprocess path; no `RequirementsService` exists in the proto yet, so the MCP `reqs` tool still shells out to `azd app reqs`; adding that service is a follow-up ADR. ## Context `azd-app` exposes the same domain operations to four consumers: -1. **Dashboard SPA** (`cli/dashboard/`) — browser, fetches via REST + EventSource. -2. **CLI (cobra commands)** — in-process Go calls today, but the inventory of "what can the dashboard do that the CLI can't" keeps growing. -3. **MCP server** — re-implements business logic to expose tools, drifting from the dashboard surface. -4. **Future TUI / external automation** — would require a third hand-rolled client. +1. **Dashboard SPA** (`cli/dashboard/`): browser, fetches via REST + EventSource. +2. **CLI (cobra commands)**: in-process Go calls today, but the inventory of "what can the dashboard do that the CLI can't" keeps growing. +3. **MCP server**: re-implements business logic to expose tools, drifting from the dashboard surface. +4. **Future TUI / external automation**: would require a third hand-rolled client. The current REST + WebSocket layer in `cli/src/internal/dashboard/` (28 endpoints + 5 streams) is hand-marshalled JSON with no shared schema. Each new endpoint requires: @@ -70,14 +70,14 @@ Each method becomes a distinct RPC with its own request/response types. This is Service split (8 services): -- `LifecycleService` — Ping, GetEnvironment, StreamBroadcast -- `ProjectService` — GetProject -- `ServicesService` — GetServices, Start/Stop/RestartService -- `LogsService` — GetLogs, StreamLocalLogs, classifications CRUD, preferences GET/SAVE -- `HealthService` — GetHealth, StreamHealth, StreamStateTransitions -- `ModeService` — GetMode, SetMode -- `AzureService` — 14 unary + 1 streaming RPC (`StreamAzureLogs`); the stream takes a `bool realtime` flag and the server picks polling or realtime transparently. Responses are framed in a `oneof { LogEntry entry; StreamStatus status; AzureDroppedNotice dropped; }` envelope so clients see entries, mode/health transitions, and overflow notices on a single wire stream. -- `BicepService` — GetBicepTemplate +- `LifecycleService`: Ping, GetEnvironment, StreamBroadcast +- `ProjectService`: GetProject +- `ServicesService`: GetServices, Start/Stop/RestartService +- `LogsService`: GetLogs, StreamLocalLogs, classifications CRUD, preferences GET/SAVE +- `HealthService`: GetHealth, StreamHealth, StreamStateTransitions +- `ModeService`: GetMode, SetMode +- `AzureService`: 14 unary + 1 streaming RPC (`StreamAzureLogs`); the stream takes a `bool realtime` flag and the server picks polling or realtime transparently. Responses are framed in a `oneof { LogEntry entry; StreamStatus status; AzureDroppedNotice dropped; }` envelope so clients see entries, mode/health transitions, and overflow notices on a single wire stream. +- `BicepService`: GetBicepTemplate ### Stream back-pressure (locked in proto comments) @@ -92,14 +92,14 @@ The five server-streaming RPCs each codify a back-pressure policy. These match w | `LifecycleService.StreamBroadcast` | Drop-oldest, disconnect slow consumer | UI hints are best-effort. Slow consumers shed load. | | `HealthService.StreamStateTransitions` | Block producer, 256-event bounded buffer | CRITICAL state changes cannot drop. Producer is rate-limited at source. | -### Service interface extraction — deferred +### Service interface extraction: deferred PR 2 will wire Connect handlers in parallel with the existing REST handlers. The handlers will call the same underlying types the REST handlers call today: - `azdconfig.ConfigClient` is already an interface ✓ - `service.LogManager`, `monitor.StateMonitor`, `azure.LogAnalyticsClient`, `azure.DiagnosticSettingsChecker` are concrete structs -Extracting interfaces around the concrete types is an orthogonal testability concern. It is **not required** for the transport swap. PR 2 calls concrete types directly (mirroring REST). A later PR (3 or post-4) extracts interfaces if the MCP/cobra clients need them — PR 3 adds a typed Connect client that talks to the dashboard over localhost HTTP, so interface extraction only matters for unit tests that want to stand in a fake handler. +Extracting interfaces around the concrete types is an orthogonal testability concern. It is **not required** for the transport swap. PR 2 calls concrete types directly (mirroring REST). A later PR (3 or post-4) extracts interfaces if the MCP/cobra clients need them; PR 3 adds a typed Connect client that talks to the dashboard over localhost HTTP, so interface extraction only matters for unit tests that want to stand in a fake handler. ### Struct usage inventory @@ -133,7 +133,7 @@ Every RPC in the rewritten proto is doc-commented with a citation to the legacy **Raw gRPC.** Browser-hostile. gRPC-Web requires a proxy, no native fetch path, no streaming-from-server without trailers. Connect speaks gRPC + gRPC-Web + Connect protocol on the same handler, so we keep gRPC compatibility for free and add a browser-friendly path. -**tRPC.** TypeScript-only. Forces us to keep a separate Go contract or generate Go from TS — the wrong direction for a Go-rooted project. +**tRPC.** TypeScript-only. Forces us to keep a separate Go contract or generate Go from TS; the wrong direction for a Go-rooted project. **OpenAPI + generated clients.** More codegen surface, weaker streaming story (SSE is a bolt-on, not first-class), and the typed contract lives in YAML/JSON instead of a real schema language. proto + buf gives us breaking-change detection, lint, and a single source of truth across four consumers. @@ -143,9 +143,9 @@ Every RPC in the rewritten proto is doc-commented with a citation to the legacy | PR | Scope | Behavior change? | |---|---|---| -| **PR 1** ✅ | proto schema, codegen, generated stubs, ADR | None — no handlers wired | +| **PR 1** ✅ | proto schema, codegen, generated stubs, ADR | None, no handlers wired | | PR 2 (Stage 2) ✅ | Connect handlers mounted in parallel with existing REST. Dashboard reads via Connect-ES client. REST handlers untouched. AzureService proto rewrite + handler + 4 sub-store decomposition + dashboard migration land in a 3-commit batch. | Dashboard reads via Connect; REST still works for legacy callers | -| PR 3 (Stage 3) ✅ | CLI cobra commands (`app info`, `app logs`) and MCP tool handlers call a typed Connect client over localhost HTTP against the running dashboard process. (The CLI and dashboard are separate processes — "in-process" calls are not possible; the Connect client talks to the same Connect handlers the browser uses.) MCP `info`-shaped tools stop spawning `azd app info` subprocesses; `reqs` remains a subprocess until a dedicated RequirementsService exists. No authentication interceptor is added — the dashboard continues to bind to localhost only, matching the trust posture of the REST surface it replaces. | MCP/CLI converge on the proto contract; subprocess round-trip eliminated for info; legacy REST still available. | +| PR 3 (Stage 3) ✅ | CLI cobra commands (`app info`, `app logs`) and MCP tool handlers call a typed Connect client over localhost HTTP against the running dashboard process. (The CLI and dashboard are separate processes, so "in-process" calls are not possible; the Connect client talks to the same Connect handlers the browser uses.) MCP `info`-shaped tools stop spawning `azd app info` subprocesses; `reqs` remains a subprocess until a dedicated RequirementsService exists. No authentication interceptor is added; the dashboard continues to bind to localhost only, matching the trust posture of the REST surface it replaces. | MCP/CLI converge on the proto contract; subprocess round-trip eliminated for info; legacy REST still available. | | Stage 3.5 ✅ | Remaining TS dashboard REST fetchers cut over to Connect-ES; WebSocket client replaced by Connect `StreamBroadcast` consumer. Landed between Stage 3 and PR 4 so PR 4 could remove the server with no live callers. | Dashboard fully on Connect; zero REST/WS callers remaining | | PR 4 ✅ | Delete REST handlers, WebSocket plumbing, and the dashboard's REST fetchers. `github.com/coder/websocket` dropped from `cli/go.mod`. `BroadcastServiceUpdate` relocated to `server_broadcast.go`; securityHeaders middleware, port-discovery/lifecycle, and `broadcast.Manager` retained. `handleCheckRequirements` remains a subprocess path (no `RequirementsService` in proto yet). | REST + WebSocket surface removed | diff --git a/docs/archive/azd-app-archive-001.md b/docs/archive/azd-app-archive-001.md index 4d32cf815..04c54363b 100644 --- a/docs/archive/azd-app-archive-001.md +++ b/docs/archive/azd-app-archive-001.md @@ -79,7 +79,7 @@ Archived: 2025-12-14 - Added dashboard Go integration coverage for /api/azure/logs defaults/bounds/service filter pass-through and /api/azure/logs/health diagnostics responses using injectable Azure wrappers. ## DONE: Add e2e coverage for logs UX {#add-e2e-logs-ux} -- Added Playwright coverage for logs UX requirements: services dropdown removal, timeframe presets (no 1h option), refresh interval clamping (5s–5m), diagnostics visibility in Azure mode, and host=local override behavior. +- Added Playwright coverage for logs UX requirements: services dropdown removal, timeframe presets (no 1h option), refresh interval clamping (5s to 5m), diagnostics visibility in Azure mode, and host=local override behavior. ## DONE: Coverage and reporting {#coverage-and-reporting} - Dashboard coverage: pnpm test:coverage (script stabilized for Windows file locking). diff --git a/docs/archive/specs-complete-archive-001.md b/docs/archive/specs-complete-archive-001.md index f8f7e9f55..c03a9c156 100644 --- a/docs/archive/specs-complete-archive-001.md +++ b/docs/archive/specs-complete-archive-001.md @@ -226,7 +226,7 @@ The dashboard logs experience currently exposes a services dropdown, a custom 1- ## DONE: 10 Review azlogs diffs and fix regressions {#10-review-azlogs-diffs-and-fix-regressions} - Removed stray inline code injected into the LogsPane header badge and restored the process badge icon render path. - Corrected service label formatting in log rows to avoid corrupted characters and preserve single timestamp + optional service label view. -- Attempted targeted vitest run for logspane.test.tsx; runner not detected by automation here—tests recommended locally. +- Attempted targeted vitest run for logspane.test.tsx; runner not detected by automation here; tests recommended locally. ## DONE: 11 Refine LogsPane timestamp/service label formatting {#11-refine-logspane-timestamp-service-label-formatting} - Unified log row formatting to display `[timestamp | service]` once per entry with stripEmbeddedTimestamp applied to payloads. diff --git a/docs/design/components/service-run-output.md b/docs/design/components/service-run-output.md index 62d7b90a7..33d8cc263 100644 --- a/docs/design/components/service-run-output.md +++ b/docs/design/components/service-run-output.md @@ -5,10 +5,10 @@ - Show run context (profile, service count, optional elapsed), phase header, services list, ready/footer line. **Components** -- Header line: `azd app run — profile: {name} — services: {count} — elapsed: {time}` (elapsed optional if available). +- Header line: `azd app run: profile: {name}, services: {count}, elapsed: {time}` (elapsed optional if available). - Phase line: `Starting services…` before listing services. - Service block: status symbol and service name on first line; subsequent lines for endpoints (indented two spaces) labeled `local:`, `custom:`, `azure:`, `domain:`; blank line between services. -- Footer: `Ready — all services healthy — logs: azd app logs --follow` (or error variant when failures exist). +- Footer: `Ready: all services healthy, logs: azd app logs --follow` (or error variant when failures exist). **States** - Service status: ok (healthy), warn (degraded/unknown), err (failed). Use ✓/⚠/✗ with color when available; ASCII fallback [OK]/[WARN]/[ERR] when not. @@ -16,7 +16,7 @@ **Interactions** - Non-interactive output; no columns. Natural wrapping handled by terminal. -- On warn/err, append a short reason on the status line (e.g., `✗ api — port 8080 in use`). Still list any known endpoints below. +- On warn/err, append a short reason on the status line (e.g., `✗ api: port 8080 in use`). Still list any known endpoints below. - Verbose mode may add extra lines per service (e.g., health details) beneath existing labels without changing the base layout. **A11y** diff --git a/docs/specs/local-container-orchestration/spec.md b/docs/specs/local-container-orchestration/spec.md index 4918e30a3..814d7d32d 100644 --- a/docs/specs/local-container-orchestration/spec.md +++ b/docs/specs/local-container-orchestration/spec.md @@ -11,9 +11,9 @@ scope: P1 Extend azd-app's **native container path** (services with an `image:`, launched via individual `docker run -d`) so a project can express a realistic multi-container -local-development topology directly in `azure.yaml` — volumes, a run `command`, +local-development topology directly in `azure.yaml`: volumes, a run `command`, multiple published ports, container-to-container name resolution, and image -pull policy — without falling back to a hand-maintained `docker-compose.yml`. +pull policy, without falling back to a hand-maintained `docker-compose.yml`. ## Background @@ -39,7 +39,7 @@ project) can adopt `azd app run` for local dev. ## Goals -1. Support `volumes:` on container services — named volumes and bind mounts, +1. Support `volumes:` on container services, named volumes and bind mounts, with relative bind paths resolved against the project directory. 2. Pass a container `command:` (string **or** array) through to `docker run`. 3. Publish **all** ports listed for a container service, not just the primary. @@ -56,7 +56,7 @@ project) can adopt `azd app run` for local dev. the port manager, health checks, log streaming, dashboard, and `azd app add` continue to work unchanged. - **No local image build.** `azd app run` does not `docker build` a service's - Dockerfile — that would reimplement Docker Compose. Deploy images are built by + Dockerfile, because that would reimplement Docker Compose. Deploy images are built by `azd deploy`; for local dev a service runs from source as a process (see the local process override in Design §6). - **No new `depends_on` field.** The orchestrator already builds a dependency @@ -82,13 +82,13 @@ Add `Volumes []string` to `Service` / `serviceRaw` / `ServiceRuntime` and Classification of each `volumes:` entry: -- **Named volume** — `name:/container/path` where the left side is a bare +- **Named volume**: `name:/container/path` where the left side is a bare volume name (`^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`). Passed through unchanged; Docker auto-creates it. -- **Bind mount** — left side is a path (`.`, `..`, `/`, `~`, or a Windows drive +- **Bind mount**: left side is a path (`.`, `..`, `/`, `~`, or a Windows drive like `C:\`). The host side is resolved to an **absolute** path relative to the project directory before being passed to Docker. -- **Anonymous volume** — a single `/container/path` with no `:` host side. +- **Anonymous volume**: a single `/container/path` with no `:` host side. Each entry is passed to `docker run -v` as a discrete argv element (never through a shell) and validated to reject empty, oversized, or control-character specs, so @@ -120,13 +120,13 @@ they can resolve each other by service name (compose-equivalent). - **Creation**: idempotent `EnsureNetwork` (`docker network create`, tolerating "already exists") performed by each container as it starts. Safe under parallel level startup because the "already exists" error is treated as - success — no serialization needed. + success, no serialization needed. - **Attachment**: each container runs with `--network ` and `--network-alias `, so `BLOB_SERVER: azurite` resolves to the azurite container regardless of its `azd-` container name. A **reused** (already-running) container is (idempotently) connected to the network with the same alias so DNS works after a fast restart. -- **Lifecycle**: azd-app container services are **persistent** — `azd app stop` +- **Lifecycle**: azd-app container services are **persistent**, so `azd app stop` and Ctrl+C run a graceful shutdown that leaves running containers in place (`shutdownAllServices` stops only OS processes; containers are reused on the next `azd app run`). The network therefore **persists with its containers** and @@ -143,10 +143,10 @@ they can resolve each other by service name (compose-equivalent). Add `pull_policy` (`missing` | `always` | `never`) to gate the existing `client.Pull()` call: -- `missing` (recommended for pinned emulator images) — pull only if the image +- `missing` (recommended for pinned emulator images): pull only if the image is not present locally. -- `always` — always pull; the container fails to start if the pull fails. -- `never` — never pull; fail only if the image is absent at `docker run` time. +- `always`: always pull; the container fails to start if the pull fails. +- `never`: never pull; fail only if the image is absent at `docker run` time. - **Default (unset)** preserves today's behavior (attempt pull, tolerate failure, continue with cached image). @@ -171,10 +171,10 @@ to run the service as a **process**, using `docker.*`/`image` only for - A service whose container-ness comes from **`docker.*`** (a build-and-deploy service) runs as a **process** when it has a local `command`/`type: process`. - A `docker.*` service **without** a local command keeps today's container - (pull) behavior — this is backward compatible. + (pull) behavior; this is backward compatible. This is a routing rule only (`Service.RunsAsLocalProcess()`), not a local image -build — building the Dockerfile locally would reimplement Docker Compose and is +build; building the Dockerfile locally would reimplement Docker Compose and is explicitly out of scope. ### 7. `azd app test` for explicit-command services @@ -193,7 +193,7 @@ language**: reports pass/fail from the process **exit code**. - `azd app test` with no `--type` (i.e. `all`) **runs each explicitly-configured type** (unit, then integration, then e2e) and aggregates, instead of falling - back to the framework's default command — so the declared commands are always + back to the framework's default command, so the declared commands are always the ones executed. `--type unit|integration|e2e` runs just that command. - Services **without** an explicit `test:` block are unchanged: language auto-detection still applies, and container/emulator services without a suite @@ -203,8 +203,8 @@ language**: A monorepo commonly points several services at one directory (e.g. `project: .` on each, backed by a single root `package.json`). The deps step collected one -install task **per service**, so the same directory was installed — and rendered -as its own progress bar — once per service (N identical `website (npm)` bars). +install task **per service**, so the same directory was installed, and rendered +as its own progress bar, once per service (N identical `website (npm)` bars). Project collection (`detectProjectsFromAzureYaml`) now **dedupes by resolved project directory**, so a shared directory is collected, installed, and shown **once**. `azd app deps --dry-run` and `azd app run`'s install phase are @@ -212,7 +212,7 @@ consistent. To keep it clear that the single install covers **all** the services (not just the directory), the install line is **labeled with the service names** that -share the directory — e.g. `web, ingest, +6 more (npm)` (sorted, truncated for +share the directory, e.g. `web, ingest, +6 more (npm)` (sorted, truncated for readability). A directory used by a single service keeps its default directory-name label (no change). @@ -225,11 +225,11 @@ ignored the stream. That mislabeled lines two ways: a JSON log like dropped to `DEBUG`, while an unstructured stderr diagnostic (`​.env not found`) with no keyword defaulted to `INFO`. The classifier now: -- **honors structured logs** — a JSON line with a `level`/`severity` field uses +- **honors structured logs**: a JSON line with a `level`/`severity` field uses that level (the emitter's own classification wins); - uses **word-boundary** keyword matching, so identifiers (`errorReporter`, `trace_worker`) no longer misfire; -- is **stream-aware** — an unclassified **stderr** line surfaces as `WARN` +- is **stream-aware**: an unclassified **stderr** line surfaces as `WARN` (where programs write diagnostics) instead of `INFO`. ## Risks / trade-offs @@ -237,7 +237,7 @@ with no keyword defaulted to `INFO`. The classifier now: - **Network lifecycle**: the network must be created idempotently (parallel startup) and cleaned up best-effort (a leftover empty network is harmless and reused next run). Container **reuse** (an already-running container) must not - be broken by network changes — reused containers are assumed already attached. + be broken by network changes; reused containers are assumed already attached. - **Arg injection**: volumes and command introduce user-controlled `docker run` arguments. Each is validated and passed as discrete `exec` argv elements (never a shell string), consistent with the existing G204-scoped exec pattern. @@ -247,38 +247,38 @@ with no keyword defaulted to `INFO`. The classifier now: ## Acceptance criteria -- **AC1** — `volumes:` supports named volumes and bind mounts; relative bind +- **AC1**: `volumes:` supports named volumes and bind mounts; relative bind paths resolve against the project dir; entries are injection-safe. -- **AC2** — container `command:` accepts string and array forms; tokens reach +- **AC2**: container `command:` accepts string and array forms; tokens reach `docker run`. -- **AC3** — every `ports:` entry is published for a container service. -- **AC4** — container services share a per-project network (created +- **AC3**: every `ports:` entry is published for a container service. +- **AC4**: container services share a per-project network (created idempotently); a container can reach another by service name; a reused container is reconnected with its alias; single-container projects still work; the network is safely reused across runs (persists with its containers). -- **AC5** — `pull_policy: missing|always|never` gates image pulls; unset +- **AC5**: `pull_policy: missing|always|never` gates image pulls; unset preserves current behavior. -- **AC6** — `uses` still health-gates startup ordering (regression). -- **AC7** — v1.1 JSON schema + CLI/web docs document volumes, array command, +- **AC6**: `uses` still health-gates startup ordering (regression). +- **AC7**: v1.1 JSON schema + CLI/web docs document volumes, array command, multi-port, pull_policy, and container networking. -- **AC8** — the website's 3-container topology (postgres + azurite + +- **AC8**: the website's 3-container topology (postgres + azurite + eventhubs) starts under `azd app run` (end-to-end validation). -- **AC9** — a service with `docker.*`/`image` **and** an explicit local +- **AC9**: a service with `docker.*`/`image` **and** an explicit local `command`/`type: process` runs as a **process** under `azd app run` (its `docker.*` stays deploy-only); a `docker.*` service **without** a local command is unchanged (still a container). No local image build is performed. -- **AC10** — a service whose `language` isn't a recognized test language (e.g. +- **AC10**: a service whose `language` isn't a recognized test language (e.g. `docker`) but which declares an explicit `test..command` is testable under `azd app test`; the runner is selected from `framework`; `--type all` runs each configured type and aggregates; services without a `test:` block are unaffected (auto-detection unchanged). -- **AC11** — when several services share one resolved `project` directory, deps +- **AC11**: when several services share one resolved `project` directory, deps collection yields a single install task for it (one progress bar, one install), and that line is **labeled with the service names** it covers (sorted, truncated, e.g. `web, ingest, +6 more (npm)`); a single-service directory keeps its directory-name label; `azd app deps --dry-run` and the `azd app run` install phase agree; distinct directories and package managers remain separate. -- **AC12** — service-log level classification honors an explicit `level`/ +- **AC12**: service-log level classification honors an explicit `level`/ `severity` on a structured (JSON) line; keyword detection is whole-word (no identifier misfires); an unclassified stderr line is `WARN`, an unclassified stdout line is `INFO`. @@ -302,15 +302,15 @@ website's dev stack under `azd app run` (dev config mirroring `compose.dev.yml`) in review: subdir network-name derivation, and array-command on process services). - Note (pre-existing, out of scope): `azd app run --detach` exits silently on - Windows (empty run.log) — unrelated to this change (`run_detach.go`). + Windows (empty run.log), unrelated to this change (`run_detach.go`). ### Deferred (follow-up) - **Container-exec health checks** (`healthcheck.test: ["CMD-SHELL", ...]`) that run - *inside* a container via `docker exec` are not yet honored — container health uses + *inside* a container via `docker exec` are not yet honored; container health uses host-side TCP/HTTP checks against the published port. This is adequate for the emulators in scope (they open their ports when ready), and `uses` health-gating works on that signal. A dedicated follow-up can add a `container-exec` health type. -- **Command tokenizer consolidation** — `parseCommandLine` is a third copy of a +- **Command tokenizer consolidation**: `parseCommandLine` is a third copy of a quote-aware splitter (the `testing` package has two). Consolidating into a shared `internal` util is a low-risk cleanup deferred to avoid touching unrelated test-infra code in this PR. @@ -319,6 +319,6 @@ website's dev stack under `azd app run` (dev config mirroring `compose.dev.yml`) Volume/command/network/pull_policy values flow to `docker run` as **discrete argv** (never a shell), and the image positional is validated to not start with `-`, so shell- and flag-injection are neutralized (security review: no findings). Bind mounts -to **absolute** host paths are intentionally allowed — the same trust model as +to **absolute** host paths are intentionally allowed; the same trust model as `docker compose` for a developer-authored `azure.yaml`. Relative binds that escape the project directory are rejected. diff --git a/docs/specs/local-container-orchestration/test-plan.md b/docs/specs/local-container-orchestration/test-plan.md index 01ec2ffcb..544b67973 100644 --- a/docs/specs/local-container-orchestration/test-plan.md +++ b/docs/specs/local-container-orchestration/test-plan.md @@ -1,4 +1,4 @@ -# Test Plan — Native container config for `host: local` services +# Test Plan: Native container config for `host: local` services Issue: https://github.com/jongio/azd-app/issues/546 Spec: ./spec.md @@ -36,15 +36,15 @@ All planned rows are automated. Mapping to implemented tests: | T23 | AC5 | `docker.TestValidatePullPolicy`; `service.TestService_PullPolicy` | automated | | T24 | AC6 | existing `service` orchestrator/graph tests (uses ordering unchanged) | automated | | T25 | AC7 | `service.TestV11SchemaDocumentsContainerFields` | automated | -| T26 | AC8 | `service.TestStartContainerService_WebsiteStyleTopology` (docker) — full path: command + 3 ports + named volume + project network, verified via docker inspect | automated | -| T27 | AC9 | `service.TestService_RunsAsLocalProcess` — routing predicate (image=container; docker.*+command=process) | automated | -| T28 | AC9 | `service.TestDetectServiceRuntime_DockerServiceWithCommandRunsAsProcess` / `_DockerServiceWithoutCommandStaysContainer` — routing + backward compat | automated | -| T29 | AC10 | `testing.TestValidateService_ExplicitCommand_UnsupportedLanguage` / `_DefaultsFrameworkToCustom` / `TestValidateService_DockerNoExplicitCommand_Skipped`; `TestHasExplicitCommand` — explicit `test:` makes a docker/unset-language service testable; no-command service still skipped | automated | -| T30 | AC10 | `testing.TestNewRunnerForService_ExplicitConfig_FrameworkDispatch` / `_LanguageWins` / `_UnsupportedLanguage_NoExplicitCommand` — runner selected by framework for explicit-config services | automated | -| T31 | AC10 | `testing.TestExecuteServiceTests_All_ExpandsExplicitTypes` — `--type all` runs each configured explicit type and aggregates | automated | -| T32 | AC10 | `testing.TestExecuteServiceTests_UnconfiguredType_NonTestLanguage_Skipped`; `TestTypeHasExplicitCommand`; `TestIsRecognizedTestLanguage` — a non-test-language service is skipped (not run via the framework default) for a requested type it did not configure | automated | -| T33 | AC11 | `commands.TestDetectProjectsFromAzureYaml_DedupesSharedProjectDir` — three services sharing `project: .` collapse to one node project; `commands.TestGroupedNodeLabel` / `TestServiceDirsFromAzureYaml`, `installer.TestAddNodeProjectLabeled` — shared install is labeled with the covering service names | automated | -| T34 | AC12 | `service.TestInferLogLevel` — structured `level`/`severity` honored; word-boundary keywords (no `errorReporter`/`trace_worker` misfire); unclassified stderr→WARN, stdout→INFO | automated | +| T26 | AC8 | `service.TestStartContainerService_WebsiteStyleTopology` (docker): full path: command + 3 ports + named volume + project network, verified via docker inspect | automated | +| T27 | AC9 | `service.TestService_RunsAsLocalProcess`: routing predicate (image=container; docker.*+command=process) | automated | +| T28 | AC9 | `service.TestDetectServiceRuntime_DockerServiceWithCommandRunsAsProcess` / `_DockerServiceWithoutCommandStaysContainer`: routing + backward compat | automated | +| T29 | AC10 | `testing.TestValidateService_ExplicitCommand_UnsupportedLanguage` / `_DefaultsFrameworkToCustom` / `TestValidateService_DockerNoExplicitCommand_Skipped`; `TestHasExplicitCommand`: explicit `test:` makes a docker/unset-language service testable; no-command service still skipped | automated | +| T30 | AC10 | `testing.TestNewRunnerForService_ExplicitConfig_FrameworkDispatch` / `_LanguageWins` / `_UnsupportedLanguage_NoExplicitCommand`: runner selected by framework for explicit-config services | automated | +| T31 | AC10 | `testing.TestExecuteServiceTests_All_ExpandsExplicitTypes`: `--type all` runs each configured explicit type and aggregates | automated | +| T32 | AC10 | `testing.TestExecuteServiceTests_UnconfiguredType_NonTestLanguage_Skipped`; `TestTypeHasExplicitCommand`; `TestIsRecognizedTestLanguage`: a non-test-language service is skipped (not run via the framework default) for a requested type it did not configure | automated | +| T33 | AC11 | `commands.TestDetectProjectsFromAzureYaml_DedupesSharedProjectDir`: three services sharing `project: .` collapse to one node project; `commands.TestGroupedNodeLabel` / `TestServiceDirsFromAzureYaml`, `installer.TestAddNodeProjectLabeled`: shared install is labeled with the covering service names | automated | +| T34 | AC12 | `service.TestInferLogLevel`: structured `level`/`severity` honored; word-boundary keywords (no `errorReporter`/`trace_worker` misfire); unclassified stderr→WARN, stdout→INFO | automated | Original planned matrix retained below for traceability. @@ -82,7 +82,7 @@ Original planned matrix retained below for traceability. ## Functionality Inventory (Phase 3 reconciliation) -Enumerated against `git diff origin/main` — every unit of new functionality maps +Enumerated against `git diff origin/main`: every unit of new functionality maps to a covering test. **Zero gaps.** | Functionality | Covering test(s) | diff --git a/docs/specs/local-view-architecture.md b/docs/specs/local-view-architecture.md index 6764ed639..e72532ed7 100644 --- a/docs/specs/local-view-architecture.md +++ b/docs/specs/local-view-architecture.md @@ -1,11 +1,11 @@ -# azd-app — Local View Architecture Spec +# azd-app: Local View Architecture Spec > **Context**: Response to [Azure/azure-dev-pr#1779](https://github.com/Azure/azure-dev-pr/discussions/1779) -> ([EPIC Azure/azure-dev#7681](https://github.com/Azure/azure-dev/issues/7681) — "Local View of Your App and Resources"). +> ([EPIC Azure/azure-dev#7681](https://github.com/Azure/azure-dev/issues/7681), "Local View of Your App and Resources"). > > The discussion evaluates four options for building a local view after `azd up`. It characterises > "Jon's existing azd app dashboard" as a **browser-only UI** and scores it against a new TUI option. -> That characterisation is incomplete. **azd-app is not a browser dashboard — it is a local +> That characterisation is incomplete. **azd-app is not a browser dashboard; it is a local > observability platform for azd projects**, and the browser UI is only one of its three > first-class presentation surfaces (CLI snapshot, web dashboard, MCP for agents). The TUI option > in the discussion is a fourth surface that can be added on top of the same data layer without a @@ -26,7 +26,7 @@ | Dashboard components | **92 React components**, **45 hooks**, **4 contexts**, **33 lib modules** | | HTTP API surface | 8 Connect-RPC services (proto-defined) with 30+ RPC methods across `proto/azdapp/v1/*.proto` | | Streaming surfaces | 5 server-streaming RPCs (local logs, azure logs, health, state transitions, broadcast) + WebSocket fallback | -| MCP tools | **12 agent-consumable tools** — observability, operations, configuration | +| MCP tools | **12 agent-consumable tools**: observability, operations, configuration | | Shipping vehicle | Single `azd` extension binary (`jongio.azd.app`) with embedded dashboard, no external deps | | Transport | **Connect-RPC v2** (proto-defined services over HTTP/1.1 JSON + HTTP/2 binary). Single `.proto` schema drives Go server, TS dashboard client, MCP tools, and future TUI. | | Azure integration | Log Analytics (KQL, time range, tables), diagnostic settings discovery, Bicep template generation, App Service / Container Apps / Functions validators | @@ -46,9 +46,9 @@ fulfils all six priorities. |---|---|---| | **P1 Terminal-native** | Primary experience runs in terminal | ✅ `azd app info`, `azd app logs -f`, `azd app health`, `azd app reqs`, `azd app start/stop/restart` are pure CLI. Dashboard is optional (`azd app run --web`). | | **P2 No external deps** | No Docker/containers/external services | ✅ Pure Go binary. Dashboard is a Vite/React SPA **embedded via `//go:embed dist`** and served by the built-in HTTP server. No runtime deps. | -| **P3 Reuses existing investment** | Reuses Jon's 9-month dashboard | ✅ ALL of it — detector, orchestrator, runner, health, Azure Log Analytics pipeline, classifications, streaming. | +| **P3 Reuses existing investment** | Reuses Jon's 9-month dashboard | ✅ ALL of it: detector, orchestrator, runner, health, Azure Log Analytics pipeline, classifications, streaming. | | **P4 Real-time streaming** | Live logs + health updates | ✅ WebSocket streaming for local logs, Azure logs, and service health. Log buffer, backpressure handling, and flood tests already exist. | -| **P5 Agent-consumable** | Structured output for AI agents | ✅ 12 MCP tools already shipped; `azd app info --output json`, `azd app logs --format json`, NDJSON streaming endpoints. Primary consumer is not the browser — it's also Copilot/Claude via MCP. | +| **P5 Agent-consumable** | Structured output for AI agents | ✅ 12 MCP tools already shipped; `azd app info --output json`, `azd app logs --format json`, NDJSON streaming endpoints. Primary consumer is not the browser; it is also Copilot/Claude via MCP. | | **P6 Extensible observability** | Grow to tracing/metrics/extension views | ✅ Clean layering (data → API → consumers). Adding tracing or a TUI panel is an additive change to the API, not a rewrite. The `monitor` package already emits structured `StateTransition` events that any surface can subscribe to. | --- @@ -137,19 +137,19 @@ full local view of an azd project from the terminal alone. | Command | Purpose | Streaming? | JSON? | |---|---|---|---| | `azd app reqs` | Verify all tool prerequisites (node, python, dotnet, docker, …) | No | Yes | -| `azd app deps` | Install deps across detected languages / package managers | No | — | -| `azd app run [--web] [-s svc] [--runtime azd\|aspire]` | Start all services; with `--web` also opens dashboard | Yes (stdout multiplex) | — | -| `azd app start ` / `stop` / `restart` | Per-service lifecycle | — | — | +| `azd app deps` | Install deps across detected languages / package managers | No | n/a | +| `azd app run [--web] [-s svc] [--runtime azd\|aspire]` | Start all services; with `--web` also opens dashboard | Yes (stdout multiplex) | n/a | +| `azd app start ` / `stop` / `restart` | Per-service lifecycle | n/a | n/a | | `azd app info` | Snapshot of all services: status, URLs, ports, Azure deployment info, env vars | No | Yes | | `azd app logs [svc] -f -n N --since 5m --level error --source local\|azure\|all --format text\|json` | Unified local + Azure logs, filterable, streaming | Yes | Yes (NDJSON) | | `azd app health` | Continuous or point-in-time health monitoring | Yes | Yes | -| `azd app test [--coverage]` | Run tests across all services with unified coverage | No | — | -| `azd app add ` | Add a well-known service to `azure.yaml` | — | — | -| `azd app notifications` | Show OS-native state transition notifications | — | — | +| `azd app test [--coverage]` | Run tests across all services with unified coverage | No | n/a | +| `azd app add ` | Add a well-known service to `azure.yaml` | n/a | n/a | +| `azd app notifications` | Show OS-native state transition notifications | n/a | n/a | | `azd app mcp serve` | Start MCP server on stdio for AI agents | N/A | Structured tool calls | -| `azd app metadata` | Emit extension metadata | — | Yes | -| `azd app listen` | Internal lifecycle-events endpoint required by azd extension framework | — | — | -| `azd app version` | Version, build time, commit | — | Yes | +| `azd app metadata` | Emit extension metadata | n/a | Yes | +| `azd app listen` | Internal lifecycle-events endpoint required by azd extension framework | n/a | n/a | +| `azd app version` | Version, build time, commit | n/a | Yes | **Observation**: Options 2 and 4 in the discussion (Enhanced `azd show` + TUI) are already ~80% implemented as `azd app info` + `azd app logs -f` + `azd app health`. The only thing missing @@ -162,43 +162,43 @@ is a Bubble-Tea-style interactive multi-panel view, which can be added as a 17th ### 5.1 Detection & Orchestration -- `internal/detector` — language/framework detection for Node, Python, .NET, plus HTTP-triggered +- `internal/detector`: language/framework detection for Node, Python, .NET, plus HTTP-triggered detection for Azure Functions. Input: `azure.yaml` + filesystem. Output: a typed service graph. -- `internal/service` — service graph, config, executor, environment, hooks, port allocation, +- `internal/service`: service graph, config, executor, environment, hooks, port allocation, health probes, log buffer + filter + manager, container integration, `docker-compose` compat. -- `internal/orchestrator` — dependency-aware lifecycle (start order, timeouts, errors, +- `internal/orchestrator`: dependency-aware lifecycle (start order, timeouts, errors, graceful shutdown). -- `internal/runner` — process spawning, Aspire runtime, log multiplexing. +- `internal/runner`: process spawning, Aspire runtime, log multiplexing. ### 5.2 Health & State -- `internal/healthcheck` — HTTP and process-based health probes with configurable profiles and +- `internal/healthcheck`: HTTP and process-based health probes with configurable profiles and metrics. -- `internal/monitor` — `StateMonitor` polls the service registry, detects transitions +- `internal/monitor`: `StateMonitor` polls the service registry, detects transitions (process crashed, port unbound, healthy→unhealthy, slow start, degraded), classifies severity (`Critical` / `Warning` / `Info`), rate-limits, and exposes a listener API. Already wired to dashboard WebSocket broadcast and OS notifications. This is the eventing spine that any - surface — CLI, dashboard, TUI, MCP — can subscribe to. + surface, CLI, dashboard, TUI, MCP, can subscribe to. ### 5.3 Azure Integration All under `internal/azure`: -- `discovery` — resolve resources from `AZURE_RESOURCE_GROUP` / `azure.yaml` outputs. -- `credentials` + `token_cache` — DefaultAzureCredential with cached tokens. -- `loganalytics` + `tables` + `query_builder` — KQL query construction and execution against +- `discovery`: resolve resources from `AZURE_RESOURCE_GROUP` / `azure.yaml` outputs. +- `credentials` + `token_cache`: DefaultAzureCredential with cached tokens. +- `loganalytics` + `tables` + `query_builder`: KQL query construction and execution against Log Analytics workspaces. -- `realtime` — polling-based streaming of Azure logs with configurable time range. -- `diagnostics` + `diagnostic_engine` — fetches diagnostic settings for each resource, detects +- `realtime`: polling-based streaming of Azure logs with configurable time range. +- `diagnostics` + `diagnostic_engine`: fetches diagnostic settings for each resource, detects misconfigurations, reports gaps. -- `validator_appservice`, `validator_containerapp`, `validator_function` — per-resource-type +- `validator_appservice`, `validator_containerapp`, `validator_function`: per-resource-type validation of logging setup. -- `bicep` — **generates a consolidated Bicep template** to fix missing diagnostic settings +- `bicep`: **generates a consolidated Bicep template** to fix missing diagnostic settings across all detected services. Returned by `GET /api/azure/bicep-template`. ### 5.4 Dashboard Server -`internal/dashboard` — HTTP server hosting 8 Connect-RPC services + WebSocket streams, port +`internal/dashboard`: HTTP server hosting 8 Connect-RPC services + WebSocket streams, port manager, embedded static assets. The API is defined in `proto/azdapp/v1/*.proto`: ``` @@ -249,16 +249,16 @@ the same `.proto` files that define the server. **Structure** (`cli/dashboard/src/`): -- 92 components — `ServiceCard`, `ServiceTable`, `ConsoleView`, `LogsPane` (+ 8 sub-components), +- 92 components: `ServiceCard`, `ServiceTable`, `ConsoleView`, `LogsPane` (+ 8 sub-components), `HealthTooltip`, `DiagnosticsModal`, `AzureSetupGuide`, `BicepTemplateModal`, `ClassificationsManager`, `KqlQueryInput`, `TableSelector`, `TimeRangeSelector`, `EnvironmentPanel`, `NotificationCenter`, `SettingsDialog`, `ThemeToggle`, … -- 45 hooks — `useServices`, `useLogsStream`, `useHealthStream`, `useBackendConnection`, +- 45 hooks: `useServices`, `useLogsStream`, `useHealthStream`, `useBackendConnection`, `useAzureTimeRange`, `useLogClassifications`, `useLogFiltering`, `useSmoothedLoadingIndicator`, `useBicepTemplate`, `useDiagnosticSettings`, `useWorkspaceVerification`, `useCodespaceEnv`, … -- 4 contexts — `ServicesContext`, `ServiceOperationsContext`, `PreferencesContext`, +- 4 contexts: `ServicesContext`, `ServiceOperationsContext`, `PreferencesContext`, `CodespaceContext`. -- 33 lib modules — service formatters, health diagnostics, log utils, search highlighting, +- 33 lib modules: service formatters, health diagnostics, log utils, search highlighting, storage utils, panel utils, provenance, shortcut handling. The dashboard is **not required**. It consumes the same HTTP API that a TUI, a CLI command, or @@ -266,31 +266,31 @@ an agent would. Treating it as the "experience" conflates the UI with the system --- -## 7. MCP Server (Agent-Consumable Surface — Already Shipping) +## 7. MCP Server (Agent-Consumable Surface: Already Shipping) `extension.yaml` declares the `mcp-server` capability. `azd app mcp serve` starts a Model Context Protocol server on stdio. Registered tools (`cli/src/cmd/app/commands/mcp_tools.go`): **Observability** -- `get_services` — full service info (status, URLs, ports, Azure info, env vars) -- `get_service_logs` — filtered logs (service, level, time range, local/azure/both) -- `get_service_errors` — errors with surrounding context, optimised for AI triage -- `get_project_info` — project metadata and service definitions +- `get_services`: full service info (status, URLs, ports, Azure info, env vars) +- `get_service_logs`: filtered logs (service, level, time range, local/azure/both) +- `get_service_errors`: errors with surrounding context, optimised for AI triage +- `get_project_info`: project metadata and service definitions **Operations** -- `run_services` — start all services -- `stop_services` — stop all or named service +- `run_services`: start all services +- `stop_services`: stop all or named service - `start_service` / `restart_service` - `install_dependencies` - `check_requirements` **Configuration** -- `get_environment_variables` — per-service or all +- `get_environment_variables`: per-service or all - `set_environment_variable` Each tool has a typed output schema, rate limiting, `ReadOnly`/`Idempotent` hints, and JSON-schema-validated args. This means **azd-app already fulfils the agent-consumable priority -(P5) at 100%** — the dashboard is not the only consumer, and never was. +(P5) at 100%**: the dashboard is not the only consumer, and never was. --- @@ -334,9 +334,9 @@ and Bicep-generation work that none of the four options in the discussion would ## 10. Recommended Path Forward 1. **Adopt azd-app as the data layer** for EPIC #7681's local view. -2. **Keep the CLI snapshots** (`info`, `logs`, `health`) as the default terminal-native experience — +2. **Keep the CLI snapshots** (`info`, `logs`, `health`) as the default terminal-native experience; they already satisfy the P1/P2 design goals for Option 2. -3. **Keep the MCP server** as the agent-consumable surface — it already satisfies P5 and is +3. **Keep the MCP server** as the agent-consumable surface; it already satisfies P5 and is ahead of the discussion's assessment of every option. 4. **Keep the browser dashboard** as an opt-in (`--web`) surface for developers who want the rich UI. Nobody is forced into a browser. @@ -366,7 +366,7 @@ cli/ │ ├── mcp.go, mcp_tools.go, mcp_resources.go │ ├── notifications.go, listen.go, metadata.go, version.go │ └── core.go, service_control.go, generate.go -└── src/internal/ # 22 packages — see §5 +└── src/internal/ # 22 packages, see §5 ├── detector/ orchestrator/ runner/ service/ ├── healthcheck/ monitor/ notifications/ portmanager/ ├── dashboard/ azure/ logging/ executor/ diff --git a/docs/specs/log-stream-races/spec.md b/docs/specs/log-stream-races/spec.md index 31ed87663..73e733f71 100644 --- a/docs/specs/log-stream-races/spec.md +++ b/docs/specs/log-stream-races/spec.md @@ -10,7 +10,7 @@ status: shipped When running `azd app run`, the dashboard sometimes fails to display logs for one or more services. The user sees an empty log pane that never receives entries -despite the service clearly producing output. A page refresh fixes it — but only +despite the service clearly producing output. A page refresh fixes it, but only if the service's buffer happens to exist by then. This is a regression-prone, non-deterministic UX bug that erodes trust in the dashboard's reliability. @@ -49,7 +49,7 @@ and dynamically subscribes + starts a pump goroutine for new services mid-stream ### 2. Reorder pump start before backfill (backend) -Start pump goroutines immediately after subscribing — before sending backfill +Start pump goroutines immediately after subscribing, before sending backfill entries to the client. This ensures the 100-capacity subscriber channels drain continuously and don't overflow from `broadcast()` drop-on-full during the (potentially slow) backfill send phase. @@ -94,7 +94,7 @@ gated on `connected` because Log Analytics genuinely needs the backend reachable - The `OnBufferAdded` listener channel (capacity 16) can fill if many services start simultaneously. The non-blocking send means the notification is dropped, - but the service still exists in the LogManager — a worst case requires one more + but the service still exists in the LogManager, a worst case requires one more reconnect cycle to discover it. Acceptable for the expected service count (<20). - Backfill entries and live-stream entries can overlap (duplicate delivery) for entries that arrive between subscribe and backfill-snapshot. The ring buffer's diff --git a/proto/azdapp/v1/common.proto b/proto/azdapp/v1/common.proto index dc97ecb72..084faba38 100644 --- a/proto/azdapp/v1/common.proto +++ b/proto/azdapp/v1/common.proto @@ -83,7 +83,7 @@ enum ServiceStatus { // HealthState mirrors internal/healthcheck health states. // // Wire stability: enum values are append-only. DEGRADED was added after the -// initial draft when wiring HealthService — the existing dashboard summary +// initial draft when wiring HealthService; the existing dashboard summary // distinguishes degraded from unhealthy, and dropping that distinction would // silently lose information. Older clients that don't recognise the value // will see HEALTH_STATE_UNSPECIFIED (proto3 unknown-enum semantics) rather diff --git a/web/e2e/screenshots.spec.ts b/web/e2e/screenshots.spec.ts index 45963deac..4b39670a6 100644 --- a/web/e2e/screenshots.spec.ts +++ b/web/e2e/screenshots.spec.ts @@ -114,7 +114,7 @@ test.describe('Component Screenshots', () => { // Check if search modal element exists before interacting const modalExists = await page.locator('#search-modal').count() > 0; - test.skip(!modalExists, 'Search modal element not found — may be provided by external component'); + test.skip(!modalExists, 'Search modal element not found; it may be provided by an external component'); // Open search with keyboard shortcut (/ key) await page.keyboard.press('/'); @@ -129,7 +129,7 @@ test.describe('Component Screenshots', () => { await searchBtn.click(); await page.waitForSelector('#search-modal.open', { timeout: 30000 }); } else { - test.skip(true, 'Search modal cannot be opened — keyboard shortcut and search button not available'); + test.skip(true, 'Search modal cannot be opened; keyboard shortcut and search button not available'); return; } } @@ -152,7 +152,7 @@ test.describe('Component Screenshots', () => { // Check if mobile menu toggle exists (rendered by external Header component) const toggleExists = await page.locator('[data-mobile-menu-toggle]').count() > 0; - test.skip(!toggleExists, 'Mobile menu toggle not found — may be provided by external Header component'); + test.skip(!toggleExists, 'Mobile menu toggle not found; it may be provided by an external Header component'); // Click the mobile menu toggle button await page.click('[data-mobile-menu-toggle]');