Skip to content
Draft
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
4 changes: 2 additions & 2 deletions .github/actions/check-changed-files/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ runs:
- name: Check changed files against patterns
id: check_files
shell: bash
env:
PATTERNS_FILE: ${{ inputs.patterns_file }}
run: |
set -ex

Expand Down Expand Up @@ -97,8 +99,6 @@ runs:

PATTERNS=()

PATTERNS_FILE="${{ inputs.patterns_file }}"

# Read glob patterns from file, skip comments and blank lines
FULL_PATH="${GITHUB_WORKSPACE}/${PATTERNS_FILE}"
if [ ! -f "$FULL_PATH" ]; then
Expand Down
40 changes: 22 additions & 18 deletions .github/actions/enumerate-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,10 @@ runs:

- name: Generate metadata and source partitions
shell: bash
run: >
./build.sh -test
/p:TestRunnerName=TestEnumerationRunsheetBuilder
/p:TestMatrixOutputPath=artifacts/canonical-test-matrix.json
/p:GenerateCIPartitions=true
/p:TestEnumerationMetadataOnly=true
/p:SkipGenerateCanonicalTestMatrix=true
${{ inputs.beforeBuildPropsPath != '' && format('/p:BeforeBuildPropsPath={0}', inputs.beforeBuildPropsPath) || '' }}
/bl
${{ inputs.buildArgs }}
env:
BUILD_ARGS: ${{ inputs.buildArgs }}
BEFORE_BUILD_PROPS_PATH: ${{ inputs.beforeBuildPropsPath }}
run: bash .github/workflows/enumerate-test-metadata.sh

- name: Find class-mode split test projects
id: class_mode
Expand All @@ -111,14 +105,21 @@ runs:
- name: Generate class partitions and canonical test matrix
if: ${{ steps.class_mode.outputs.count != '0' }}
shell: bash
run: >
./build.sh -test
/p:TestRunnerName=TestEnumerationRunsheetBuilder
/p:TestMatrixOutputPath=artifacts/canonical-test-matrix.json
/p:GenerateCIPartitions=true
/p:BeforeBuildPropsPath=${{ github.workspace }}/artifacts/ClassModeTestProjects.props
/bl
${{ inputs.buildArgs }}
env:
BUILD_ARGS: ${{ inputs.buildArgs }}
run: |
args_file=$(mktemp)
trap 'rm -f "$args_file"' EXIT
python3 .github/workflows/parse-command-line.py --format nul BUILD_ARGS > "$args_file"
build_args=()
while IFS= read -r -d '' arg; do build_args+=("$arg"); done < "$args_file"
./build.sh -test \
/p:TestRunnerName=TestEnumerationRunsheetBuilder \
/p:TestMatrixOutputPath=artifacts/canonical-test-matrix.json \
/p:GenerateCIPartitions=true \
"/p:BeforeBuildPropsPath=$GITHUB_WORKSPACE/artifacts/ClassModeTestProjects.props" \
/bl \
"${build_args[@]}"

- name: Generate canonical test matrix
if: ${{ steps.class_mode.outputs.count == '0' }}
Expand All @@ -137,10 +138,13 @@ runs:

if (Test-Path $canonicalMatrixPath) {
& $expandScript -CanonicalMatrixFile $canonicalMatrixPath -OutputToGitHubEnv
# Keep the large matrix out of both shell source and per-variable OS env limits.
& $expandScript -CanonicalMatrixFile $canonicalMatrixPath -OutputMatrixFile "$env:GITHUB_WORKSPACE/artifacts/github-test-matrix.json"
} else {
$emptyMatrix = '{"include":[]}'
"all_tests=$emptyMatrix" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "No canonical test matrix found, using empty matrix"
$emptyMatrix | Set-Content -Path "$env:GITHUB_WORKSPACE/artifacts/github-test-matrix.json" -Encoding utf8
}

- name: Upload logs
Expand Down
18 changes: 12 additions & 6 deletions .github/actions/unlock-macos-keychain/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,19 @@ runs:
- name: Unlock keychain
if: runner.os == 'macOS'
shell: bash
env:
KEYCHAIN_NAME: ${{ inputs.keychain-name }}
KEYCHAIN_PASSWORD: ${{ inputs.keychain-password }}
KEYCHAIN_TIMEOUT: ${{ inputs.timeout }}
run: |
KEYCHAIN_PATH="$HOME/Library/Keychains/${{ inputs.keychain-name }}"
KEYCHAIN_PATH="$HOME/Library/Keychains/$KEYCHAIN_NAME"

echo "Unlocking keychain: $KEYCHAIN_PATH"
security unlock-keychain -p "${{ inputs.keychain-password }}" "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

if [ "${{ inputs.timeout }}" != "0" ]; then
echo "Setting keychain timeout to ${{ inputs.timeout }} seconds"
security set-keychain-settings -lut ${{ inputs.timeout }} "$KEYCHAIN_PATH"
if [ "$KEYCHAIN_TIMEOUT" != "0" ]; then
echo "Setting keychain timeout to $KEYCHAIN_TIMEOUT seconds"
security set-keychain-settings -lut "$KEYCHAIN_TIMEOUT" "$KEYCHAIN_PATH"
else
echo "Disabling keychain timeout"
security set-keychain-settings "$KEYCHAIN_PATH"
Expand All @@ -48,8 +52,10 @@ runs:
- name: Verify keychain is unlocked
if: runner.os == 'macOS'
shell: bash
env:
KEYCHAIN_NAME: ${{ inputs.keychain-name }}
run: |
KEYCHAIN_PATH="$HOME/Library/Keychains/${{ inputs.keychain-name }}"
KEYCHAIN_PATH="$HOME/Library/Keychains/$KEYCHAIN_NAME"

# Try to access the keychain without password prompt
if security show-keychain-info "$KEYCHAIN_PATH" 2>&1 | grep -q "no-timeout"; then
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# GitHub Workflows

## Script inputs

Pass GitHub expressions containing inputs, matrix values, or step outputs through
step `env:` values, then reference quoted shell variables (or `process.env` in
JavaScript). Do not interpolate them into executable `run:` or `script:` bodies.
See [GitHub's script injection guidance](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections).

Reusable workflows accepting argument strings use `parse-command-line.py` to
split them into literal argument arrays. These inputs support POSIX-style
quoting, for example `--filter-trait "Partition=My Tests"`, on all platforms.
They do not support shell expressions, command substitution, variable expansion,
or wildcard expansion. Malformed quoting fails the step. The parser uses the
Python installation supplied by the GitHub-hosted runner (`python` on Windows,
`python3` on Linux/macOS); callers must pass the resulting arrays without
evaluating them as shell source.

Large matrices must travel through files rather than executable here-strings or
environment variables, which are subject to per-variable OS size limits.
`enumerate-tests` writes `artifacts/github-test-matrix.json` for `tests.yml`.
Metadata enumeration and matrix-file transport live in `enumerate-test-metadata.sh`
and `write-test-matrix.ps1`; behavior tests execute these scripts directly.

## Agentic workflow maintenance

Agentic workflows are authored in `.github/workflows/*.md`. Upgrade the active
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/backmerge-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ jobs:
id: create-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
BEHIND_COUNT: ${{ steps.check.outputs.behind_count }}
run: |
# Check if a PR already exists for this branch
EXISTING_PR=$(gh pr list --head backmerge/release-13.5-to-main --base main --json number --jq '.[0].number // empty')
Expand All @@ -87,7 +88,7 @@ jobs:

This PR merges changes from \`release/13.5\` back into \`main\`.

**Commits to merge:** ${{ steps.check.outputs.behind_count }}
**Commits to merge:** $BEHIND_COUNT

This PR was created automatically to keep \`main\` up-to-date with release branch changes.
Once approved, please merge using a **merge commit** (not squash or rebase).
Expand Down Expand Up @@ -118,6 +119,8 @@ jobs:
- name: Create issue for merge conflicts
if: steps.check.outputs.changes == 'true' && steps.merge.outputs.merge_success == 'false'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
BEHIND_COUNT: ${{ steps.check.outputs.behind_count }}
with:
script: |
const workflowRunUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
Expand Down Expand Up @@ -163,7 +166,7 @@ jobs:
'### Details',
'',
`**Workflow run:** ${workflowRunUrl}`,
'**Commits to merge:** ${{ steps.check.outputs.behind_count }}',
`**Commits to merge:** ${process.env.BEHIND_COUNT}`,
'',
'---',
`*This issue was created automatically by the [backmerge-release](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/workflows/backmerge-release.yml) workflow.*`
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/backport.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,11 @@ jobs:

- name: Post backport started comment to pull request
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
TARGET_BRANCH: ${{ steps.target-branch-extractor.outputs.result }}
with:
script: |
const target_branch = '${{ steps.target-branch-extractor.outputs.result }}';
const target_branch = process.env.TARGET_BRANCH;
const workflow_run_url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const backport_start_body = `Started backporting to \`${target_branch}\` ([link to workflow run](${workflow_run_url}))`;
await github.rest.issues.createComment({
Expand Down Expand Up @@ -154,6 +156,7 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
TARGET_BRANCH: ${{ steps.target-branch-extractor.outputs.result }}
BACKPORT_PR_TITLE_TEMPLATE: '[%target_branch%] %source_pr_title%'
BACKPORT_PR_DESCRIPTION_TEMPLATE: |
Backport of #%source_pr_number% to %target_branch%
Expand All @@ -170,7 +173,7 @@ jobs:
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const target_branch = '${{ steps.target-branch-extractor.outputs.result }}';
const target_branch = process.env.TARGET_BRANCH;
const repo_owner = context.payload.repository.owner.login;
const repo_name = context.payload.repository.name;
const pr_number = context.payload.issue.number;
Expand Down
97 changes: 57 additions & 40 deletions .github/workflows/build-cli-native-archives.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ jobs:
# separate parse of the version suffix at runtime.
env:
ASPIRE_CLI_CHANNEL: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || (startsWith(github.ref, 'refs/heads/release/') || startsWith(github.ref, 'refs/heads/internal/release/')) && 'staging' || github.ref == 'refs/heads/main' && 'local' || 'daily' }}
BUILD_CONFIGURATION: ${{ inputs.configuration }}
TARGET_RID: ${{ matrix.targets.rids }}
VERSION_OVERRIDE_ARG: ${{ inputs.versionOverrideArg }}

steps:
- name: Configure NuGet package cache (Windows)
Expand Down Expand Up @@ -82,21 +85,25 @@ jobs:
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$buildLog = '${{ github.workspace }}/artifacts/log/${{ inputs.configuration }}/BuildBundleDeps.log'
$python = if ($IsWindows) { 'python' } else { 'python3' }
$versionArgsJson = & $python .github/workflows/parse-command-line.py VERSION_OVERRIDE_ARG
if ($LASTEXITCODE -ne 0) { throw 'Invalid versionOverrideArg.' }
$versionArgs = @($versionArgsJson | ConvertFrom-Json)
$buildLog = "$env:GITHUB_WORKSPACE/artifacts/log/$env:BUILD_CONFIGURATION/BuildBundleDeps.log"
# Repository cleanup can remove artifacts/log while the build is streaming output.
$temporaryLog = Join-Path '${{ runner.temp }}' 'BuildBundleDeps.log'
& '${{ runner.os == 'Windows' && './build.cmd' || './build.sh' }}' `
-ci `
-restore `
-build `
-pack `
-configuration '${{ inputs.configuration }}' `
'/bl:${{ github.workspace }}/artifacts/log/${{ inputs.configuration }}/BuildBundleDeps.binlog' `
-configuration $env:BUILD_CONFIGURATION `
"/bl:$env:GITHUB_WORKSPACE/artifacts/log/$env:BUILD_CONFIGURATION/BuildBundleDeps.binlog" `
/p:ContinuousIntegrationBuild=true `
/p:BuildBundleDepsOnly=true `
/p:TargetRids=${{ matrix.targets.rids }} `
/p:AspireCliChannel=${{ env.ASPIRE_CLI_CHANNEL }} `
${{ inputs.versionOverrideArg }} 2>&1 | Tee-Object -FilePath $temporaryLog
"/p:TargetRids=$env:TARGET_RID" `
"/p:AspireCliChannel=$env:ASPIRE_CLI_CHANNEL" `
@versionArgs 2>&1 | Tee-Object -FilePath $temporaryLog
$buildExitCode = $LASTEXITCODE
New-Item -ItemType Directory -Force -Path (Split-Path $buildLog -Parent) | Out-Null
Copy-Item -LiteralPath $temporaryLog -Destination $buildLog
Expand All @@ -106,61 +113,71 @@ jobs:
shell: pwsh
run: >
./eng/scripts/verify-native-dashboard-warnings.ps1
-BuildLog "${{ github.workspace }}/artifacts/log/${{ inputs.configuration }}/BuildBundleDeps.log"
-BuildLog "$env:GITHUB_WORKSPACE/artifacts/log/$env:BUILD_CONFIGURATION/BuildBundleDeps.log"

- name: Build bundle payload archive
# SkipNativeBuild skips only the outer CLI. Bundle.proj still publishes
# native tray payloads on Windows/macOS and verifies the actual archive.
run: >
${{ runner.os == 'Windows' && './dotnet.cmd' || './dotnet.sh' }}
msbuild
eng/Bundle.proj
/restore
/p:Configuration=${{ inputs.configuration }}
/p:TargetRid=${{ matrix.targets.rids }}
/p:BundleVersion=ci-bundlepayload
/p:SkipNativeBuild=true
/p:ContinuousIntegrationBuild=true
/bl:${{ github.workspace }}/artifacts/log/${{ inputs.configuration }}/BundlePayload.binlog
${{ inputs.versionOverrideArg }}
shell: pwsh
run: |
$python = if ($IsWindows) { 'python' } else { 'python3' }
$versionArgsJson = & $python .github/workflows/parse-command-line.py VERSION_OVERRIDE_ARG
if ($LASTEXITCODE -ne 0) { throw 'Invalid versionOverrideArg.' }
$versionArgs = @($versionArgsJson | ConvertFrom-Json)
& '${{ runner.os == 'Windows' && './dotnet.cmd' || './dotnet.sh' }}' `
msbuild `
eng/Bundle.proj `
/restore `
"/p:Configuration=$env:BUILD_CONFIGURATION" `
"/p:TargetRid=$env:TARGET_RID" `
/p:BundleVersion=ci-bundlepayload `
/p:SkipNativeBuild=true `
/p:ContinuousIntegrationBuild=true `
"/bl:$env:GITHUB_WORKSPACE/artifacts/log/$env:BUILD_CONFIGURATION/BundlePayload.binlog" `
@versionArgs

- name: Smoke test bundled Native AOT TerminalHost
if: ${{ !contains(matrix.targets.rids, 'musl') && ((runner.arch == 'ARM64' && endsWith(matrix.targets.rids, 'arm64')) || (runner.arch == 'X64' && endsWith(matrix.targets.rids, 'x64'))) }}
shell: pwsh
run: >
./eng/scripts/test-native-terminalhost.ps1
-TerminalHostPath "${{ github.workspace }}/artifacts/bundle/${{ matrix.targets.rids }}/terminalhost/Aspire.TerminalHost${{ runner.os == 'Windows' && '.exe' || '' }}"
-TerminalHostPath "$env:GITHUB_WORKSPACE/artifacts/bundle/$env:TARGET_RID/terminalhost/Aspire.TerminalHost${{ runner.os == 'Windows' && '.exe' || '' }}"

- name: Build CLI packages
run: >
${{ runner.os == 'Windows' && './build.cmd' || './build.sh' }}
-ci
-build
-pack
-restore
-configuration ${{ inputs.configuration }}
/bl:${{ github.workspace }}/artifacts/log/${{ inputs.configuration }}/BuildCli.binlog
/p:ContinuousIntegrationBuild=true
/p:SkipManagedBuild=true
/p:TargetRids=${{ matrix.targets.rids }}
/p:BundlePayloadPath=${{ github.workspace }}/artifacts/bundle/aspire-ci-bundlepayload-${{ matrix.targets.rids }}.tar.gz
/p:AspireCliChannel=${{ env.ASPIRE_CLI_CHANNEL }}
${{ inputs.versionOverrideArg }}
shell: pwsh
run: |
$python = if ($IsWindows) { 'python' } else { 'python3' }
$versionArgsJson = & $python .github/workflows/parse-command-line.py VERSION_OVERRIDE_ARG
if ($LASTEXITCODE -ne 0) { throw 'Invalid versionOverrideArg.' }
$versionArgs = @($versionArgsJson | ConvertFrom-Json)
& '${{ runner.os == 'Windows' && './build.cmd' || './build.sh' }}' `
-ci `
-build `
-pack `
-restore `
-configuration $env:BUILD_CONFIGURATION `
"/bl:$env:GITHUB_WORKSPACE/artifacts/log/$env:BUILD_CONFIGURATION/BuildCli.binlog" `
/p:ContinuousIntegrationBuild=true `
/p:SkipManagedBuild=true `
"/p:TargetRids=$env:TARGET_RID" `
"/p:BundlePayloadPath=$env:GITHUB_WORKSPACE/artifacts/bundle/aspire-ci-bundlepayload-$env:TARGET_RID.tar.gz" `
"/p:AspireCliChannel=$env:ASPIRE_CLI_CHANNEL" `
@versionArgs

- name: Verify CLI tool nupkg
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'

$rid = '${{ matrix.targets.rids }}'
$rid = $env:TARGET_RID
$archiveExtension = if ($rid.StartsWith('win-')) { 'zip' } else { 'tar.gz' }
$archive = @(Get-ChildItem -Path 'artifacts/packages/${{ inputs.configuration }}' -Filter "aspire-cli-$rid-*.$archiveExtension" -Recurse -File -ErrorAction SilentlyContinue)
$archive = @(Get-ChildItem -Path "artifacts/packages/$env:BUILD_CONFIGURATION" -Filter "aspire-cli-$rid-*.$archiveExtension" -Recurse -File -ErrorAction SilentlyContinue)
if ($archive.Count -ne 1) {
throw "Expected exactly one CLI archive for $rid, but found $($archive.Count): $($archive.FullName -join ', ')"
}

eng/scripts/verify-cli-tool-nupkg.ps1 `
-PackagesDir artifacts/packages/${{ inputs.configuration }} `
-PackagesDir "artifacts/packages/$env:BUILD_CONFIGURATION" `
-Rid $rid `
-ArchivePath $archive[0].FullName

Expand Down Expand Up @@ -253,15 +270,15 @@ jobs:
run: |
$ErrorActionPreference = 'Stop'

$rid = '${{ matrix.targets.rids }}'
$rid = $env:TARGET_RID
$archiveExtension = if ($rid.StartsWith('win-')) { 'zip' } else { 'tar.gz' }
$archive = @(Get-ChildItem -Path 'artifacts/packages/${{ inputs.configuration }}' -Filter "aspire-cli-$rid-*.$archiveExtension" -Recurse -File -ErrorAction SilentlyContinue)
$archive = @(Get-ChildItem -Path "artifacts/packages/$env:BUILD_CONFIGURATION" -Filter "aspire-cli-$rid-*.$archiveExtension" -Recurse -File -ErrorAction SilentlyContinue)
if ($archive.Count -ne 1) {
throw "Expected exactly one CLI archive for $rid, but found $($archive.Count): $($archive.FullName -join ', ')"
}

eng/scripts/verify-cli-npm-package.ps1 `
-PackagesDir artifacts/packages/${{ inputs.configuration }} `
-PackagesDir "artifacts/packages/$env:BUILD_CONFIGURATION" `
-Rid $rid `
-ArchivePath $archive[0].FullName

Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/build-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Build with packages
run: ./build.sh -restore -build -ci -pack -bl -p:InstallBrowsersForPlaywright=false -p:SkipTestProjects=true -p:SkipPlaygroundProjects=true -p:SkipBundleDeps=true ${{ inputs.versionOverrideArg }}
env:
VERSION_OVERRIDE_ARG: ${{ inputs.versionOverrideArg }}
run: |
args_file=$(mktemp)
trap 'rm -f "$args_file"' EXIT
python3 .github/workflows/parse-command-line.py --format nul VERSION_OVERRIDE_ARG > "$args_file"
version_args=()
while IFS= read -r -d '' arg; do version_args+=("$arg"); done < "$args_file"
./build.sh -restore -build -ci -pack -bl -p:InstallBrowsersForPlaywright=false -p:SkipTestProjects=true -p:SkipPlaygroundProjects=true -p:SkipBundleDeps=true "${version_args[@]}"

- name: Generate template component manifest
shell: pwsh
Expand Down
Loading
Loading