From 46b214f4737a6f04086befe26bd85e9f0dcc7602 Mon Sep 17 00:00:00 2001 From: Marshal Hayes <17213165+marshalhayes@users.noreply.github.com> Date: Sat, 7 Mar 2026 01:12:33 -0600 Subject: [PATCH 1/6] feat: add dogfooding scripts and workflow for PR testing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/dogfood-comment.yml | 77 +++++++ eng/scripts/dogfood-pr.ps1 | 281 ++++++++++++++++++++++++++ eng/scripts/dogfood-pr.sh | 262 ++++++++++++++++++++++++ 3 files changed, 620 insertions(+) create mode 100644 .github/workflows/dogfood-comment.yml create mode 100755 eng/scripts/dogfood-pr.ps1 create mode 100755 eng/scripts/dogfood-pr.sh diff --git a/.github/workflows/dogfood-comment.yml b/.github/workflows/dogfood-comment.yml new file mode 100644 index 000000000..49d8968f1 --- /dev/null +++ b/.github/workflows/dogfood-comment.yml @@ -0,0 +1,77 @@ +name: Add Dogfooding Comment + +on: + # Use pull_request_target to run in the context of the base branch + # This allows commenting on PRs from forks + pull_request_target: + types: [opened, reopened, synchronize] + branches: + - 'main' + # Allow manual triggering + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to add dogfooding comment to' + required: true + type: number + +jobs: + add-dogfood-comment: + # Only run on the CommunityToolkit org to avoid running on forks + if: ${{ github.repository_owner == 'CommunityToolkit' }} + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Add dogfooding comment + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + // Get PR number from either the PR event or manual input + const prNumber = context.payload.number || context.payload.inputs.pr_number; + const bashScript = 'https://raw.githubusercontent.com/CommunityToolkit/Aspire/main/eng/scripts/dogfood-pr.sh'; + const psScript = 'https://raw.githubusercontent.com/CommunityToolkit/Aspire/main/eng/scripts/dogfood-pr.ps1'; + + // Unique marker to identify dogfooding comments + const dogfoodMarker = ''; + + const comment = `${dogfoodMarker} + πŸš€ **Dogfood this PR with:** + + > **⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.** + + \`\`\`bash + curl -fsSL ${bashScript} | bash -s -- ${prNumber} + \`\`\` + Or + - Run remotely in PowerShell: + \`\`\`powershell + iex "& { $(irm ${psScript}) } ${prNumber}" + \`\`\``; + + // Check for existing dogfooding comment + const comments = await github.rest.issues.listComments({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + }); + + const existingComment = comments.data.find(comment => comment.body.includes(dogfoodMarker)); + + if (existingComment) { + // Update existing comment + await github.rest.issues.updateComment({ + comment_id: existingComment.id, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } diff --git a/eng/scripts/dogfood-pr.ps1 b/eng/scripts/dogfood-pr.ps1 new file mode 100755 index 000000000..7f1d2e297 --- /dev/null +++ b/eng/scripts/dogfood-pr.ps1 @@ -0,0 +1,281 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Download and install NuGet packages from a PR's build artifacts for local testing. +.EXAMPLE + ./dogfood-pr.ps1 1129 + ./dogfood-pr.ps1 1129 -WorkflowRunId 12345678 +#> + +param( + [Parameter(Position = 0, Mandatory = $true, HelpMessage = "Pull request number")] + [ValidateRange(1, [int]::MaxValue)] + [int]$PRNumber, + + [Parameter(HelpMessage = "Workflow run ID (skip PR resolution)")] + [ValidateRange(1, [long]::MaxValue)] + [long]$WorkflowRunId = 0, + + [Parameter(HelpMessage = "Install prefix directory")] + [string]$InstallPath = "", + + [Parameter(HelpMessage = "Verbose output")] + [switch]$VerboseOutput, + + [Parameter(HelpMessage = "Keep temp download directory")] + [switch]$KeepArchive, + + [Parameter(HelpMessage = "Show help")] + [switch]$Help +) + +$ErrorActionPreference = "Stop" + +$Script:Repo = "CommunityToolkit/Aspire" +$Script:CIWorkflow = "dotnet-ci.yml" +$Script:ArtifactName = "nuget-packages" + +# --- Output Helpers --- + +function Write-Link { + param([string]$Url, [string]$Label) + return "$([char]27)]8;;${Url}$([char]27)\${Label}$([char]27)]8;;$([char]27)\" +} + +function Get-DisplayPath { + param([string]$Path) + if ($Path.StartsWith($HOME)) { + return "~" + $Path.Substring($HOME.Length) + } + return $Path +} + +function Invoke-WithSpinner { + param([string]$Message, [string]$Command, [string[]]$Arguments) + $psi = [System.Diagnostics.ProcessStartInfo]::new($Command) + foreach ($arg in $Arguments) { $psi.ArgumentList.Add($arg) } + $psi.UseShellExecute = $false + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $proc = [System.Diagnostics.Process]::Start($psi) + $chars = [char[]]'⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' + $i = 0 + while (-not $proc.HasExited) { + Write-Host "`r$($chars[$i++ % $chars.Length]) $Message" -NoNewline + Start-Sleep -Milliseconds 100 + } + $proc.WaitForExit() + Write-Host "`r`e[K" -NoNewline + return $proc.ExitCode +} + +function Show-Help { + @" +Usage: dogfood-pr.ps1 [-PRNumber] [OPTIONS] + +Download and install NuGet packages from a PR's build artifacts for local testing. + +OPTIONS: + -WorkflowRunId ID Workflow run ID (skip PR resolution) + -InstallPath PATH Install prefix (default: `$HOME/.aspire) + -VerboseOutput Verbose output + -KeepArchive Keep temp download directory + -Help Show this help + +EXAMPLES: + ./dogfood-pr.ps1 1129 + ./dogfood-pr.ps1 1129 -WorkflowRunId 12345678 + ./dogfood-pr.ps1 1129 -InstallPath ./local-packages + +REQUIREMENTS: + - GitHub CLI (gh) authenticated: https://cli.github.com +"@ +} + +# --- Core Functions --- + +function Test-Prerequisites { + if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-Host "βœ— GitHub CLI (gh) is required. Install from: https://cli.github.com" -ForegroundColor Red + exit 1 + } + + & gh auth status 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "βœ— GitHub CLI is not authenticated. Run: gh auth login" -ForegroundColor Red + exit 1 + } +} + +function Resolve-PullRequest { + $prUrl = "https://github.com/$($Script:Repo)/pull/$PRNumber" + try { + $prJson = & gh api "repos/$($Script:Repo)/pulls/$PRNumber" --jq '{sha: .head.sha, title: .title, author: .user.login}' 2>$null | ConvertFrom-Json + } catch { + Write-Host "βœ— PR #$PRNumber not found in $($Script:Repo)" -ForegroundColor Red + exit 1 + } + + $script:HeadSha = $prJson.sha + $prTitle = $prJson.title + $prAuthor = $prJson.author + + $authorDisplay = Write-Link "https://github.com/$prAuthor" "@$prAuthor" + + Write-Host "" + Write-Host "$(Write-Link $prUrl "PR #$PRNumber") β€” $prTitle by $authorDisplay" + if ($VerboseOutput) { Write-Host " Head commit: $(Write-Link "https://github.com/$($Script:Repo)/commit/$($script:HeadSha)" $script:HeadSha.Substring(0,7))" -ForegroundColor DarkGray } + Write-Host "" +} + +function Find-WorkflowRun { + if ($WorkflowRunId -gt 0) { + $script:RunId = $WorkflowRunId + return + } + + $script:RunId = & gh api "repos/$($Script:Repo)/actions/workflows/$($Script:CIWorkflow)/runs?event=pull_request&head_sha=$($script:HeadSha)" ` + --jq '.workflow_runs | sort_by(.created_at, .updated_at) | reverse | .[0].id' 2>$null + + if (-not $script:RunId -or $script:RunId -eq "null") { + Write-Host "βœ— No workflow run found for PR #$PRNumber (SHA: $($script:HeadSha))" -ForegroundColor Red + Write-Host " Check: https://github.com/$($Script:Repo)/actions/workflows/$($Script:CIWorkflow)" + exit 1 + } + + if ($VerboseOutput) { Write-Host " Workflow run: $(Write-Link "https://github.com/$($Script:Repo)/actions/runs/$($script:RunId)" $script:RunId)" -ForegroundColor DarkGray } +} + +function Save-Artifacts { + $script:DownloadDir = Join-Path $tempDir "nuget-packages" + $runUrl = "https://github.com/$($Script:Repo)/actions/runs/$($script:RunId)" + + $exitCode = Invoke-WithSpinner "πŸ“¦ Downloading packages..." "gh" @("run", "download", $script:RunId, "-R", $Script:Repo, "--name", $Script:ArtifactName, "-D", $script:DownloadDir) + if ($exitCode -ne 0) { + Write-Host "βœ— Failed to download artifacts β€” build may still be in progress or artifacts may have expired" -ForegroundColor Red + Write-Host " $(Write-Link $runUrl "View workflow run")" -ForegroundColor DarkGray + exit 1 + } + + $script:Packages = Get-ChildItem -Path $script:DownloadDir -Filter "*.nupkg" -Recurse + if ($script:Packages.Count -eq 0) { + Write-Host "βœ— No NuGet packages found in downloaded artifacts" -ForegroundColor Red + exit 1 + } + + $totalSize = ($script:Packages | Measure-Object -Property Length -Sum).Sum + $sizeDisplay = if ($totalSize -ge 1MB) { "{0:N1} MB" -f ($totalSize / 1MB) } elseif ($totalSize -ge 1KB) { "{0:N0} KB" -f ($totalSize / 1KB) } else { "$totalSize B" } + Write-Host "πŸ“¦ Downloaded $($script:Packages.Count) packages ($sizeDisplay)" +} + +function Install-Packages { + New-Item -ItemType Directory -Path $hiveDir -Force | Out-Null + $script:Packages | Copy-Item -Destination $hiveDir -Force + Write-Host "πŸ“‚ Installed to $(Get-DisplayPath $hiveDir)" + + $script:Version = "" + $firstPkg = $script:Packages | Select-Object -First 1 + if ($firstPkg) { + Add-Type -AssemblyName System.IO.Compression.FileSystem + $zip = [System.IO.Compression.ZipFile]::OpenRead($firstPkg.FullName) + try { + $nuspec = $zip.Entries | Where-Object { $_.Name -like "*.nuspec" } | Select-Object -First 1 + if ($nuspec) { + $reader = [System.IO.StreamReader]::new($nuspec.Open()) + $xml = [xml]$reader.ReadToEnd() + $reader.Close() + $script:Version = $xml.package.metadata.version + } + } finally { + $zip.Dispose() + } + } +} + +function Register-NuGetSource { + $script:NuGetConfig = $null + + if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) { + Write-Host "⚠ dotnet CLI not found β€” configure NuGet source manually:" -ForegroundColor Yellow + Write-Host " dotnet nuget add source `"$hiveDir`" --name `"$sourceName`"" -ForegroundColor DarkGray + return + } + + $script:NuGetConfig = (& dotnet nuget config paths 2>$null | Select-Object -First 1) + + if (-not $script:NuGetConfig) { + Write-Host "⚠ Could not determine NuGet config file β€” configure manually:" -ForegroundColor Yellow + Write-Host " dotnet nuget add source `"$hiveDir`" --name `"$sourceName`"" -ForegroundColor DarkGray + return + } + + $existingSources = & dotnet nuget list source --configfile $script:NuGetConfig 2>$null + if ($existingSources -match [regex]::Escape($sourceName)) { + & dotnet nuget update source $sourceName --source $hiveDir --configfile $script:NuGetConfig 2>&1 | Out-Null + } else { + & dotnet nuget add source $hiveDir --name $sourceName --configfile $script:NuGetConfig 2>&1 | Out-Null + } + Write-Host "βš™οΈ Configured source $sourceName in $(Get-DisplayPath $script:NuGetConfig)" +} + +function Write-Summary { + Write-Host "" + if ($script:Version) { + Write-Host "🐢 Ready β€” use version $($script:Version) to test these changes" -ForegroundColor Green + } + + Write-Host "" + Write-Host "To undo:" -ForegroundColor DarkGray + $hivePath = Get-DisplayPath (Join-Path $InstallPath "hives" "community-toolkit-pr-$PRNumber") + if ($script:NuGetConfig) { + $configPath = Get-DisplayPath $script:NuGetConfig + Write-Host " dotnet nuget remove source `"$sourceName`" --configfile $configPath > `$null && rm -rf $hivePath" -ForegroundColor DarkGray + } else { + Write-Host " dotnet nuget remove source `"$sourceName`" > `$null && rm -rf $hivePath" -ForegroundColor DarkGray + } + + if ($KeepArchive) { + Write-Host "" + Write-Host "Archive kept at: $tempDir" -ForegroundColor DarkGray + } + + if ($VerboseOutput) { + Write-Host "" + Write-Host "Packages:" -ForegroundColor DarkGray + Get-ChildItem -Path $hiveDir -Filter "*.nupkg" | Sort-Object Name | ForEach-Object { + Write-Host " $($_.Name)" -ForegroundColor DarkGray + } + } +} + +# --- Entry Point --- + +if ($Help) { + Show-Help + exit 0 +} + +Test-Prerequisites + +if (-not $InstallPath) { + $InstallPath = Join-Path $HOME ".aspire" +} +$hiveDir = Join-Path $InstallPath "hives" "community-toolkit-pr-$PRNumber" "packages" +$sourceName = "CommunityToolkit-PR-$PRNumber" + +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "dogfood-pr-$([System.Guid]::NewGuid().ToString('N').Substring(0,8))" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + Resolve-PullRequest + Find-WorkflowRun + Save-Artifacts + Install-Packages + Register-NuGetSource + Write-Summary +} finally { + if (-not $KeepArchive -and (Test-Path $tempDir)) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/eng/scripts/dogfood-pr.sh b/eng/scripts/dogfood-pr.sh new file mode 100755 index 000000000..e5634dd63 --- /dev/null +++ b/eng/scripts/dogfood-pr.sh @@ -0,0 +1,262 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# dogfood-pr.sh - Download and install NuGet packages from a PR's build artifacts +# Usage: ./dogfood-pr.sh PR_NUMBER [OPTIONS] + +readonly REPO="CommunityToolkit/Aspire" +readonly CI_WORKFLOW="dotnet-ci.yml" +readonly ARTIFACT_NAME="nuget-packages" + +# --- Output Helpers --- + +RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[0;33m' +BOLD=$'\033[1m'; DIM=$'\033[2m'; RESET=$'\033[0m' + +link() { printf '\e]8;;%s\e\\%s\e]8;;\e\\' "$1" "$2"; } +display_path() { echo "${1/#$HOME/\~}"; } + +spin() { + local msg="$1"; shift + "$@" &>/dev/null & + local pid=$! + local chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' + local i=0 + while kill -0 "$pid" 2>/dev/null; do + printf "\033[1A\033[K%s %s\n" "${chars:i++%${#chars}:1}" "$msg" >&2 + sleep 0.1 + done + wait "$pid" + local rc=$? + printf "\033[1A\033[K" >&2 + return $rc +} + +say() { echo -e "$*" >&2; } +say_err() { echo -e "${RED}βœ— $*${RESET}" >&2; } +say_warn() { echo -e "${YELLOW}⚠ $*${RESET}" >&2; } +verbose() { [[ "$VERBOSE" == true ]] && echo -e "${DIM} $*${RESET}" >&2 || true; } + +show_help() { + cat </dev/null; then + say_err "GitHub CLI (gh) is required. Install from: https://cli.github.com" + exit 1 + fi + + if ! gh auth status &>/dev/null; then + say_err "GitHub CLI is not authenticated. Run: gh auth login" + exit 1 + fi +} + +resolve_pull_request() { + local pr_url="https://github.com/${REPO}/pull/${PR_NUMBER}" + local pr_json + pr_json=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '{sha: .head.sha, title: .title, author: .user.login}' 2>/dev/null) || { + say_err "PR #${PR_NUMBER} not found in ${REPO}" + exit 1 + } + + head_sha=$(echo "$pr_json" | jq -r '.sha') + local pr_title pr_author + pr_title=$(echo "$pr_json" | jq -r '.title') + pr_author=$(echo "$pr_json" | jq -r '.author') + + local author_display + author_display="$(link "https://github.com/${pr_author}" "@${pr_author}")" + + say "" + say "$(link "$pr_url" "PR #${PR_NUMBER}") β€” ${BOLD}${pr_title}${RESET} ${DIM}by ${author_display}${RESET}" + verbose "Head commit: $(link "https://github.com/${REPO}/commit/${head_sha}" "${head_sha:0:7}")" + say "" +} + +find_workflow_run() { + if [[ -n "$RUN_ID" ]]; then + workflow_run_id="$RUN_ID" + return + fi + + workflow_run_id=$(gh api "repos/${REPO}/actions/workflows/${CI_WORKFLOW}/runs?event=pull_request&head_sha=${head_sha}" \ + --jq '.workflow_runs | sort_by(.created_at, .updated_at) | reverse | .[0].id' 2>/dev/null) + + if [[ -z "$workflow_run_id" || "$workflow_run_id" == "null" ]]; then + say_err "No workflow run found for PR #${PR_NUMBER} (SHA: ${head_sha})" + say " Check: https://github.com/${REPO}/actions/workflows/${CI_WORKFLOW}" + exit 1 + fi + + verbose "Workflow run: $(link "https://github.com/${REPO}/actions/runs/${workflow_run_id}" "$workflow_run_id")" +} + +download_artifacts() { + download_dir="${temp_dir}/nuget-packages" + local run_url="https://github.com/${REPO}/actions/runs/${workflow_run_id}" + + say "" + if ! spin "πŸ“¦ Downloading packages..." gh run download "$workflow_run_id" -R "$REPO" --name "$ARTIFACT_NAME" -D "$download_dir"; then + say_err "Failed to download artifacts β€” build may still be in progress or artifacts may have expired" + say " ${DIM}$(link "$run_url" "View workflow run")${RESET}" + exit 1 + fi + + pkg_count=$(find "$download_dir" -name '*.nupkg' | wc -l) + if [[ "$pkg_count" -eq 0 ]]; then + say_err "No NuGet packages found in downloaded artifacts" + exit 1 + fi + + local download_size + download_size=$(du -sh "$download_dir" 2>/dev/null | cut -f1 | sed 's/K$/ KB/;s/M$/ MB/;s/G$/ GB/') + say "πŸ“¦ Downloaded ${BOLD}${pkg_count}${RESET} packages (${download_size})" +} + +install_packages() { + mkdir -p "$hive_dir" + find "$download_dir" -name '*.nupkg' -exec cp {} "$hive_dir/" \; + say "πŸ“‚ Installed to $(display_path "$hive_dir")" + + version="" + local first_pkg + first_pkg=$(find "$download_dir" -name '*.nupkg' -print -quit) + if [[ -n "$first_pkg" ]]; then + version=$(unzip -p "$first_pkg" "*.nuspec" 2>/dev/null | grep -oP '\K[^<]+') + fi +} + +configure_nuget_source() { + nuget_config="" + + if ! command -v dotnet &>/dev/null; then + say_warn "dotnet CLI not found β€” configure NuGet source manually:" + say " ${DIM}dotnet nuget add source \"${hive_dir}\" --name \"${source_name}\"${RESET}" + return + fi + + nuget_config=$(dotnet nuget config paths 2>/dev/null | sed -n '1p') + + if [[ -z "$nuget_config" ]]; then + say_warn "Could not determine NuGet config file β€” configure manually:" + say " ${DIM}dotnet nuget add source \"${hive_dir}\" --name \"${source_name}\"${RESET}" + return + fi + + if dotnet nuget list source --configfile "$nuget_config" 2>/dev/null | grep -Fq "$source_name"; then + dotnet nuget update source "$source_name" --source "$hive_dir" --configfile "$nuget_config" &>/dev/null + else + dotnet nuget add source "$hive_dir" --name "$source_name" --configfile "$nuget_config" &>/dev/null + fi + say "βš™οΈ Configured source ${BOLD}${source_name}${RESET} in $(display_path "$nuget_config")" +} + +print_summary() { + say "" + if [[ -n "$version" ]]; then + say "🐢 ${GREEN}Ready${RESET} β€” use version ${BOLD}${version}${RESET} to test these changes" + fi + + say "" + say "${DIM}To undo:${RESET}" + if [[ -n "${nuget_config:-}" ]]; then + say " ${DIM}dotnet nuget remove source \"${source_name}\" --configfile $(display_path "$nuget_config") > /dev/null && rm -rf $(display_path "${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}")${RESET}" + else + say " ${DIM}dotnet nuget remove source \"${source_name}\" > /dev/null && rm -rf $(display_path "${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}")${RESET}" + fi + + if [[ "$KEEP_ARCHIVE" == true ]]; then + say "" + say "Archive kept at: ${DIM}${temp_dir}${RESET}" + fi + + if [[ "$VERBOSE" == true ]]; then + say "" + say "${DIM}Packages:${RESET}" + find "$hive_dir" -name '*.nupkg' -printf ' %f\n' | sort >&2 + fi +} + +# --- Entry Point --- + +main() { + parse_arguments "$@" + check_prerequisites + + INSTALL_PREFIX="${INSTALL_PREFIX:-$HOME/.aspire}" + hive_dir="${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}/packages" + source_name="CommunityToolkit-PR-${PR_NUMBER}" + + temp_dir=$(mktemp -d -t dogfood-pr-XXXXXX) + [[ "$KEEP_ARCHIVE" != true ]] && trap 'rm -rf "$temp_dir"' EXIT + + resolve_pull_request + find_workflow_run + download_artifacts + install_packages + configure_nuget_source + print_summary +} + +main "$@" From b0196ea619bb3970e69ab1282f16c9a1fbfaa74d Mon Sep 17 00:00:00 2001 From: Marshal Hayes <17213165+marshalhayes@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:23:39 -0600 Subject: [PATCH 2/6] Quote paths in undo commands to handle spaces Use full quoted paths instead of unquoted display paths in the printed undo commands so they work correctly when copy-pasted on systems where paths contain spaces. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/scripts/dogfood-pr.ps1 | 7 +++---- eng/scripts/dogfood-pr.sh | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/eng/scripts/dogfood-pr.ps1 b/eng/scripts/dogfood-pr.ps1 index 7f1d2e297..0db5b4578 100755 --- a/eng/scripts/dogfood-pr.ps1 +++ b/eng/scripts/dogfood-pr.ps1 @@ -227,12 +227,11 @@ function Write-Summary { Write-Host "" Write-Host "To undo:" -ForegroundColor DarkGray - $hivePath = Get-DisplayPath (Join-Path $InstallPath "hives" "community-toolkit-pr-$PRNumber") + $hivePath = Join-Path $InstallPath "hives" "community-toolkit-pr-$PRNumber" if ($script:NuGetConfig) { - $configPath = Get-DisplayPath $script:NuGetConfig - Write-Host " dotnet nuget remove source `"$sourceName`" --configfile $configPath > `$null && rm -rf $hivePath" -ForegroundColor DarkGray + Write-Host " dotnet nuget remove source `"$sourceName`" --configfile `"$($script:NuGetConfig)`" > `$null && rm -rf `"$hivePath`"" -ForegroundColor DarkGray } else { - Write-Host " dotnet nuget remove source `"$sourceName`" > `$null && rm -rf $hivePath" -ForegroundColor DarkGray + Write-Host " dotnet nuget remove source `"$sourceName`" > `$null && rm -rf `"$hivePath`"" -ForegroundColor DarkGray } if ($KeepArchive) { diff --git a/eng/scripts/dogfood-pr.sh b/eng/scripts/dogfood-pr.sh index e5634dd63..f4d4da203 100755 --- a/eng/scripts/dogfood-pr.sh +++ b/eng/scripts/dogfood-pr.sh @@ -221,9 +221,9 @@ print_summary() { say "" say "${DIM}To undo:${RESET}" if [[ -n "${nuget_config:-}" ]]; then - say " ${DIM}dotnet nuget remove source \"${source_name}\" --configfile $(display_path "$nuget_config") > /dev/null && rm -rf $(display_path "${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}")${RESET}" + say " ${DIM}dotnet nuget remove source \"${source_name}\" --configfile \"${nuget_config}\" > /dev/null && rm -rf \"${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}\"${RESET}" else - say " ${DIM}dotnet nuget remove source \"${source_name}\" > /dev/null && rm -rf $(display_path "${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}")${RESET}" + say " ${DIM}dotnet nuget remove source \"${source_name}\" > /dev/null && rm -rf \"${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}\"${RESET}" fi if [[ "$KEEP_ARCHIVE" == true ]]; then From 61a2b1b9bf44d0509d1dfd78475a13eb047a0552 Mon Sep 17 00:00:00 2001 From: Marshal Hayes <17213165+marshalhayes@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:33:23 -0600 Subject: [PATCH 3/6] Validate flag arguments have values in dogfood-pr.sh Add guards for --run-id and --install-path to check that a value is provided and that it doesn't look like another flag. Prevents unbound variable errors and silent flag consumption. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/scripts/dogfood-pr.sh | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/eng/scripts/dogfood-pr.sh b/eng/scripts/dogfood-pr.sh index f4d4da203..6078f927b 100755 --- a/eng/scripts/dogfood-pr.sh +++ b/eng/scripts/dogfood-pr.sh @@ -73,8 +73,22 @@ parse_arguments() { while [[ $# -gt 0 ]]; do case "$1" in -h|--help) show_help; exit 0 ;; - -r|--run-id) RUN_ID="$2"; shift 2 ;; - --install-path) INSTALL_PREFIX="$2"; shift 2 ;; + -r|--run-id) + if [[ $# -lt 2 || "$2" == -* ]]; then + say_err "--run-id requires a value" + show_help + exit 1 + fi + RUN_ID="$2"; shift 2 + ;; + --install-path) + if [[ $# -lt 2 || "$2" == -* ]]; then + say_err "--install-path requires a value" + show_help + exit 1 + fi + INSTALL_PREFIX="$2"; shift 2 + ;; -v|--verbose) VERBOSE=true; shift ;; -k|--keep-archive) KEEP_ARCHIVE=true; shift ;; -*) say_err "Unknown option: $1"; show_help; exit 1 ;; From d9e5a89ee3bc3f8daf94c9fb818e4fe0db62d6a4 Mon Sep 17 00:00:00 2001 From: Marshal Hayes <17213165+marshalhayes@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:50:20 -0600 Subject: [PATCH 4/6] Improve portability and prerequisites in dogfood-pr.sh - Add unzip to prerequisite checks - Eliminate standalone jq dependency by using gh api --jq directly - Replace GNU-specific grep -oP with portable sed for nuspec parsing - Replace GNU-specific find -printf with portable find -exec basename - Simplify configure_nuget_source() by dropping --configfile and letting dotnet nuget use default config resolution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/scripts/dogfood-pr.ps1 | 9 ++++++- eng/scripts/dogfood-pr.sh | 55 +++++++++++++++++++++----------------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/eng/scripts/dogfood-pr.ps1 b/eng/scripts/dogfood-pr.ps1 index 0db5b4578..ec0e4faed 100755 --- a/eng/scripts/dogfood-pr.ps1 +++ b/eng/scripts/dogfood-pr.ps1 @@ -124,6 +124,13 @@ function Resolve-PullRequest { $authorDisplay = Write-Link "https://github.com/$prAuthor" "@$prAuthor" Write-Host "" + $cols = if ($Host.UI.RawUI.WindowSize.Width) { $Host.UI.RawUI.WindowSize.Width } else { 80 } + $prefix = "PR #$PRNumber β€” " + $suffix = " by @$prAuthor" + $maxTitle = $cols - $prefix.Length - $suffix.Length - 2 + if ($prTitle.Length -gt $maxTitle -and $maxTitle -gt 3) { + $prTitle = $prTitle.Substring(0, $maxTitle - 1) + "…" + } Write-Host "$(Write-Link $prUrl "PR #$PRNumber") β€” $prTitle by $authorDisplay" if ($VerboseOutput) { Write-Host " Head commit: $(Write-Link "https://github.com/$($Script:Repo)/commit/$($script:HeadSha)" $script:HeadSha.Substring(0,7))" -ForegroundColor DarkGray } Write-Host "" @@ -216,7 +223,7 @@ function Register-NuGetSource { } else { & dotnet nuget add source $hiveDir --name $sourceName --configfile $script:NuGetConfig 2>&1 | Out-Null } - Write-Host "βš™οΈ Configured source $sourceName in $(Get-DisplayPath $script:NuGetConfig)" + Write-Host "πŸ”§ Configured source $sourceName in $(Get-DisplayPath $script:NuGetConfig)" } function Write-Summary { diff --git a/eng/scripts/dogfood-pr.sh b/eng/scripts/dogfood-pr.sh index 6078f927b..6db34d6ec 100755 --- a/eng/scripts/dogfood-pr.sh +++ b/eng/scripts/dogfood-pr.sh @@ -124,25 +124,35 @@ check_prerequisites() { say_err "GitHub CLI is not authenticated. Run: gh auth login" exit 1 fi + + if ! command -v unzip &>/dev/null; then + say_err "unzip is required but not found" + exit 1 + fi } resolve_pull_request() { local pr_url="https://github.com/${REPO}/pull/${PR_NUMBER}" - local pr_json - pr_json=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '{sha: .head.sha, title: .title, author: .user.login}' 2>/dev/null) || { + head_sha=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha' 2>/dev/null) || { say_err "PR #${PR_NUMBER} not found in ${REPO}" exit 1 } - head_sha=$(echo "$pr_json" | jq -r '.sha') local pr_title pr_author - pr_title=$(echo "$pr_json" | jq -r '.title') - pr_author=$(echo "$pr_json" | jq -r '.author') + pr_title=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.title' 2>/dev/null) + pr_author=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.user.login' 2>/dev/null) local author_display author_display="$(link "https://github.com/${pr_author}" "@${pr_author}")" say "" + local cols=${COLUMNS:-$(tput cols 2>/dev/null || echo 80)} + local prefix="PR #${PR_NUMBER} β€” " + local suffix=" by @${pr_author}" + local max_title=$((cols - ${#prefix} - ${#suffix} - 2)) + if [[ ${#pr_title} -gt $max_title && $max_title -gt 3 ]]; then + pr_title="${pr_title:0:$((max_title - 1))}…" + fi say "$(link "$pr_url" "PR #${PR_NUMBER}") β€” ${BOLD}${pr_title}${RESET} ${DIM}by ${author_display}${RESET}" verbose "Head commit: $(link "https://github.com/${REPO}/commit/${head_sha}" "${head_sha:0:7}")" say "" @@ -197,33 +207,32 @@ install_packages() { local first_pkg first_pkg=$(find "$download_dir" -name '*.nupkg' -print -quit) if [[ -n "$first_pkg" ]]; then - version=$(unzip -p "$first_pkg" "*.nuspec" 2>/dev/null | grep -oP '\K[^<]+') + version=$(unzip -p "$first_pkg" "*.nuspec" 2>/dev/null \ + | sed -n 's:.*\([^<]*\).*:\1:p' \ + | head -n 1) fi } configure_nuget_source() { - nuget_config="" - if ! command -v dotnet &>/dev/null; then say_warn "dotnet CLI not found β€” configure NuGet source manually:" say " ${DIM}dotnet nuget add source \"${hive_dir}\" --name \"${source_name}\"${RESET}" return fi - nuget_config=$(dotnet nuget config paths 2>/dev/null | sed -n '1p') - - if [[ -z "$nuget_config" ]]; then - say_warn "Could not determine NuGet config file β€” configure manually:" - say " ${DIM}dotnet nuget add source \"${hive_dir}\" --name \"${source_name}\"${RESET}" - return + if dotnet nuget list source 2>/dev/null | grep -Fq "$source_name"; then + dotnet nuget update source "$source_name" --source "$hive_dir" &>/dev/null + else + dotnet nuget add source "$hive_dir" --name "$source_name" &>/dev/null fi - if dotnet nuget list source --configfile "$nuget_config" 2>/dev/null | grep -Fq "$source_name"; then - dotnet nuget update source "$source_name" --source "$hive_dir" --configfile "$nuget_config" &>/dev/null - else - dotnet nuget add source "$hive_dir" --name "$source_name" --configfile "$nuget_config" &>/dev/null + local config_display="" + local config_path + config_path=$(dotnet nuget config paths 2>/dev/null | head -n 1) + if [[ -n "$config_path" ]]; then + config_display=" in $(display_path "$config_path")" fi - say "βš™οΈ Configured source ${BOLD}${source_name}${RESET} in $(display_path "$nuget_config")" + say "πŸ”§ Configured source ${BOLD}${source_name}${RESET}${config_display}" } print_summary() { @@ -234,11 +243,7 @@ print_summary() { say "" say "${DIM}To undo:${RESET}" - if [[ -n "${nuget_config:-}" ]]; then - say " ${DIM}dotnet nuget remove source \"${source_name}\" --configfile \"${nuget_config}\" > /dev/null && rm -rf \"${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}\"${RESET}" - else - say " ${DIM}dotnet nuget remove source \"${source_name}\" > /dev/null && rm -rf \"${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}\"${RESET}" - fi + say " ${DIM}dotnet nuget remove source \"${source_name}\" > /dev/null && rm -rf \"${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}\"${RESET}" if [[ "$KEEP_ARCHIVE" == true ]]; then say "" @@ -248,7 +253,7 @@ print_summary() { if [[ "$VERBOSE" == true ]]; then say "" say "${DIM}Packages:${RESET}" - find "$hive_dir" -name '*.nupkg' -printf ' %f\n' | sort >&2 + find "$hive_dir" -name '*.nupkg' -exec basename {} \; | sed 's/^/ /' | sort >&2 fi } From b78d80cdded70d42fad7f5da51d3c7ef69963852 Mon Sep 17 00:00:00 2001 From: Marshal Hayes <17213165+marshalhayes@users.noreply.github.com> Date: Sat, 7 Mar 2026 15:24:17 -0600 Subject: [PATCH 5/6] Use native PowerShell syntax in undo commands Replace bash-style 'rm -rf', '> $null', and '&&' with PowerShell equivalents: Remove-Item -Recurse -Force, Out-Null, and semicolons. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/scripts/dogfood-pr.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/scripts/dogfood-pr.ps1 b/eng/scripts/dogfood-pr.ps1 index ec0e4faed..b36796952 100755 --- a/eng/scripts/dogfood-pr.ps1 +++ b/eng/scripts/dogfood-pr.ps1 @@ -236,9 +236,9 @@ function Write-Summary { Write-Host "To undo:" -ForegroundColor DarkGray $hivePath = Join-Path $InstallPath "hives" "community-toolkit-pr-$PRNumber" if ($script:NuGetConfig) { - Write-Host " dotnet nuget remove source `"$sourceName`" --configfile `"$($script:NuGetConfig)`" > `$null && rm -rf `"$hivePath`"" -ForegroundColor DarkGray + Write-Host " dotnet nuget remove source `"$sourceName`" --configfile `"$($script:NuGetConfig)`" | Out-Null; Remove-Item -Recurse -Force `"$hivePath`"" -ForegroundColor DarkGray } else { - Write-Host " dotnet nuget remove source `"$sourceName`" > `$null && rm -rf `"$hivePath`"" -ForegroundColor DarkGray + Write-Host " dotnet nuget remove source `"$sourceName`" | Out-Null; Remove-Item -Recurse -Force `"$hivePath`"" -ForegroundColor DarkGray } if ($KeepArchive) { From 9cb6924b41c864794d917855ac22447503520cdd Mon Sep 17 00:00:00 2001 From: Marshal Hayes <17213165+marshalhayes@users.noreply.github.com> Date: Sat, 7 Mar 2026 15:38:53 -0600 Subject: [PATCH 6/6] Restore configfile in displayed undo commands Keep add/update behavior on dotnet's default NuGet config resolution, but restore --configfile in the printed undo commands when the resolved config path is known so users can undo against the same nuget.config from any working directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/scripts/dogfood-pr.ps1 | 20 +++++++++----------- eng/scripts/dogfood-pr.sh | 15 ++++++++++----- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/eng/scripts/dogfood-pr.ps1 b/eng/scripts/dogfood-pr.ps1 index b36796952..4db07f034 100755 --- a/eng/scripts/dogfood-pr.ps1 +++ b/eng/scripts/dogfood-pr.ps1 @@ -209,21 +209,19 @@ function Register-NuGetSource { return } - $script:NuGetConfig = (& dotnet nuget config paths 2>$null | Select-Object -First 1) - - if (-not $script:NuGetConfig) { - Write-Host "⚠ Could not determine NuGet config file β€” configure manually:" -ForegroundColor Yellow - Write-Host " dotnet nuget add source `"$hiveDir`" --name `"$sourceName`"" -ForegroundColor DarkGray - return + $existingSources = & dotnet nuget list source 2>$null + if ($existingSources -match [regex]::Escape($sourceName)) { + & dotnet nuget update source $sourceName --source $hiveDir 2>&1 | Out-Null + } else { + & dotnet nuget add source $hiveDir --name $sourceName 2>&1 | Out-Null } - $existingSources = & dotnet nuget list source --configfile $script:NuGetConfig 2>$null - if ($existingSources -match [regex]::Escape($sourceName)) { - & dotnet nuget update source $sourceName --source $hiveDir --configfile $script:NuGetConfig 2>&1 | Out-Null + $script:NuGetConfig = (& dotnet nuget config paths 2>$null | Select-Object -First 1) + if ($script:NuGetConfig) { + Write-Host "πŸ”§ Configured source $sourceName in $(Get-DisplayPath $script:NuGetConfig)" } else { - & dotnet nuget add source $hiveDir --name $sourceName --configfile $script:NuGetConfig 2>&1 | Out-Null + Write-Host "πŸ”§ Configured source $sourceName" } - Write-Host "πŸ”§ Configured source $sourceName in $(Get-DisplayPath $script:NuGetConfig)" } function Write-Summary { diff --git a/eng/scripts/dogfood-pr.sh b/eng/scripts/dogfood-pr.sh index 6db34d6ec..6eaac43c7 100755 --- a/eng/scripts/dogfood-pr.sh +++ b/eng/scripts/dogfood-pr.sh @@ -214,6 +214,8 @@ install_packages() { } configure_nuget_source() { + nuget_config="" + if ! command -v dotnet &>/dev/null; then say_warn "dotnet CLI not found β€” configure NuGet source manually:" say " ${DIM}dotnet nuget add source \"${hive_dir}\" --name \"${source_name}\"${RESET}" @@ -227,10 +229,9 @@ configure_nuget_source() { fi local config_display="" - local config_path - config_path=$(dotnet nuget config paths 2>/dev/null | head -n 1) - if [[ -n "$config_path" ]]; then - config_display=" in $(display_path "$config_path")" + nuget_config=$(dotnet nuget config paths 2>/dev/null | head -n 1) + if [[ -n "$nuget_config" ]]; then + config_display=" in $(display_path "$nuget_config")" fi say "πŸ”§ Configured source ${BOLD}${source_name}${RESET}${config_display}" } @@ -243,7 +244,11 @@ print_summary() { say "" say "${DIM}To undo:${RESET}" - say " ${DIM}dotnet nuget remove source \"${source_name}\" > /dev/null && rm -rf \"${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}\"${RESET}" + if [[ -n "${nuget_config:-}" ]]; then + say " ${DIM}dotnet nuget remove source \"${source_name}\" --configfile \"${nuget_config}\" > /dev/null && rm -rf \"${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}\"${RESET}" + else + say " ${DIM}dotnet nuget remove source \"${source_name}\" > /dev/null && rm -rf \"${INSTALL_PREFIX}/hives/community-toolkit-pr-${PR_NUMBER}\"${RESET}" + fi if [[ "$KEEP_ARCHIVE" == true ]]; then say ""