Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/lint-ext-microsoft-azd-exec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: ext-microsoft-azd-exec-ci

on:
pull_request:
paths:
- "cli/azd/extensions/microsoft.azd.exec/**"
- ".github/workflows/lint-ext-microsoft-azd-exec.yml"
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: write

jobs:
lint:
uses: ./.github/workflows/lint-go.yml
with:
working-directory: cli/azd/extensions/microsoft.azd.exec
9 changes: 9 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,15 @@ overrides:
- filename: pkg/infra/provisioning/bicep/local_preflight.go
words:
- actioned
- filename: docs/code-coverage-guide.md
words:
- covdata
- GOWORK
- filename: extensions/microsoft.azd.exec/**
words:
- azvs
- notwindows
- shellutil
ignorePaths:
- "**/*_test.go"
- "**/mock*.go"
Expand Down
5 changes: 5 additions & 0 deletions cli/azd/extensions/microsoft.azd.exec/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bin/
*.exe
coverage
coverage.out
cov
17 changes: 17 additions & 0 deletions cli/azd/extensions/microsoft.azd.exec/.golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: "2"

linters:
default: none
enable:
- gosec
- lll
- unused
- errorlint
settings:
lll:
line-length: 220
tab-width: 4

formatters:
enable:
- gofmt
9 changes: 9 additions & 0 deletions cli/azd/extensions/microsoft.azd.exec/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Changelog

## 0.5.0

- Initial release as `microsoft.azd.exec` in the Azure/azure-dev repository.
- Execute scripts and inline commands with full azd environment context.
- Cross-platform shell detection and execution (bash, sh, zsh, pwsh, powershell, cmd).
- Interactive mode for scripts requiring stdin.
- Child process exit code propagation for CI/CD pipelines.
78 changes: 78 additions & 0 deletions cli/azd/extensions/microsoft.azd.exec/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# microsoft.azd.exec

Execute scripts and commands with Azure Developer CLI context and environment variables.

## Installation

```bash
azd extension install microsoft.azd.exec
```

## Usage

```bash
# Run a command directly with azd environment (exact argv, no shell wrapping)
azd exec python script.py
azd exec npm run dev
azd exec -- python app.py --port 8000 --reload
azd exec docker compose up --build

# Execute a script file with azd environment
azd exec ./setup.sh
azd exec ./build.sh -- --verbose

# Inline shell command (single quoted argument uses shell)
azd exec 'echo $AZURE_ENV_NAME'
azd exec --shell pwsh "Write-Host $env:AZURE_ENV_NAME"

# Interactive mode
azd exec -i ./interactive-setup.sh
```

## Execution Modes

| Invocation | Mode | How it works |
|---|---|---|
| `azd exec python script.py` | **Direct exec** | `exec.Command("python", "script.py")` — exact argv, no shell |
| `azd exec 'echo $VAR'` | **Shell inline** | `bash -c "echo $VAR"` — shell expansion available |
| `azd exec ./setup.sh` | **Script file** | `bash ./setup.sh` — shell detected from extension |
| `azd exec --shell pwsh "cmd"` | **Shell inline** | `pwsh -Command "cmd"` — explicit shell |

**Heuristic**: Multiple arguments without `--shell` → direct process exec.
Single quoted argument or explicit `--shell` → shell inline execution.
File path → script file execution with auto-detected or explicit shell.

## Features

- **Direct process execution**: Run programs with exact argv semantics (no shell wrapping)
- **Shell auto-detection**: Detects shell from file extension for script files
- **Cross-platform**: Supports bash, sh, zsh, pwsh, powershell, and cmd
- **Interactive mode**: Connect stdin for scripts requiring user input (`-i`)
- **Environment loading**: Inherits azd environment variables, including any Key Vault secrets resolved by azd core
- **Exit code propagation**: Child process exit codes forwarded for CI/CD pipelines

## Security Considerations

- **Environment inheritance**: Child processes receive all parent environment variables,
including Azure tokens and any Key Vault secrets resolved by azd. Be cautious when
executing untrusted scripts.
- **cmd.exe quoting**: On Windows, `cmd.exe` expands `%VAR%` patterns even inside double
quotes. This is an inherent cmd.exe behavior that cannot be fully mitigated. Prefer
PowerShell (`--shell pwsh`) for untrusted arguments on Windows.
- **Script execution**: This extension runs arbitrary scripts by design. Only execute
scripts you trust.

## Development

```bash
cd cli/azd/extensions/microsoft.azd.exec

# Build
go build ./...

# Test
go test ./...

# Build for all platforms
EXTENSION_ID=microsoft.azd.exec EXTENSION_VERSION=0.5.0 ./build.sh
```
73 changes: 73 additions & 0 deletions cli/azd/extensions/microsoft.azd.exec/build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Ensure script fails on any error
$ErrorActionPreference = 'Stop'

# Get the directory of the script
$EXTENSION_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path

# Change to the script directory
Set-Location -Path $EXTENSION_DIR

# Create a safe version of EXTENSION_ID replacing dots with dashes
$EXTENSION_ID_SAFE = $env:EXTENSION_ID -replace '\.', '-'

# Define output directory
$OUTPUT_DIR = if ($env:OUTPUT_DIR) { $env:OUTPUT_DIR } else { Join-Path $EXTENSION_DIR "bin" }

# Create output directory if it doesn't exist
if (-not (Test-Path -Path $OUTPUT_DIR)) {
New-Item -ItemType Directory -Path $OUTPUT_DIR | Out-Null
}

# List of OS and architecture combinations
if ($env:EXTENSION_PLATFORM) {
$PLATFORMS = @($env:EXTENSION_PLATFORM)
}
else {
$PLATFORMS = @(
"windows/amd64",
"windows/arm64",
"darwin/amd64",
"darwin/arm64",
"linux/amd64",
"linux/arm64"
)
}

$APP_PATH = "$env:EXTENSION_ID/internal/cmd"

# Loop through platforms and build
foreach ($PLATFORM in $PLATFORMS) {
$OS, $ARCH = $PLATFORM -split '/'

$OUTPUT_NAME = Join-Path $OUTPUT_DIR "$EXTENSION_ID_SAFE-$OS-$ARCH"

if ($OS -eq "windows") {
$OUTPUT_NAME += ".exe"
}

Write-Host "Building for $OS/$ARCH..."

# Delete the output file if it already exists
if (Test-Path -Path $OUTPUT_NAME) {
Remove-Item -Path $OUTPUT_NAME -Force
}

# Set environment variables for Go build
$env:GOOS = $OS
$env:GOARCH = $ARCH

go build `
-trimpath `
-buildmode=pie `
-tags=cfi,cfg,osusergo `
-ldflags="-s -w -X '$APP_PATH.Version=$env:EXTENSION_VERSION'" `
-o $OUTPUT_NAME

if ($LASTEXITCODE -ne 0) {
Write-Host "An error occurred while building for $OS/$ARCH"
exit 1
}
}

Write-Host "Build completed successfully!"
Write-Host "Binaries are located in the $OUTPUT_DIR directory."
65 changes: 65 additions & 0 deletions cli/azd/extensions/microsoft.azd.exec/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/bin/bash

# Get the directory of the script
EXTENSION_DIR="$(cd "$(dirname "$0")" && pwd)"

# Change to the script directory
cd "$EXTENSION_DIR" || exit

# Create a safe version of EXTENSION_ID replacing dots with dashes
EXTENSION_ID_SAFE="${EXTENSION_ID//./-}"

# Define output directory
OUTPUT_DIR="${OUTPUT_DIR:-$EXTENSION_DIR/bin}"

# Create output and target directories if they don't exist
mkdir -p "$OUTPUT_DIR"

# List of OS and architecture combinations
if [ -n "$EXTENSION_PLATFORM" ]; then
PLATFORMS=("$EXTENSION_PLATFORM")
else
PLATFORMS=(
"windows/amd64"
"windows/arm64"
"darwin/amd64"
"darwin/arm64"
"linux/amd64"
"linux/arm64"
)
fi

APP_PATH="$EXTENSION_ID/internal/cmd"

# Loop through platforms and build
for PLATFORM in "${PLATFORMS[@]}"; do
OS=$(echo "$PLATFORM" | cut -d'/' -f1)
ARCH=$(echo "$PLATFORM" | cut -d'/' -f2)

OUTPUT_NAME="$OUTPUT_DIR/$EXTENSION_ID_SAFE-$OS-$ARCH"

if [ "$OS" = "windows" ]; then
OUTPUT_NAME+='.exe'
fi

echo "Building for $OS/$ARCH..."

# Delete the output file if it already exists
[ -f "$OUTPUT_NAME" ] && rm -f "$OUTPUT_NAME"

# Set environment variables for Go build
GOOS=$OS GOARCH=$ARCH go build \
-trimpath \
-buildmode=pie \
-tags=cfi,cfg,osusergo \
-ldflags="-s -w -X '$APP_PATH.Version=$EXTENSION_VERSION'" \
-o "$OUTPUT_NAME"

if [ $? -ne 0 ]; then
echo "An error occurred while building for $OS/$ARCH"
exit 1
fi
done

echo "Build completed successfully!"
echo "Binaries are located in the $OUTPUT_DIR directory."
86 changes: 86 additions & 0 deletions cli/azd/extensions/microsoft.azd.exec/ci-build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
param(
[string] $Version = (Get-Content "$PSScriptRoot/../version.txt"),
[string] $SourceVersion = (git rev-parse HEAD),
[switch] $CodeCoverageEnabled,
[string] $OutputFileName
)
$PSNativeCommandArgumentPassing = 'Legacy'

# Remove any previously built binaries
go clean

if ($LASTEXITCODE) {
Write-Host "Error running go clean"
exit $LASTEXITCODE
}

# Run `go help build` to obtain detailed information about `go build` flags.
$buildFlags = @(
# remove all file system paths from the resulting executable.
"-trimpath",

# Use buildmode=pie (Position Independent Executable) for enhanced security.
"-buildmode=pie"
)

if ($CodeCoverageEnabled) {
$buildFlags += "-cover"
}

# Build constraint tags
$tagsFlag = "-tags=cfi,cfg,osusergo"

# ld linker flags
$ldFlag = "-ldflags=-s -w -X 'microsoft.azd.exec/internal/cmd.Version=$Version'"

if ($IsWindows) {
Write-Host "Building for Windows"
}
elseif ($IsLinux) {
Write-Host "Building for linux"
}
elseif ($IsMacOS) {
Write-Host "Building for macOS"
}

# Add output file flag based on specified output file name
$outputFlag = "-o=$OutputFileName"

# collect flags
$buildFlags += @(
$tagsFlag,
$ldFlag,
$outputFlag
)

function PrintFlags() {
param(
[string] $flags
)

$i = 0
foreach ($buildFlag in $buildFlags) {
$argWithValue = $buildFlag.Split('=', 2)
if ($argWithValue.Length -eq 2 -and !$argWithValue[1].StartsWith("`"")) {
$buildFlag = "$($argWithValue[0])=`"$($argWithValue[1])`""
}

if ($i -eq $buildFlags.Length - 1) {
Write-Host " $buildFlag"
}
else {
Write-Host " $buildFlag ``"
}
$i++
}
}

Write-Host "Running: go build ``"
PrintFlags -flags $buildFlags
go build @buildFlags
if ($LASTEXITCODE) {
Write-Host "Error running go build"
exit $LASTEXITCODE
}

Write-Host "go build succeeded"
Loading
Loading