Skip to content

Commit d0a1845

Browse files
committed
Merge remote-tracking branch 'origin/main' into copilot/add-buildtestsonly-property
2 parents e5dab02 + b045f8e commit d0a1845

File tree

8 files changed

+360
-90
lines changed

8 files changed

+360
-90
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
name: Test Scenario Workflow
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: write
10+
11+
jobs:
12+
test-scenario:
13+
# Only run when the comment starts with /test-scenario on a PR
14+
if: >-
15+
${{
16+
startsWith(github.event.comment.body, '/test-scenario') &&
17+
github.event.issue.pull_request
18+
}}
19+
runs-on: ubuntu-latest
20+
env:
21+
REPO_OWNER: dotnet
22+
REPO_NAME: aspire-playground
23+
GH_CLI_VERSION: 2.81.0
24+
GH_TOKEN: ${{ secrets.GH_PLAYGROUND_TOKEN }}
25+
steps:
26+
- name: Checkout repository
27+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
28+
29+
- name: Parse and validate scenario name
30+
id: parse_scenario
31+
env:
32+
COMMENT_BODY: ${{ github.event.comment.body }}
33+
run: |
34+
echo "Comment body: $COMMENT_BODY"
35+
36+
# Extract scenario name from comment
37+
SCENARIO_NAME=$(echo "$COMMENT_BODY" | \
38+
grep -oP '^/test-scenario\s+\K[a-z0-9]+(-[a-z0-9]+)*[a-z0-9]$' | head -1)
39+
40+
if [ -z "$SCENARIO_NAME" ]; then
41+
echo "Error: Invalid or missing scenario name"
42+
echo "Expected format: /test-scenario scenario-name"
43+
echo "Scenario name must be lowercase alphanumeric with hyphens"
44+
exit 1
45+
fi
46+
47+
echo "Scenario name: $SCENARIO_NAME"
48+
echo "scenario_name=$SCENARIO_NAME" >> $GITHUB_OUTPUT
49+
50+
- name: Check for prompt file
51+
id: check_prompt
52+
run: |
53+
SCENARIO_NAME="${{ steps.parse_scenario.outputs.scenario_name }}"
54+
PROMPT_FILE="tests/agent-scenarios/${SCENARIO_NAME}/prompt.md"
55+
56+
if [ ! -f "$PROMPT_FILE" ]; then
57+
echo "Error: Prompt file not found at $PROMPT_FILE"
58+
exit 1
59+
fi
60+
61+
echo "Found prompt file: $PROMPT_FILE"
62+
echo "prompt_file=$PROMPT_FILE" >> $GITHUB_OUTPUT
63+
64+
- name: Download and install GitHub CLI
65+
run: |
66+
CURRENT_VERSION=""
67+
if command -v gh &> /dev/null; then
68+
CURRENT_VERSION=$(gh --version | \
69+
grep -oP 'gh version \K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
70+
echo "Current GitHub CLI version: $CURRENT_VERSION"
71+
fi
72+
73+
if [ "$CURRENT_VERSION" = "$GH_CLI_VERSION" ]; then
74+
echo "GitHub CLI v${GH_CLI_VERSION} already installed"
75+
else
76+
echo "Downloading GitHub CLI v${GH_CLI_VERSION}..."
77+
DOWNLOAD_URL="https://github.com/cli/cli/releases/download/v${GH_CLI_VERSION}"
78+
ARCHIVE_NAME="gh_${GH_CLI_VERSION}_linux_amd64.tar.gz"
79+
curl -fsSL "${DOWNLOAD_URL}/${ARCHIVE_NAME}" -o gh.tar.gz
80+
tar -xzf gh.tar.gz
81+
sudo mv "gh_${GH_CLI_VERSION}_linux_amd64/bin/gh" /usr/local/bin/
82+
rm -rf gh.tar.gz "gh_${GH_CLI_VERSION}_linux_amd64"
83+
84+
echo "Verifying GitHub CLI installation..."
85+
gh --version
86+
fi
87+
88+
- name: Create agent task
89+
id: create_agent_task
90+
run: |
91+
echo "Creating agent task..."
92+
PROMPT_FILE="${{ steps.check_prompt.outputs.prompt_file }}"
93+
94+
# Create the agent task using stdin and capture the output
95+
OUTPUT=$(cat "$PROMPT_FILE" | gh agent-task create \
96+
--repo "${REPO_OWNER}/${REPO_NAME}" \
97+
-F - \
98+
2>&1)
99+
100+
echo "Agent task output:"
101+
echo "$OUTPUT"
102+
103+
# Extract the PR URL from the output
104+
PR_URL=$(echo "$OUTPUT" | \
105+
grep -oP 'https://github.com/[^/]+/[^/]+/pull/\d+' | head -1)
106+
107+
if [ -z "$PR_URL" ]; then
108+
echo "Warning: Could not extract PR URL from output"
109+
echo "pr_url=" >> $GITHUB_OUTPUT
110+
else
111+
echo "Successfully created agent task with PR: $PR_URL"
112+
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
113+
fi
114+
115+
- name: Comment on PR with agent task link
116+
if: steps.create_agent_task.outputs.pr_url != ''
117+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
118+
with:
119+
github-token: ${{ secrets.GITHUB_TOKEN }}
120+
script: |
121+
const prUrl = '${{ steps.create_agent_task.outputs.pr_url }}';
122+
const scenarioName = '${{ steps.parse_scenario.outputs.scenario_name }}';
123+
const comment = `🤖 **AI Agent Task Created**
124+
125+
Scenario: **${scenarioName}**
126+
127+
An AI agent has been triggered to execute this scenario.
128+
You can track the progress here:
129+
130+
${prUrl}`;
131+
132+
await github.rest.issues.createComment({
133+
issue_number: context.issue.number,
134+
owner: context.repo.owner,
135+
repo: context.repo.repo,
136+
body: comment
137+
});

.github/workflows/tests-outerloop.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ on:
1616
schedule:
1717
- cron: '0 2 * * *' # Daily at 02:00 UTC
1818

19-
# Trigger on pull requests that modify infrastructure or workflow files
20-
pull_request:
21-
paths:
22-
- '.github/actions/**'
23-
- '.github/workflows/**'
24-
- 'eng/**'
25-
- '!eng/pipelines/**'
26-
- '!eng/scripts/**'
27-
- '!eng/*pack/**'
19+
# TEMPORARILY DISABLED pull_request trigger due to #12143 (disk space issues): https://github.com/dotnet/aspire/issues/12143
20+
# pull_request:
21+
# paths:
22+
# - '.github/actions/**'
23+
# - '.github/workflows/**'
24+
# - 'eng/**'
25+
# - '!eng/pipelines/**'
26+
# - '!eng/scripts/**'
27+
# - '!eng/*pack/**'
2828

2929
concurrency:
3030
group: ${{ github.workflow }}-${{ github.ref }}

.github/workflows/tests-quarantine.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ on:
1616
schedule:
1717
- cron: '0 2,14 * * *' # Twice daily at 02:00 and 14:00 UTC
1818

19-
# Trigger on pull requests that modify infrastructure or workflow files
20-
pull_request:
21-
paths:
22-
- '.github/actions/**'
23-
- '.github/workflows/**'
24-
- 'eng/**'
25-
- '!eng/pipelines/**'
26-
- '!eng/scripts/**'
27-
- '!eng/*pack/**'
19+
# TEMPORARILY DISABLED pull_request trigger due to #12143 (disk space issues): https://github.com/dotnet/aspire/issues/12143
20+
# pull_request:
21+
# paths:
22+
# - '.github/actions/**'
23+
# - '.github/workflows/**'
24+
# - 'eng/**'
25+
# - '!eng/pipelines/**'
26+
# - '!eng/scripts/**'
27+
# - '!eng/*pack/**'
2828

2929
concurrency:
3030
group: ${{ github.workflow }}-${{ github.ref }}

src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs

Lines changed: 97 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -183,101 +183,128 @@ private static async Task ExecuteStepsAsTaskDag(
183183
// Validate no cycles exist in the dependency graph
184184
ValidateDependencyGraph(steps, stepsByName);
185185

186-
// Create a TaskCompletionSource for each step
187-
var stepCompletions = new Dictionary<string, TaskCompletionSource>(steps.Count, StringComparer.Ordinal);
188-
foreach (var step in steps)
189-
{
190-
stepCompletions[step.Name] = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
191-
}
186+
// Create a linked CancellationTokenSource that will be cancelled when any step fails
187+
// or when the original context token is cancelled
188+
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken);
189+
190+
// Store the original token and set the linked token on the context
191+
var originalToken = context.CancellationToken;
192+
context.CancellationToken = linkedCts.Token;
192193

193-
// Execute a step after its dependencies complete
194-
async Task ExecuteStepWithDependencies(PipelineStep step)
194+
try
195195
{
196-
var stepTcs = stepCompletions[step.Name];
196+
// Create a TaskCompletionSource for each step
197+
var stepCompletions = new Dictionary<string, TaskCompletionSource>(steps.Count, StringComparer.Ordinal);
198+
foreach (var step in steps)
199+
{
200+
stepCompletions[step.Name] = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
201+
}
197202

198-
// Wait for all dependencies to complete (will throw if any dependency failed)
199-
if (step.DependsOnSteps.Count > 0)
203+
// Execute a step after its dependencies complete
204+
async Task ExecuteStepWithDependencies(PipelineStep step)
200205
{
206+
var stepTcs = stepCompletions[step.Name];
207+
208+
// Wait for all dependencies to complete (will throw if any dependency failed)
209+
if (step.DependsOnSteps.Count > 0)
210+
{
211+
try
212+
{
213+
var depTasks = step.DependsOnSteps.Select(depName => stepCompletions[depName].Task);
214+
await Task.WhenAll(depTasks).ConfigureAwait(false);
215+
}
216+
catch (Exception ex)
217+
{
218+
// Find all dependencies that failed
219+
var failedDeps = step.DependsOnSteps
220+
.Where(depName => stepCompletions[depName].Task.IsFaulted)
221+
.ToList();
222+
223+
var message = failedDeps.Count > 0
224+
? $"Step '{step.Name}' cannot run because {(failedDeps.Count == 1 ? "dependency" : "dependencies")} {string.Join(", ", failedDeps.Select(d => $"'{d}'"))} failed"
225+
: $"Step '{step.Name}' cannot run because a dependency failed";
226+
227+
// Wrap the dependency failure with context about this step
228+
var wrappedException = new InvalidOperationException(message, ex);
229+
stepTcs.TrySetException(wrappedException);
230+
return;
231+
}
232+
}
233+
201234
try
202235
{
203-
var depTasks = step.DependsOnSteps.Select(depName => stepCompletions[depName].Task);
204-
await Task.WhenAll(depTasks).ConfigureAwait(false);
236+
await ExecuteStepAsync(step, context).ConfigureAwait(false);
237+
238+
stepTcs.TrySetResult();
205239
}
206240
catch (Exception ex)
207241
{
208-
// Find all dependencies that failed
209-
var failedDeps = step.DependsOnSteps
210-
.Where(depName => stepCompletions[depName].Task.IsFaulted)
211-
.ToList();
242+
// Execution failure - mark as failed, cancel all other work, and re-throw
243+
stepTcs.TrySetException(ex);
212244

213-
var message = failedDeps.Count > 0
214-
? $"Step '{step.Name}' cannot run because {(failedDeps.Count == 1 ? "dependency" : "dependencies")} {string.Join(", ", failedDeps.Select(d => $"'{d}'"))} failed"
215-
: $"Step '{step.Name}' cannot run because a dependency failed";
245+
// Cancel all remaining work
246+
try
247+
{
248+
linkedCts.Cancel();
249+
}
250+
catch (ObjectDisposedException)
251+
{
252+
// Ignore cancellation errors
253+
}
216254

217-
// Wrap the dependency failure with context about this step
218-
var wrappedException = new InvalidOperationException(message, ex);
219-
stepTcs.TrySetException(wrappedException);
220-
return;
255+
throw;
221256
}
222257
}
223258

224-
try
259+
// Start all steps (they'll wait on their dependencies internally)
260+
var allStepTasks = new Task[steps.Count];
261+
for (var i = 0; i < steps.Count; i++)
225262
{
226-
await ExecuteStepAsync(step, context).ConfigureAwait(false);
227-
228-
stepTcs.TrySetResult();
263+
var step = steps[i];
264+
allStepTasks[i] = Task.Run(() => ExecuteStepWithDependencies(step));
229265
}
230-
catch (Exception ex)
266+
267+
// Wait for all steps to complete (or fail)
268+
try
231269
{
232-
// Execution failure - mark as failed and re-throw so it's counted
233-
stepTcs.TrySetException(ex);
234-
throw;
270+
await Task.WhenAll(allStepTasks).ConfigureAwait(false);
235271
}
236-
}
237-
238-
// Start all steps (they'll wait on their dependencies internally)
239-
var allStepTasks = new Task[steps.Count];
240-
for (var i = 0; i < steps.Count; i++)
241-
{
242-
var step = steps[i];
243-
allStepTasks[i] = Task.Run(() => ExecuteStepWithDependencies(step));
244-
}
245-
246-
// Wait for all steps to complete (or fail)
247-
try
248-
{
249-
await Task.WhenAll(allStepTasks).ConfigureAwait(false);
250-
}
251-
catch
252-
{
253-
// Collect all failed steps and their names
254-
var failures = allStepTasks
255-
.Where(t => t.IsFaulted)
256-
.Select(t => t.Exception!)
257-
.SelectMany(ae => ae.InnerExceptions)
258-
.ToList();
259-
260-
if (failures.Count > 1)
272+
catch
261273
{
262-
// Match failures to steps to get their names
263-
var failedStepNames = new List<string>();
264-
for (var i = 0; i < allStepTasks.Length; i++)
274+
// Collect all failed steps and their names
275+
var failures = allStepTasks
276+
.Where(t => t.IsFaulted)
277+
.Select(t => t.Exception!)
278+
.SelectMany(ae => ae.InnerExceptions)
279+
.ToList();
280+
281+
if (failures.Count > 1)
265282
{
266-
if (allStepTasks[i].IsFaulted)
283+
// Match failures to steps to get their names
284+
var failedStepNames = new List<string>();
285+
for (var i = 0; i < allStepTasks.Length; i++)
267286
{
268-
failedStepNames.Add(steps[i].Name);
287+
if (allStepTasks[i].IsFaulted)
288+
{
289+
failedStepNames.Add(steps[i].Name);
290+
}
269291
}
270-
}
271292

272-
var message = failedStepNames.Count > 0
273-
? $"Multiple pipeline steps failed: {string.Join(", ", failedStepNames.Distinct())}"
274-
: "Multiple pipeline steps failed.";
293+
var message = failedStepNames.Count > 0
294+
? $"Multiple pipeline steps failed: {string.Join(", ", failedStepNames.Distinct())}"
295+
: "Multiple pipeline steps failed.";
275296

276-
throw new AggregateException(message, failures);
277-
}
297+
throw new AggregateException(message, failures);
298+
}
278299

279-
// Single failure - just rethrow
280-
throw;
300+
// Single failure - just rethrow
301+
throw;
302+
}
303+
}
304+
finally
305+
{
306+
// Restore the original token
307+
context.CancellationToken = originalToken;
281308
}
282309
}
283310

src/Aspire.Hosting/Publishing/DeployingContext.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ public sealed class DeployingContext(
5555
public ILogger Logger { get; } = logger;
5656

5757
/// <summary>
58-
/// Gets the cancellation token for the deploying operation.
58+
/// Gets or sets the cancellation token for the deploying operation.
5959
/// </summary>
60-
public CancellationToken CancellationToken { get; } = cancellationToken;
60+
public CancellationToken CancellationToken { get; set; } = cancellationToken;
6161

6262
/// <summary>
6363
/// Gets the output path for deployment artifacts.

0 commit comments

Comments
 (0)