diff --git a/.gitattributes b/.gitattributes index b08dc0eeed9..28250ab7c5d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -57,3 +57,5 @@ *.dbproj text=auto *.sln text=auto + +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/agents/agentic-workflows.md b/.github/agents/agentic-workflows.md new file mode 100644 index 00000000000..82e1ed027c2 --- /dev/null +++ b/.github/agents/agentic-workflows.md @@ -0,0 +1,263 @@ +--- +name: Agentic Workflows +description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing. +disable-model-invocation: true +--- + +# GitHub Agentic Workflows Agent + +This agent helps you work with **GitHub Agentic Workflows (gh-aw)**, a CLI extension for creating AI-powered workflows in natural language using markdown files. + +## What This Agent Does + +This is a **dispatcher agent** that routes your request to the appropriate specialized prompt based on your task: + +- **Creating new workflows**: Routes to `create` prompt +- **Updating existing workflows**: Routes to `update` prompt +- **Debugging workflows**: Routes to `debug` prompt +- **Upgrading workflows**: Routes to `upgrade-agentic-workflows` prompt +- **Creating report-generating workflows**: Routes to `report` prompt — consult this whenever the workflow posts status updates, audits, analyses, or any structured output as issues, discussions, or comments +- **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt +- **Fixing Dependabot PRs**: Routes to `dependabot` prompt — use this when Dependabot opens PRs that modify generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`). Never merge those PRs directly; instead update the source `.md` files and rerun `gh aw compile --dependabot` to bundle all fixes +- **Analyzing test coverage**: Routes to `test-coverage` prompt — consult this whenever the workflow reads, analyzes, or reports on test coverage data from PRs or CI runs +- **Rendering ASCII charts in markdown**: Routes to `asciicharts` guide — consult this whenever the workflow needs compact charts that render reliably in GitHub issues, comments, or discussions +- **CLI commands and triggering workflows**: Routes to `cli-commands` guide — consult this whenever the user asks how to run, compile, debug, or manage workflows from the command line, or when they need the MCP tool equivalent of a `gh aw` command +- **Reducing token consumption / cost optimization**: Routes to `token-optimization` guide — consult this whenever the user asks how to reduce token usage, lower costs, speed up workflows, or measure the impact of prompt changes with experiments +- **Choosing workflow architectures and design patterns**: Routes to `patterns` guide — consult this whenever the user asks for strategy, architecture, operating models, or pattern selection for agentic workflows + +Workflows may optionally include: + +- **Project tracking / monitoring** (GitHub Projects updates, status reporting) +- **Orchestration / coordination** (one workflow assigning agents or dispatching and coordinating other workflows) + +## Files This Applies To + +- Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md` +- Workflow lock files: `.github/workflows/*.lock.yml` +- Shared components: `.github/workflows/shared/*.md` +- Configuration: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/github-agentic-workflows.md` + +## Problems This Solves + +- **Workflow Creation**: Design secure, validated agentic workflows with proper triggers, tools, and permissions +- **Workflow Debugging**: Analyze logs, identify missing tools, investigate failures, and fix configuration issues +- **Version Upgrades**: Migrate workflows to new gh-aw versions, apply codemods, fix breaking changes +- **Component Design**: Create reusable shared workflow components that wrap MCP servers + +## How to Use + +When you interact with this agent, it will: + +1. **Understand your intent** - Determine what kind of task you're trying to accomplish +2. **Route to the right prompt** - Load the specialized prompt file for your task +3. **Execute the task** - Follow the detailed instructions in the loaded prompt + +## Available Prompts + +> **Note**: The prompt and reference files listed below are located in the [`github/gh-aw`](https://github.com/github/gh-aw) repository and are **not available locally** in this repository. Load them from their public URLs. + +### Create New Workflow +**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet + +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/create-agentic-workflow.md` + +**Use cases**: +- "Create a workflow that triages issues" +- "I need a workflow to label pull requests" +- "Design a weekly research automation" + +### Update Existing Workflow +**Load when**: User wants to modify, improve, or refactor an existing workflow + +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/update-agentic-workflow.md` + +**Use cases**: +- "Add web-fetch tool to the issue-classifier workflow" +- "Update the PR reviewer to use discussions instead of issues" +- "Improve the prompt for the weekly-research workflow" + +### Debug Workflow +**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors + +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/debug-agentic-workflow.md` + +**Use cases**: +- "Why is this workflow failing?" +- "Analyze the logs for workflow X" +- "Investigate missing tool calls in run #12345" + +### Upgrade Agentic Workflows +**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations + +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/upgrade-agentic-workflows.md` + +**Use cases**: +- "Upgrade all workflows to the latest version" +- "Fix deprecated fields in workflows" +- "Apply breaking changes from the new release" + +### Create a Report-Generating Workflow +**Load when**: The workflow being created or updated produces reports — recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment + +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/report.md` + +**Use cases**: +- "Create a weekly CI health report" +- "Post a daily security audit to Discussions" +- "Add a status update comment to open PRs" + +### Create Shared Agentic Workflow +**Load when**: User wants to create a reusable workflow component or wrap an MCP server + +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/create-shared-agentic-workflow.md` + +**Use cases**: +- "Create a shared component for Notion integration" +- "Wrap the Slack MCP server as a reusable component" +- "Design a shared workflow for database queries" + +### Fix Dependabot PRs +**Load when**: User needs to close or fix open Dependabot PRs that update dependencies in generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`) + +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/dependabot.md` + +**Use cases**: +- "Fix the open Dependabot PRs for npm dependencies" +- "Bundle and close the Dependabot PRs for workflow dependencies" +- "Update @playwright/test to fix the Dependabot PR" + +### Analyze Test Coverage +**Load when**: The workflow reads, analyzes, or reports test coverage — whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy. + +**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/test-coverage.md` + +**Use cases**: +- "Create a workflow that comments coverage on PRs" +- "Analyze coverage trends over time" +- "Add a coverage gate that blocks PRs below a threshold" + +### CLI Commands Reference +**Load when**: The user asks how to run, compile, debug, or manage workflows from the command line; needs the MCP tool equivalent of a `gh aw` command; or is in a restricted environment (e.g., Copilot Cloud) without direct CLI access. + +**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/cli-commands.md` + +**Use cases**: +- "How do I trigger workflow X on the main branch?" +- "What's the MCP equivalent of `gh aw logs`?" +- "I'm in Copilot Cloud — how do I compile a workflow?" +- "Show me all available gh aw commands" + +### Token Consumption Optimization +**Load when**: The user asks how to reduce token usage, lower workflow costs, make a workflow faster or cheaper, or measure the impact of prompt or configuration changes. + +**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/token-optimization.md` + +**Use cases**: +- "How do I reduce the token cost of this workflow?" +- "My workflow is too expensive — how do I optimize it?" +- "How do I compare token usage between two runs?" +- "Should I use gh-proxy or the MCP server?" +- "How do I use sub-agents to reduce costs?" +- "How do I measure the impact of a prompt change?" + +### Workflow Pattern Selection +**Load when**: The user asks for architecture, strategy, operating model selection, or pattern recommendations for building agentic workflows. + +**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/patterns.md` + +**Use cases**: +- "Which pattern should I use for multi-repo rollout?" +- "How should I structure this workflow architecture?" +- "What pattern fits slash-command triage?" +- "Should this be DispatchOps or DailyOps?" + +## Instructions + +When a user interacts with you: + +1. **Identify the task type** from the user's request +2. **Load the appropriate prompt** from the URLs listed above +3. **Follow the loaded prompt's instructions** exactly +4. **If uncertain**, ask clarifying questions to determine the right prompt + +## Quick Reference + +```bash +# Initialize repository for agentic workflows +gh aw init + +# Generate the lock file for a workflow +gh aw compile [workflow-name] + +# Trigger a workflow on demand (preferred over gh workflow run) +gh aw run # interactive input collection +gh aw run --ref main # run on a specific branch + +# Debug workflow runs +gh aw logs [workflow-name] +gh aw audit + +# Upgrade workflows +gh aw fix --write +gh aw compile --validate +``` + +## Key Features of gh-aw + +- **Natural Language Workflows**: Write workflows in markdown with YAML frontmatter +- **AI Engine Support**: Copilot, Claude, Codex, or custom engines +- **MCP Server Integration**: Connect to Model Context Protocol servers for tools +- **Safe Outputs**: Structured communication between AI and GitHub API +- **Strict Mode**: Security-first validation and sandboxing +- **Shared Components**: Reusable workflow building blocks +- **Repo Memory**: Persistent git-backed storage for agents +- **Sandboxed Execution**: All workflows run in the Agent Workflow Firewall (AWF) sandbox, enabling full `bash` and `edit` tools by default + +## Important Notes + +- Always reference the instructions file at `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/github-agentic-workflows.md` for complete documentation +- Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud +- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions +- **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF +- Follow security best practices: minimal permissions, explicit network access, no template injection +- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/network.md` for the full list of valid ecosystem identifiers and domain patterns. +- **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself. +- **Triggering runs**: Always use `gh aw run ` to trigger a workflow on demand — not `gh workflow run .lock.yml`. `gh aw run` handles workflow resolution by short name, input parsing and validation, and correct run-tracking for agentic workflows. Use `--ref ` to run on a specific branch. +- **CLI commands reference**: For a complete guide on all `gh aw` commands and their MCP tool equivalents (for restricted environments), see `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/cli-commands.md` + +## Repository-Specific Requirements: Copilot PAT Pool + +**⚠️ MANDATORY**: Every agentic workflow in this repository **must** utilize the Copilot PAT Pool in the `copilot-pat-pool` environment as detailed in `.github/workflows/shared/pat_pool.README.md`. This mechanism selects a random Copilot PAT from a numbered pool of environment secrets to avoid rate-limiting from a single shared PAT, while preventing agentic workflows from having access to secrets from other environments. + +When **creating** or **updating** an agentic workflow, always include the following frontmatter that integrates the PAT pool, as found in as detailed in the **Usage** section of `.github/workflows/shared/pat_pool.README.md`. This should be used verbatim when first added. + +If other `engine` properties are customized for the workflow, that customization will need to be added into this same `engine` block and hand-editing can rearrange the PAT pool frontmatter and comment for ideal maintainability. + +## Repository-Specific Requirements: Schedule Seed + +When compiling a workflow that uses **fuzzy scheduling** (e.g., `schedule: weekly on monday around 9:00`), or when recompiling all workflows, always pass the `--schedule-seed` flag with this repository's identity: + +```bash +# Single workflow with fuzzy scheduling +gh aw compile .github/workflows/.md --schedule-seed dotnet/extensions + +# Recompile all workflows +gh aw compile --schedule-seed dotnet/extensions +``` + +The schedule seed ensures fuzzy schedule times are deterministic for this repository — the same seed always produces the same cron offsets, preventing unnecessary lock file churn across compilations. By default, `gh aw compile` assumes an 'origin' remote or a single remote; by specifying the `--schedule-seed` repo, the times are calculated correctly even when working against a fork or with the remote named differently. + +## Repository-Specific Requirements: Frontmatter Ordering + +When **creating** or **updating** an agentic workflow's frontmatter, follow this ordering convention. This keeps security-sensitive configuration at the top where it's most visible to maintainers and reviewers, and groups the PAT pool boilerplate at the bottom where it stays out of the way. + +### Frontmatter section ordering + +Arrange top-level frontmatter keys in this order within the `---` markers: + +1. **Descriptive** — `id`, `name`, `description`, etc. +2. **Security** — `permissions`, `safe_outputs`, `network`, `roles`, etc. +3. **Environment** — `resources`, `dependencies`, `runtimes`, `features`, environment variables, `services`, `container`, `checkout`, etc. +4. **Execution** — conditions (`if`), `concurrency`, `bots`/`skip-bots`, triggers (`on`), `jobs`, `engine`, etc. + +The PAT pool integration naturally falls into the **Execution** group at the bottom, including the `environment` property that defines the _execution environment_ for the agentic job. Keep the PAT pool content together as the last items in the frontmatter. diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json new file mode 100644 index 00000000000..11e3ee5382a --- /dev/null +++ b/.github/aw/actions-lock.json @@ -0,0 +1,14 @@ +{ + "entries": { + "actions/github-script@v9.0.0": { + "repo": "actions/github-script", + "version": "v9.0.0", + "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3" + }, + "github/gh-aw-actions/setup@v0.81.6": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.81.6", + "sha": "ba6380cc6e5be5d21677bebe04d52fb48e3abec7" + } + } +} diff --git a/.github/mcp.json b/.github/mcp.json new file mode 100644 index 00000000000..341a8b87588 --- /dev/null +++ b/.github/mcp.json @@ -0,0 +1,20 @@ +{ + "mcpServers": { + "github-agentic-workflows": { + "type": "local", + "command": "gh", + "args": [ + "aw", + "mcp-server" + ], + "tools": [ + "compile", + "audit", + "logs", + "inspect", + "status", + "audit-diff" + ] + } + } +} \ No newline at end of file diff --git a/.github/scripts/agent-framework-discover.cs b/.github/scripts/agent-framework-discover.cs new file mode 100644 index 00000000000..d6d7314814e --- /dev/null +++ b/.github/scripts/agent-framework-discover.cs @@ -0,0 +1,171 @@ +#!/usr/bin/env dotnet +#:package NuGet.Protocol@6.12.1 +#:property NuGetAudit=false +#:property NoWarn=IL2026;IL3050 +#:property JsonSerializerIsReflectionEnabledByDefault=true +#:property ManagePackageVersionsCentrally=false + +// Deterministic discovery of the newest coherent Microsoft Agent Framework release on the +// dnceng "dotnet-public" NuGet feed, driven entirely by the feed. Framework-general: it carries +// no consumer-specific (e.g. project-template) knowledge. It emits the release version, its +// date-stamp, and every Microsoft.Agents.AI* family package's version at that release (plus each +// package's newest-overall and newest-stable). Consumers -- currently the aiagent-webapi project +// template worker -- map this signal onto their own package subset and files. +// +// The family is enumerated from the feed (PackageSearchResource, which sends semVerLevel=2.0.0 so +// preview/alpha-only packages are visible) unioned with a known-id seed so a search hiccup can +// never silently drop a package. AutoCompleteResource is NOT used: AzDO feeds expose no +// SearchAutocompleteService, so IdStartsWith throws at runtime. Version selection uses NuGetVersion +// / VersionComparer for correct SemVer 2.0 ordering, so the feed's return order is irrelevant and +// the anomalous 0.0.1-preview.* entry (present across most of the family) can never win. +// +// When an output-file path is passed as the first argument the JSON array is written there (so the +// "dotnet run" build/restore output on stdout never contaminates the capture); otherwise it is +// written to stdout. Notices/warnings/errors go to stderr as GitHub Actions workflow commands. + +using System.Text.Json; +using NuGet.Common; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +const string Channel = "dotnet-public"; +const string FamilyPrefix = "Microsoft.Agents.AI"; +const string Anchor = "Microsoft.Agents.AI"; // always publishes a stable release +const string ExcludeId = "Microsoft.Agents.AI.ProjectTemplates"; // consumer package, not framework + +// Known family ids (determinism seed; search adds any new ones automatically). +string[] seed = +[ + "Microsoft.Agents.AI", "Microsoft.Agents.AI.Abstractions", "Microsoft.Agents.AI.OpenAI", + "Microsoft.Agents.AI.Workflows", "Microsoft.Agents.AI.Workflows.Generators", + "Microsoft.Agents.AI.DevUI", "Microsoft.Agents.AI.Hosting", + "Microsoft.Agents.AI.Foundry", "Microsoft.Agents.AI.Foundry.Hosting", + "Microsoft.Agents.AI.Hosting.OpenAI", +]; + +static string FeedIndex(string channel) => + $"https://pkgs.dev.azure.com/dnceng/public/_packaging/{channel}/nuget/v3/index.json"; +static void Notice(string m) => Console.Error.WriteLine($"::notice::{m}"); +static void Warn(string m) => Console.Error.WriteLine($"::warning::{m}"); +static void Fail(string m) +{ + Console.Error.WriteLine($"::error::{m}"); + Environment.Exit(1); +} + +var repo = Repository.Factory.GetCoreV3(FeedIndex(Channel)); +var cache = new SourceCacheContext { NoCache = true }; // see just-published builds immediately +var log = NullLogger.Instance; +var ct = CancellationToken.None; + +// ---- enumerate the family: search (semVerLevel=2.0.0, page-by-count) UNION seed -------------- +async Task> DiscoverFamily() +{ + var ids = new SortedSet(seed, StringComparer.OrdinalIgnoreCase); + try + { + var search = await repo.GetResourceAsync(); // NOT AutoCompleteResource (throws on AzDO) + var filter = new SearchFilter(includePrerelease: true); + int skip = 0, take = 100; + while (true) + { + var page = (await search.SearchAsync(FamilyPrefix, filter, skip, take, log, ct)).ToList(); + foreach (var r in page) + if (r.Identity.Id.StartsWith(FamilyPrefix, StringComparison.OrdinalIgnoreCase)) + ids.Add(r.Identity.Id); + if (page.Count < take) break; // AzDO totalHits is always "0"; page by returned count + skip += take; + } + } + catch (Exception ex) + { + Warn($"feed search unavailable ({ex.GetType().Name}: {ex.Message}); falling back to seed ids only."); + } + ids.RemoveWhere(id => id.Equals(ExcludeId, StringComparison.OrdinalIgnoreCase)); + return ids; +} + +// All published versions of a package on the feed, minus the anomalous 0.0.1-preview.* entry. +// Retries a few transient feed errors before treating the package as having no builds. +async Task> AllVersions(string id) +{ + Exception? last = null; + for (var attempt = 1; attempt <= 3; attempt++) + { + try + { + var res = await repo.GetResourceAsync(); + var vs = await res.GetAllVersionsAsync(id, cache, log, ct); + return vs.Where(v => v.Major > 0).ToList(); + } + catch (Exception ex) + { + last = ex; + if (attempt < 3) await Task.Delay(2000); + } + } + Warn($"could not read versions for '{id}' after 3 attempts: {last?.Message}"); + return []; +} + +var family = await DiscoverFamily(); +var versionsById = new Dictionary>(StringComparer.OrdinalIgnoreCase); +foreach (var id in family) + versionsById[id] = await AllVersions(id); + +// ---- release_version = newest STABLE of the anchor ------------------------------------------ +var anchorVersions = versionsById.TryGetValue(Anchor, out var av) ? av : []; +var release = anchorVersions.Where(v => !v.IsPrerelease).Max(); +if (release is null) + Fail($"No stable {Anchor} version found on {Channel}; aborting rather than emitting an empty signal."); + +bool AtRelease(NuGetVersion v) => + v.Major == release!.Major && v.Minor == release.Minor && v.Patch == release.Patch; + +static string Tier(NuGetVersion v) => + !v.IsPrerelease ? "stable" + : v.Release.StartsWith("alpha", StringComparison.OrdinalIgnoreCase) ? "alpha" + : v.Release.StartsWith("preview", StringComparison.OrdinalIgnoreCase) ? "preview" + : v.Release.Split('.')[0].ToLowerInvariant(); + +// ---- per-package: newest at the coherent release (any tier), newest overall, newest stable --- +var packages = new SortedDictionary(StringComparer.Ordinal); +string? releaseDate = null; +foreach (var (id, vs) in versionsById) +{ + var atRelease = vs.Where(AtRelease).Max(); + var newest = vs.Count > 0 ? vs.Max() : null; + var newestStable = vs.Where(v => !v.IsPrerelease).Max(); + + if (releaseDate is null && atRelease is { IsPrerelease: true }) + { + var parts = atRelease.Release.Split('.'); // e.g. "preview", "260703", "1" + if (parts.Length >= 2) releaseDate = parts[1]; + } + + packages[id] = new Dictionary + { + ["tier"] = atRelease is null ? null : Tier(atRelease), + ["at_release"] = atRelease?.ToNormalizedString(), + ["latest"] = newest?.ToNormalizedString(), + ["latest_stable"] = newestStable?.ToNormalizedString(), + }; +} + +Notice($"Agent Framework release {release} (date {releaseDate ?? "n/a"}) across {family.Count} packages on {Channel}."); + +var signal = new Dictionary +{ + ["source_feed"] = Channel, + ["release_version"] = release!.ToNormalizedString(), // e.g. "1.13.0" + ["release_date"] = releaseDate, // e.g. "260703" + ["packages"] = packages, +}; + +// Emit a single-element array so the orchestrator's matrix fans out over one target (the release). +var json = JsonSerializer.Serialize(new[] { signal }); +if (args.Length > 0) + File.WriteAllText(args[0], json); +else + Console.WriteLine(json); diff --git a/.github/scripts/agent-framework-worker-setup.sh b/.github/scripts/agent-framework-worker-setup.sh new file mode 100644 index 00000000000..920b0b25d99 --- /dev/null +++ b/.github/scripts/agent-framework-worker-setup.sh @@ -0,0 +1,494 @@ +#!/usr/bin/env bash +# Host-side setup for the "Agent Framework Template Worker" agentic workflow. +# +# Deterministically prepares everything the agent needs before it runs so the agent never has to +# discover, filter, de-duplicate, or build anything itself: +# 1. Resolves the target Agent Framework release (from the orchestrator's `target` input -- the +# framework signal emitted by agent-framework-discover.cs -- or, on a standalone dispatch, +# by running that discovery app here). +# 2. Maps the framework signal onto the aiagent-webapi template's package subset, reads the +# versions currently pinned in eng/packages/ProjectTemplates.props, and computes whether main +# needs a bump (the delta). +# 3. CI-validates the prospective bump: applies the desired versions to the props and the aligned +# version to the template package project, restores + builds + packs the template package +# through the repo's Arcade build, and runs the snapshot + execution tests -- recording whether +# they succeeded. The working tree is left clean; the exact validated files are saved for the +# agent to drop into place on the PR branch, so the agent never has to build. +# 4. Discovers the maintained draft PR and classifies it: OURS (carries our tracking marker), +# ADOPT (a human-bootstrapped automation+area-ai-templates PR on our branch with no marker +# yet), BLOCKED (a non-automation PR occupying our branch -- a human owns it), or NONE. +# 5. Reads the PR's recorded agent-framework-version and feedback-processed-through watermark, +# detects review activity newer than the watermark (author-agnostic wake gate; never reads +# comment bodies), and computes a recommended lifecycle action. +# +# Writes (under $AGENT_DIR, uploaded in the agent artifact): +# target.json resolved target + template versions + PR discovery + action +# ProjectTemplates.props.bumped the validated, fully-bumped props file the agent drops in +# build.log tail of the validation build (for the PR body / debugging) +# +# A transient API blip must never masquerade as "no PR / no work": discovery is retried and, when +# the maintained PR cannot be established with confidence, the action defaults to "produce". +# +# Environment: +# GITHUB_REPOSITORY owner/repo (set by Actions) +# GH_TOKEN token with pull-requests:read (the agent job's github.token) +# TARGET_JSON framework signal from the orchestrator (workflow_call); may be empty +# AGENT_DIR output dir (default /tmp/gh-aw/agent) +set -euo pipefail + +AGENT_DIR="${AGENT_DIR:-/tmp/gh-aw/agent}" +REPO="${GITHUB_REPOSITORY:-}" +TARGET_JSON="${TARGET_JSON:-}" +mkdir -p "$AGENT_DIR" +target_file="$AGENT_DIR/target.json" + +run_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + +DESIRED_BRANCH="update-agent-framework-template" +BASE_BRANCH="main" +# Labels the maintained PR must carry for the automation to own/act on it. +LABEL_A="automation" +LABEL_B="area-ai-templates" +# The state block delimiters -- a worker-internal concern. Whole yaml-comment lines +# `# ${STATE_MARKER}:state:begin` / `:state:end`. +STATE_MARKER="agent-framework-template" +PROPS="eng/packages/ProjectTemplates.props" +# The template NuGet package project, whose own version (Major/Minor/Patch, keeping the prerelease +# label) is aligned with the Agent Framework release on every bump. The shipped template itself is a +# .csproj-in whose ${PackageVersion:*} tokens resolve from PROPS at pack time, so a version bump +# only edits PROPS and this package project -- never the template content. +TEMPLATE_PKG_PROJ="src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.csproj" +# The template's snapshot + execution test project, run to confirm the template still works after the +# bump. It packs the template, runs `dotnet new aiagent-webapi`, then restores/builds the generated +# project -- so validation goes through the repo's Arcade build (build.sh), not a bare `dotnet build`. +TEST_PROJECT="test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/Microsoft.Agents.AI.ProjectTemplates.Tests.csproj" +# Upstream Agent Framework repo whose dotnet-* releases describe what changed between versions. +UPSTREAM_REPO="microsoft/agent-framework" +DISCOVER="$(dirname "$0")/agent-framework-discover.cs" + +# The template package itself ships on its own cadence and is excluded from the lockstep bump. +EXCLUDE_PKG="Microsoft.Agents.AI.ProjectTemplates" + +# ---- helpers ----------------------------------------------------------------------- +esc_re() { printf '%s' "$1" | sed 's/[.[\*^$/]/\\&/g'; } +get_pkg_version() { # file id + local esc; esc="$(esc_re "$2")" + sed -n -E "s|.*([^<]*).*|\1|p" "$1" | head -1; } +tmpl_pkg_version() { # file -> e.g. 1.3.0-preview + local maj min pat lbl + maj="$(tmpl_prop "$1" MajorVersion)"; min="$(tmpl_prop "$1" MinorVersion)" + pat="$(tmpl_prop "$1" PatchVersion)"; lbl="$(tmpl_prop "$1" PreReleaseVersionLabel)" + printf '%s.%s.%s%s' "$maj" "$min" "$pat" "${lbl:+-$lbl}" +} +set_tmpl_pkg_version() { # file major minor patch (label preserved) + sed -i -E "s|()[^<]*()|\1${2}\2|; \ + s|()[^<]*()|\1${3}\2|; \ + s|()[^<]*()|\1${4}\2|" "$1" +} + +# ---- 1. Resolve the target framework release --------------------------------------- +if [ -z "$TARGET_JSON" ]; then + # Standalone dispatch: run discovery here. It writes a single-element array; take element 0. + # ImportDirectoryBuild{Props,Targets}=false isolates the file-based app from the repo's + # Directory.Build.props (which injects analyzer PackageReferences that break the standalone build). + scratch="$(mktemp)" + if command -v dotnet >/dev/null 2>&1 && dotnet run "$DISCOVER" \ + --property:ImportDirectoryBuildProps=false --property:ImportDirectoryBuildTargets=false \ + -- "$scratch" >/dev/null 2>&1; then + TARGET_JSON="$(jq -c '.[0]' "$scratch" 2>/dev/null || true)" + fi + rm -f "$scratch" +fi +if [ -z "$TARGET_JSON" ] || [ "$(jq -r 'type' <<<"$TARGET_JSON" 2>/dev/null || echo null)" != "object" ]; then + echo "::error::No Agent Framework release signal available (empty or unparseable target). Refusing to continue." + exit 1 +fi + +release_version="$(jq -r '.release_version // ""' <<<"$TARGET_JSON")" +release_date="$(jq -r '.release_date // ""' <<<"$TARGET_JSON")" +source_feed="$(jq -r '.source_feed // "dotnet-public"' <<<"$TARGET_JSON")" +if [ -z "$release_version" ]; then + echo "::error::Framework signal is missing release_version. Refusing to continue." + exit 1 +fi + +# Split the release into Major.Minor.Patch (the anchor release is always a stable X.Y.Z). These +# drive the template NuGet package's own version; its prerelease label is never changed. +rel_mmp="${release_version%%-*}" +rel_major="$(cut -d. -f1 <<<"$rel_mmp")" +rel_minor="$(cut -d. -f2 <<<"$rel_mmp")" +rel_patch="$(cut -d. -f3 <<<"$rel_mmp")" +rel_patch="${rel_patch:-0}" + +# Discover every Microsoft.Agents.AI* package pinned in the props -- the Agent Framework dependencies +# that ship as a set and must be bumped in lockstep, each at its own tier -- excluding the template +# package itself. Data-driven so nothing is missed as the family grows. +mapfile -t AF_PKGS < <(grep -oE ' desired_versions{ id: version }. +desired_versions="{}" +for id in "${AF_PKGS[@]}"; do + ver="$(jq -r --arg s "$id" '.packages[$s].at_release // ""' <<<"$TARGET_JSON")" + if [ -z "$ver" ] || [ "$ver" = "null" ]; then + echo "::error::Framework signal has no at_release version for '${id}'." + exit 1 + fi + desired_versions="$(jq -c --arg id "$id" --arg v "$ver" '. + {($id): $v}' <<<"$desired_versions")" +done + +# Current anchor version + whether any AF package differs from its target. +current_version="$(get_pkg_version "$PROPS" "Microsoft.Agents.AI" || true)" +main_needs_bump="false" +for id in "${AF_PKGS[@]}"; do + cur="$(get_pkg_version "$PROPS" "$id" || true)" + want="$(jq -r --arg id "$id" '.[$id]' <<<"$desired_versions")" + [ "$cur" != "$want" ] && main_needs_bump="true" +done + +# Template NuGet package version: align Major/Minor/Patch with the release, keep the prerelease label. +template_pkg_old="" +template_pkg_new="" +if [ -f "$TEMPLATE_PKG_PROJ" ]; then + template_pkg_old="$(tmpl_pkg_version "$TEMPLATE_PKG_PROJ")" + tmpl_label="$(tmpl_prop "$TEMPLATE_PKG_PROJ" PreReleaseVersionLabel)" + template_pkg_new="${rel_major}.${rel_minor}.${rel_patch}${tmpl_label:+-$tmpl_label}" + [ "$template_pkg_old" != "$template_pkg_new" ] && main_needs_bump="true" + + # Produce the bumped template package project for the agent to apply (Major/Minor/Patch aligned to + # the release; prerelease label untouched). Pure version edit -- independent of the build below. + cp "$TEMPLATE_PKG_PROJ" "$AGENT_DIR/ProjectTemplates.csproj.orig" + cp "$TEMPLATE_PKG_PROJ" "$AGENT_DIR/ProjectTemplates.csproj.bumped" + set_tmpl_pkg_version "$AGENT_DIR/ProjectTemplates.csproj.bumped" "$rel_major" "$rel_minor" "$rel_patch" +fi + +# ---- 2. CI-validate the prospective bump (build the template package + run its tests) --------- +# Apply the bump to the working tree, then confirm the template still works through the repo's Arcade +# build: restore, build + pack the template package (producing the .nupkg the tests install), and run +# the snapshot + execution tests. The working tree is left clean afterward; the exact validated files +# are saved for the agent to drop onto the PR branch. If the build infrastructure cannot run (no repo +# SDK, feeds unreachable), validation is left inconclusive so the agent reports incomplete rather than +# shipping an unvalidated bump. +validated="false" +build_summary="not attempted" +tests_summary="not attempted" + +if [ -f "./build.sh" ]; then + cp "$PROPS" "$AGENT_DIR/ProjectTemplates.props.orig" + for id in "${AF_PKGS[@]}"; do + set_pkg_version "$PROPS" "$id" "$(jq -r --arg id "$id" '.[$id]' <<<"$desired_versions")" + done + cp "$PROPS" "$AGENT_DIR/ProjectTemplates.props.bumped" + # Apply the template package version bump too, so the validation build reflects the full change. + [ -f "$AGENT_DIR/ProjectTemplates.csproj.bumped" ] && cp "$AGENT_DIR/ProjectTemplates.csproj.bumped" "$TEMPLATE_PKG_PROJ" + + : >"$AGENT_DIR/build.log" + build_ok="false" + # Restore + build + pack the template package so the .nupkg the execution tests install exists. + # Single-dash flags so eng/build.sh's -projects handler fires (realpath + no root-.sln fallback). + if bash ./build.sh -ci -restore -build -pack \ + -projects "$PWD/src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.csproj" \ + -configuration Release >>"$AGENT_DIR/build.log" 2>&1; then + build_ok="true" + build_summary="template package restored, built, and packed against ${release_version}" + else + build_summary="template package restore/build/pack FAILED against ${release_version} (see build.log)" + echo "::warning::${build_summary}" + fi + tail -80 "$AGENT_DIR/build.log" >"$AGENT_DIR/build.tail.log" 2>/dev/null || true + + # Snapshot + execution tests through Arcade (how CI runs them; picks up the packed template + # nupkg from artifacts/packages). Only meaningful once the package built. + tests_ok="false" + if [ "$build_ok" = "true" ] && [ -f "$TEST_PROJECT" ]; then + if bash ./build.sh -ci -restore -build -integrationTest \ + -projects "$PWD/$TEST_PROJECT" \ + -configuration Release >"$AGENT_DIR/tests.log" 2>&1; then + tests_ok="true" + tests_summary="snapshot + execution tests passed against ${release_version}" + else + tests_summary="snapshot/execution tests FAILED against ${release_version} (see tests.log)" + echo "::warning::${tests_summary}" + fi + tail -80 "$AGENT_DIR/tests.log" >"$AGENT_DIR/tests.tail.log" 2>/dev/null || true + elif [ "$build_ok" = "true" ]; then + tests_summary="package built but tests could not run (no test project found); rely on PR CI" + fi + + [ "$build_ok" = "true" ] && [ "$tests_ok" = "true" ] && validated="true" + # Restore a clean working tree; the agent re-applies the bumped files on the PR branch. + cp "$AGENT_DIR/ProjectTemplates.props.orig" "$PROPS" + [ -f "$AGENT_DIR/ProjectTemplates.csproj.orig" ] && cp "$AGENT_DIR/ProjectTemplates.csproj.orig" "$TEMPLATE_PKG_PROJ" +else + # No repo build script available. Still save bumped copies so the agent can proceed, but flag it. + cp "$PROPS" "$AGENT_DIR/ProjectTemplates.props.bumped" + for id in "${AF_PKGS[@]}"; do + set_pkg_version "$AGENT_DIR/ProjectTemplates.props.bumped" "$id" "$(jq -r --arg id "$id" '.[$id]' <<<"$desired_versions")" + done + build_summary="repo build.sh not found on host; bump not CI-validated" + tests_summary="repo build.sh not found on host; tests not run" +fi + +# ---- helpers for PR discovery / tracking block ------------------------------------- +fetch_pr_body() { + local pr_number="$1" battempt + body="" body_ok="false" + for battempt in 1 2 3; do + if body="$(gh pr view "$pr_number" --repo "$REPO" --json body -q '.body' 2>/dev/null)"; then + body_ok="true"; return 0 + fi + [ "$battempt" -lt 3 ] && sleep 2 + done + return 0 +} +tracking_value() { + local name="$1" + sed -n "s/^[[:space:]]*[-*+>]*[[:space:]]*${name}:[[:space:]]*//p" | + head -1 | tr -d '"'\''\r' | sed 's/[[:space:]]*#.*$//; s/[[:space:]]*$//' +} +tracking_block() { + awk -v b="# ${STATE_MARKER}:state:begin" -v e="# ${STATE_MARKER}:state:end" ' + { t=$0; sub(/\r$/,"",t); gsub(/^[[:space:]]+|[[:space:]]+$/,"",t) } + t == b { inb=1; buf=$0 ORS; next } + inb { buf=buf $0 ORS; if (t == e) { inb=0; last=buf } } + END { if (inb) last=buf; printf "%s", last }' +} +body_has_state_marker() { + printf '%s' "$1" | jq -Rrs --arg m "# ${STATE_MARKER}:state:begin" \ + 'split("\n") | any(gsub("^\\s+|\\s+$";"") == $m)' +} + +write_target() { # $1=pr $2=pr_state $3=pr_is_draft $4=pr_recorded_version $5=classification $6=action + jq -cn \ + --arg source_feed "$source_feed" --arg release_version "$release_version" \ + --arg release_date "$release_date" --arg current_version "${current_version:-}" \ + --argjson main_needs_bump "$main_needs_bump" \ + --argjson desired_versions "$desired_versions" \ + --argjson validated "$validated" --arg build_summary "$build_summary" \ + --arg desired_branch "$DESIRED_BRANCH" --arg base_branch "$BASE_BRANCH" \ + --arg pr_branch "${PR_BRANCH:-}" \ + --arg pr "$1" --arg pr_state "$2" --argjson pr_is_draft "${3:-false}" \ + --arg pr_recorded_version "$4" --arg classification "$5" --arg action "$6" \ + --argjson has_new_feedback "${has_new_feedback:-false}" \ + --arg watermark "${watermark:-}" --arg run_started_at "$run_started_at" \ + --arg props_path "$PROPS" \ + --arg template_pkg_proj "$TEMPLATE_PKG_PROJ" \ + --arg template_pkg_old "${template_pkg_old:-}" --arg template_pkg_new "${template_pkg_new:-}" \ + --arg tests_summary "${tests_summary:-}" --arg test_project "$TEST_PROJECT" \ + --arg upstream_repo "$UPSTREAM_REPO" \ + --arg from_version "${from_version:-}" --argjson af_change_count "${af_change_count:-0}" \ + --arg af_changes_path "${af_changes_rel:-}" \ + '{source_feed:$source_feed, release_version:$release_version, release_date:$release_date, + current_version:$current_version, main_needs_bump:$main_needs_bump, + desired_versions:$desired_versions, validated:$validated, build_summary:$build_summary, + template_pkg_proj:$template_pkg_proj, template_pkg_old:$template_pkg_old, + template_pkg_new:$template_pkg_new, + tests_summary:$tests_summary, test_project:$test_project, + upstream_repo:$upstream_repo, from_version:$from_version, af_change_count:$af_change_count, + af_changes_path:$af_changes_path, + desired_branch:$desired_branch, base_branch:$base_branch, pr_branch:$pr_branch, + pr:$pr, pr_state:$pr_state, pr_is_draft:$pr_is_draft, + pr_recorded_version:$pr_recorded_version, classification:$classification, action:$action, + has_new_feedback:$has_new_feedback, watermark:$watermark, run_started_at:$run_started_at, + props_path:$props_path}' >"$target_file" +} + +step_summary() { # $1=classification $2=action $3=pr $4=pr_recorded_version $5=new_feedback + { + echo "## Agent Framework Template worker -- setup decision" + echo "" + echo "| field | value |" + echo "|---|---|" + echo "| source feed | \`${source_feed}\` |" + echo "| release version | \`${release_version}\` |" + echo "| current version (main) | \`${current_version:-}\` |" + echo "| main needs bump | ${main_needs_bump} |" + echo "| template package version | \`${template_pkg_old:-}\` -> \`${template_pkg_new:-}\` |" + echo "| bump CI-validated | ${validated} |" + echo "| build | ${build_summary} |" + echo "| tests | ${tests_summary:-} |" + echo "| Agent Framework changes to review | ${af_change_count:-0} release(s) ${from_version:+from ${from_version} }to ${release_version} |" + echo "| maintained PR | ${3:-} |" + echo "| maintained PR branch | \`${PR_BRANCH:-}\` |" + echo "| PR recorded version | \`${4:-}\` |" + echo "| classification | **${1}** |" + echo "| recommended action | **${2}** |" + echo "| new review activity | ${5} |" + [ "${feedback_query_failed:-false}" = "true" ] && echo "| review-activity query | **failed** -- wake gate opened |" + } >>"${GITHUB_STEP_SUMMARY:-/dev/null}" 2>/dev/null || true +} + +watermark="" +has_new_feedback="false" +feedback_query_failed="false" +from_version="" +af_change_count=0 +af_changes_rel="" + +# Gather the Agent Framework dotnet-* releases published strictly after $1 (from) and up to and +# including $2 (to), writing their notes to af-changes.md for the agent to evaluate consumption +# against. Version-range selection uses `sort -V` for SemVer-correct ordering. Best-effort: a feed +# hiccup leaves an empty change set rather than failing the run. +gather_af_changes() { # $1=from_version (may be empty) $2=to_version + local from="$1" to="$2" out="$AGENT_DIR/af-changes.md" rel_json="" tag ver v_in_range + af_changes_rel="af-changes.md" + : >"$out" + rel_json="$(gh api "repos/${UPSTREAM_REPO}/releases" --paginate 2>/dev/null \ + | jq -c '[.[] | select((.tag_name // "") | startswith("dotnet-")) + | {tag:.tag_name, name:.name, date:.published_at, body:.body}]' 2>/dev/null || true)" + if [ -z "$rel_json" ] || [ "$rel_json" = "null" ]; then + echo "_No Agent Framework release notes could be retrieved from ${UPSTREAM_REPO}._" >>"$out" + af_change_count=0 + return 0 + fi + + { + echo "# Agent Framework changes to review" + echo "" + echo "Releases from ${UPSTREAM_REPO} published ${from:+after \`${from}\` and }up to \`${to}\`." + echo "Evaluate each for API changes, deprecations, and newly prescribed patterns, then update" + echo "the template and any other Agent Framework consumption to match." + echo "" + } >>"$out" + + local count=0 i len + len="$(jq 'length' <<<"$rel_json")" + # Releases are newest-first from the API; emit oldest-first for readability. + for (( i=len-1; i>=0; i-- )); do + tag="$(jq -r ".[$i].tag" <<<"$rel_json")" + ver="${tag#dotnet-}" + # Keep versions v with: from < v <= to (skip from itself; include to). + if [ -n "$from" ]; then + [ "$ver" = "$from" ] && continue + [ "$(printf '%s\n%s\n' "$from" "$ver" | sort -V | tail -1)" = "$ver" ] || continue + fi + [ "$(printf '%s\n%s\n' "$ver" "$to" | sort -V | tail -1)" = "$to" ] || continue + count=$((count + 1)) + { + echo "## ${tag} ($(jq -r ".[$i].date" <<<"$rel_json"))" + echo "" + jq -r ".[$i].body // \"(no release notes)\"" <<<"$rel_json" + echo "" + } >>"$out" + done + af_change_count=$count + [ "$count" -eq 0 ] && echo "_No Agent Framework releases fall in this range._" >>"$out" + return 0 +} + +if [ -z "$REPO" ] || [ -z "${GH_TOKEN:-}" ]; then + echo "GITHUB_REPOSITORY or GH_TOKEN unset; cannot discover PR -- defaulting to produce (fresh) if a bump is needed" + act="noop"; [ "$main_needs_bump" = "true" ] && act="produce" + write_target "" "" false "" "none" "$act" + step_summary "none" "$act" "" "" "false" + exit 0 +fi + +# ---- 4. Discover the maintained PR and classify it --------------------------------- +BASE_OWNER="${REPO%%/*}" +pr="" PR_BRANCH="" pr_is_draft="false" classification="none" body_ok="false" +for attempt in 1 2 3; do + rows="$(gh api --method GET "repos/${REPO}/pulls" \ + -f state=open -f head="${BASE_OWNER}:${DESIRED_BRANCH}" -f base="${BASE_BRANCH}" -f per_page=100 2>/dev/null \ + | jq -c --arg owner "$BASE_OWNER" --arg branch "$DESIRED_BRANCH" --arg base "$BASE_BRANCH" \ + '[.[] | select((.head.repo.owner.login // "") == $owner and .head.ref == $branch and (.base.ref // "") == $base) + | {number, headRefName:.head.ref, isDraft:(.draft // false), labels:(.labels // []), updatedAt:.updated_at}] + | sort_by(.updatedAt) | reverse' 2>/dev/null || true)" + [ -n "$rows" ] && break + [ "$attempt" -lt 3 ] && sleep 2 +done +rows="${rows:-[]}" + +pr="$(jq -r '(.[0].number) // empty' <<<"$rows" 2>/dev/null || true)" +if [ -n "$pr" ]; then + pr_is_draft="$(jq -r '.[0].isDraft // false' <<<"$rows")" + PR_BRANCH="$(jq -r '.[0].headRefName // ""' <<<"$rows")" + has_labels="$(jq -r --arg a "$LABEL_A" --arg b "$LABEL_B" '[.[0].labels[]?.name] | (index($a) != null) and (index($b) != null)' <<<"$rows")" + fetch_pr_body "$pr" + has_marker="$(body_has_state_marker "$body")" + if [ "$body_ok" != "true" ] && [ "$has_labels" = "true" ]; then + classification="ours" + elif [ "$has_labels" = "true" ] && [ "$has_marker" = "true" ]; then + classification="ours" + elif [ "$has_labels" = "true" ] && [ "$has_marker" != "true" ] && [ "$pr_is_draft" = "true" ]; then + # Human-bootstrapped: automation labels + draft + no tracking marker yet -> adopt it. + # Both the label and draft gates must pass; a labeled but non-draft (ready) PR is left to + # the human (falls through to blocked). + classification="adopt" + else + classification="blocked" + fi +fi + +# ---- 5. Read recorded state + wake gate + action ----------------------------------- +pr_recorded_version="" +if [ -n "$pr" ] && [ "$classification" != "blocked" ] && [ "$body_ok" = "true" ]; then + watermark="$(printf '%s\n' "$body" | tracking_block | tracking_value "feedback-processed-through")" + pr_recorded_version="$(printf '%s\n' "$body" | tracking_block | tracking_value "agent-framework-version")" +fi + +# The evaluation baseline is what the repo already integrates: the maintained PR's recorded version +# when we own one, otherwise main's current version. Gather the Agent Framework changes from there to +# the target release for the agent to review consumption against. +from_version="$current_version" +{ [ "$classification" = "ours" ] || [ "$classification" = "adopt" ]; } && [ -n "$pr_recorded_version" ] && from_version="$pr_recorded_version" +gather_af_changes "$from_version" "$release_version" + +fetch_activity() { + local endpoint="$1" jqexpr="$2" fattempt scratch + scratch="$(mktemp)" + for fattempt in 1 2 3; do + if gh api "$endpoint" --paginate -q "$jqexpr" >"$scratch" 2>/dev/null; then + cat "$scratch" >>"$feedback_times"; rm -f "$scratch"; return 0 + fi + [ "$fattempt" -lt 3 ] && sleep 2 + done + rm -f "$scratch"; return 1 +} +if [ -n "$pr" ] && [ "$classification" != "blocked" ]; then + feedback_times="$(mktemp)" + fetch_activity "repos/${REPO}/issues/${pr}/comments" '.[] | select(.user.type != "Bot") | .created_at' || feedback_query_failed="true" + fetch_activity "repos/${REPO}/pulls/${pr}/comments" '.[] | select(.user.type != "Bot") | .created_at' || feedback_query_failed="true" + fetch_activity "repos/${REPO}/pulls/${pr}/reviews" '.[] | select(.user.type != "Bot") | .submitted_at' || feedback_query_failed="true" + if [ "$feedback_query_failed" = "true" ]; then + echo "::warning::review-activity query failed for PR #${pr} after retries; opening the wake gate" + has_new_feedback="true" + else + new_count="$(awk -v since="$watermark" 'NF && (since=="" || $0 > since)' "$feedback_times" | grep -c . || true)" + [ "${new_count:-0}" -gt 0 ] && has_new_feedback="true" + fi + rm -f "$feedback_times" +fi + +# Recommended lifecycle action. The agent's Step 3 remains authoritative and refines edge cases. +action="produce" +case "$classification" in + blocked) + action="noop" ;; + none) + # Fresh only if main actually needs the bump; otherwise the template is already current. + [ "$main_needs_bump" = "true" ] && action="produce" || action="noop" ;; + ours|adopt) + # Caught up (PR already at this release) with no new feedback -> no-op; else produce. + if [ "$classification" = "ours" ] && [ "$pr_recorded_version" = "$release_version" ] && [ "$has_new_feedback" != "true" ]; then + action="noop" + else + action="produce" + fi ;; +esac + +write_target "$pr" "open" "$pr_is_draft" "$pr_recorded_version" "$classification" "$action" +step_summary "$classification" "$action" "$pr" "$pr_recorded_version" "$has_new_feedback" +echo "Setup complete: classification=${classification} action=${action} release=${release_version} validated=${validated}" diff --git a/.github/scripts/meai-otel-genai-orchestrator-discover.sh b/.github/scripts/meai-otel-genai-orchestrator-discover.sh new file mode 100644 index 00000000000..90be10b8ea1 --- /dev/null +++ b/.github/scripts/meai-otel-genai-orchestrator-discover.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# meai-otel-genai-orchestrator-discover.sh +# +# Deterministic discovery for the MEAI Otel GenAI Orchestrator. The gen-ai integration is +# a SINGLE evergreen work unit -- one draft PR that tracks the upstream +# open-telemetry/semantic-conventions-genai repository into Microsoft.Extensions.AI -- +# so discovery emits AT MOST ONE target. Its whole job is to resolve the upstream ref +# to scan into a concrete commit SHA (so the worker never guesses) and hand the +# worker a fully specified target. All produce/no-op/adoption decisions are made +# later, in the worker setup, from the resolved SHA. +# +# A transient API blip must never masquerade as "no upstream" (which would silently +# drop the target and skip the run), so the resolve is retried; an unresolvable ref is +# a hard error (exit 1), never an empty target set. +# +# Requires `gh` (GH_TOKEN) and `jq`. Emits a JSON array (0 or 1 target) to stdout. +# +# Environment: +# UPSTREAM_REPO optional upstream repo (default open-telemetry/semantic-conventions-genai) +# UPSTREAM_REF optional ref to scan (branch/tag/SHA); empty = default-branch HEAD +set -euo pipefail + +UPSTREAM_REPO="${UPSTREAM_REPO:-open-telemetry/semantic-conventions-genai}" +UPSTREAM_REF="${UPSTREAM_REF:-}" + +# resolve_sha -> concrete commit SHA (retried); empty on authoritative miss. +resolve_sha() { + local ref="$1" sha="" attempt + for attempt in 1 2 3; do + if [ -z "$ref" ]; then + # Default-branch HEAD. + local default_branch + default_branch="$(gh api "repos/${UPSTREAM_REPO}" -q '.default_branch' 2>/dev/null || true)" + [ -n "$default_branch" ] && sha="$(gh api "repos/${UPSTREAM_REPO}/commits/${default_branch}" -q '.sha' 2>/dev/null || true)" + else + sha="$(gh api "repos/${UPSTREAM_REPO}/commits/${ref}" -q '.sha' 2>/dev/null || true)" + fi + [ -n "$sha" ] && { printf '%s' "$sha"; return 0; } + echo "::warning::could not resolve '${ref:-}' in ${UPSTREAM_REPO} (attempt ${attempt}/3)" >&2 + [ "$attempt" -lt 3 ] && sleep 2 + done + return 0 +} + +upstream_sha="$(resolve_sha "$UPSTREAM_REF")" +if [ -z "$upstream_sha" ]; then + echo "::error::Could not resolve upstream ref '${UPSTREAM_REF:-}' in ${UPSTREAM_REPO} after retries. Aborting instead of emitting an empty target set (a transient blip must never silently skip the run)." >&2 + exit 1 +fi + +echo "::notice::${UPSTREAM_REPO} scan ref = ${UPSTREAM_REF:-} -> ${upstream_sha}" >&2 + +# The gen-ai integration uses a single evergreen branch; the tracked version and scan SHA +# live in the worker's PR body state block, never in the branch (so the one PR refreshes +# forever as upstream advances). The state block's marker is a worker-internal detail -- +# the orchestrator neither emits nor knows it. +target="$(jq -cn \ + --arg upstream_repo "$UPSTREAM_REPO" \ + --arg upstream_ref "$UPSTREAM_REF" \ + --arg upstream_sha "$upstream_sha" \ + --arg desired_branch "update-otel-genai-to-latest" \ + '{upstream_repo:$upstream_repo, upstream_ref:$upstream_ref, upstream_sha:$upstream_sha, + desired_branch:$desired_branch}')" + +jq -cn --argjson t "$target" '[$t]' diff --git a/.github/scripts/meai-otel-genai-worker-setup.sh b/.github/scripts/meai-otel-genai-worker-setup.sh new file mode 100644 index 00000000000..c2c7abea33f --- /dev/null +++ b/.github/scripts/meai-otel-genai-worker-setup.sh @@ -0,0 +1,434 @@ +#!/usr/bin/env bash +# Host-side setup for the "MEAI: Otel GenAI Worker" agentic workflow. +# +# Deterministically prepares everything the agent needs before it runs so the agent +# never has to discover, filter, or de-duplicate state itself: +# 1. Resolves the upstream scan target (from the orchestrator's `target` input, or from a +# standalone dispatch's upstream_ref / default HEAD), retrying transient failures and +# hard-failing when the ref cannot be resolved to a commit SHA at all. +# 2. Discovers the maintained draft PR and classifies it: OURS (carries our tracking +# marker), ADOPT (a human-bootstrapped automation+area-ai PR on our branch with no +# marker yet), BLOCKED (a non-automation PR occupying our branch -- a human owns it), +# or NONE. +# 3. Reads the PR's recorded upstream-scan-ref, upstream-release, and +# feedback-processed-through watermark, and computes a recommended lifecycle action. +# 4. Detects whether any PR review activity is newer than the recorded watermark. This +# is an author-agnostic wake gate only -- it never reads comment bodies. The agent +# collects and evaluates the actual review feedback itself under gh-aw integrity +# filtering (min-integrity + integrity-reactions), the single source of truth for +# which feedback is trusted and actionable. +# +# Writes (under $AGENT_DIR, uploaded in the agent artifact so downstream jobs read them): +# target.json resolved scan target + PR discovery + recommended action, plus the +# feedback-processed-through watermark and this run's run_started_at (which +# the agent stamps back as the new watermark) +# +# The recommended action lets a caught-up run early-noop before the expensive build. The +# agent's Step 3 remains authoritative and refines edge cases (e.g. release-gate mark-ready). +# +# A transient API blip must never masquerade as "no PR / no work"; discovery is retried +# and, when the maintained PR cannot be established with confidence, the action defaults +# to "produce" (never a silent skip). +# +# Environment: +# GITHUB_REPOSITORY owner/repo (set by Actions) +# GH_TOKEN token with pull-requests:read (the agent job's github.token) +# TARGET_JSON target object from the orchestrator (workflow_call); may be empty +# UPSTREAM_REF standalone-dispatch scan ref override (used only when TARGET_JSON empty) +# AGENT_DIR output dir (default /tmp/gh-aw/agent) +set -euo pipefail + +AGENT_DIR="${AGENT_DIR:-/tmp/gh-aw/agent}" +REPO="${GITHUB_REPOSITORY:-}" +TARGET_JSON="${TARGET_JSON:-}" +UPSTREAM_REF="${UPSTREAM_REF:-}" +mkdir -p "$AGENT_DIR" +target_file="$AGENT_DIR/target.json" + +run_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + +# ---- 1. Resolve the upstream scan target ------------------------------------------- +UPSTREAM_REPO="open-telemetry/semantic-conventions-genai" +DESIRED_BRANCH="update-otel-genai-to-latest" +# The maintained PR always targets main (see the worker's create-pull-request +# `base-branch: main` safe output). PR discovery restricts to this base so a tracking-branch +# PR opened against some other base is never mistaken for the maintained PR. +BASE_BRANCH="main" +# The state block's delimiters are hard-coded to this workflow's name -- a worker-internal +# concern the orchestrator neither knows nor supplies. The block is delimited by whole yaml-comment +# lines `# ${STATE_MARKER}:state:begin` / `:state:end`. +STATE_MARKER="meai-otel-genai-worker" +upstream_sha="" +if [ -n "$TARGET_JSON" ]; then + UPSTREAM_REPO="$(jq -r '.upstream_repo // "open-telemetry/semantic-conventions-genai"' <<<"$TARGET_JSON")" + DESIRED_BRANCH="$(jq -r '.desired_branch // "update-otel-genai-to-latest"' <<<"$TARGET_JSON")" + UPSTREAM_REF="$(jq -r '.upstream_ref // ""' <<<"$TARGET_JSON")" + upstream_sha="$(jq -r '.upstream_sha // ""' <<<"$TARGET_JSON")" +fi + +resolve_sha() { + local ref="$1" sha="" attempt db + for attempt in 1 2 3; do + if [ -z "$ref" ]; then + db="$(gh api "repos/${UPSTREAM_REPO}" -q '.default_branch' 2>/dev/null || true)" + [ -n "$db" ] && sha="$(gh api "repos/${UPSTREAM_REPO}/commits/${db}" -q '.sha' 2>/dev/null || true)" + else + sha="$(gh api "repos/${UPSTREAM_REPO}/commits/${ref}" -q '.sha' 2>/dev/null || true)" + fi + [ -n "$sha" ] && { printf '%s' "$sha"; return 0; } + [ "$attempt" -lt 3 ] && sleep 2 + done + return 0 +} + +resolve_release_commit() { + local tag="$1" ref_json="" object_type="" object_sha="" tag_json="" + [ -z "$tag" ] && return 0 + + ref_json="$(gh api "repos/${UPSTREAM_REPO}/git/ref/tags/${tag}" 2>/dev/null || true)" + object_type="$(jq -r '.object.type // empty' <<<"$ref_json" 2>/dev/null || true)" + object_sha="$(jq -r '.object.sha // empty' <<<"$ref_json" 2>/dev/null || true)" + case "$object_type" in + commit) + [ -n "$object_sha" ] && { printf '%s' "$object_sha"; return 0; } ;; + tag) + tag_json="$(gh api "repos/${UPSTREAM_REPO}/git/tags/${object_sha}" 2>/dev/null || true)" + object_type="$(jq -r '.object.type // empty' <<<"$tag_json" 2>/dev/null || true)" + object_sha="$(jq -r '.object.sha // empty' <<<"$tag_json" 2>/dev/null || true)" + [ "$object_type" = "commit" ] && [ -n "$object_sha" ] && { printf '%s' "$object_sha"; return 0; } ;; + esac + + # Fallback for lightweight tags or unusual ref layouts; the commits endpoint + # resolves branch/tag names to the commit GitHub would show for that ref. + object_sha="$(gh api "repos/${UPSTREAM_REPO}/commits/${tag}" -q '.sha' 2>/dev/null || true)" + [ -n "$object_sha" ] && printf '%s' "$object_sha" + # Always succeed: the result is communicated via stdout (the caller tests for a + # non-empty commit), so an unresolved tag must degrade to low confidence -- never + # abort the run under `set -e` when this test is the function's final command. + return 0 +} + +if [ -z "$upstream_sha" ] && [ -n "${GH_TOKEN:-}" ]; then + upstream_sha="$(resolve_sha "$UPSTREAM_REF")" + if [ -z "$upstream_sha" ]; then + echo "::error::Could not resolve upstream ref '${UPSTREAM_REF:-}' in ${UPSTREAM_REPO} to a commit SHA after retries. Refusing to continue with an unresolved scan target." + exit 1 + fi +fi + +# Best-effort latest published gen-ai release tag (normally none -- Development stability). +upstream_release="none" +upstream_release_commit="" +release_matches_scan="false" +release_matches_pr="false" +release_ready="false" +release_detection_confidence="none" +release_detection_signal="No GitHub release is published for the upstream repo." +if [ -n "${GH_TOKEN:-}" ]; then + rel="$(gh api "repos/${UPSTREAM_REPO}/releases/latest" -q '.tag_name' 2>/dev/null || true)" + if [ -n "$rel" ]; then + upstream_release="$rel" + upstream_release_commit="$(resolve_release_commit "$rel")" + if [ -n "$upstream_release_commit" ]; then + release_detection_confidence="high" + release_detection_signal="GitHub latest release ${rel} resolves through refs/tags to commit ${upstream_release_commit}." + [ -n "$upstream_sha" ] && [ "$upstream_release_commit" = "$upstream_sha" ] && release_matches_scan="true" + else + release_detection_confidence="low" + release_detection_signal="GitHub latest release ${rel} exists, but its tag commit could not be resolved; release-gate readiness is intentionally withheld." + fi + fi +fi + +write_target() { + # $1=pr $2=pr_state $3=pr_is_draft $4=pr_recorded_sha $5=pr_recorded_release + # $6=classification(ours|adopt|blocked|none) $7=action + jq -cn \ + --arg upstream_repo "$UPSTREAM_REPO" --arg upstream_ref "$UPSTREAM_REF" \ + --arg upstream_sha "$upstream_sha" --arg upstream_release "$upstream_release" \ + --arg upstream_release_commit "$upstream_release_commit" \ + --argjson release_matches_scan "$release_matches_scan" \ + --argjson release_matches_pr "$release_matches_pr" \ + --argjson release_ready "$release_ready" \ + --arg release_detection_confidence "$release_detection_confidence" \ + --arg release_detection_signal "$release_detection_signal" \ + --arg desired_branch "$DESIRED_BRANCH" --arg pr_branch "${PR_BRANCH:-}" \ + --arg pr "$1" --arg pr_state "$2" --argjson pr_is_draft "${3:-false}" \ + --arg pr_recorded_sha "$4" --arg pr_recorded_release "$5" \ + --arg classification "$6" --arg action "$7" \ + --argjson has_new_feedback "${has_new_feedback:-false}" \ + --arg watermark "${watermark:-}" --arg run_started_at "$run_started_at" \ + '{upstream_repo:$upstream_repo, upstream_ref:$upstream_ref, upstream_sha:$upstream_sha, + upstream_release:$upstream_release, upstream_release_commit:$upstream_release_commit, + release_matches_scan:$release_matches_scan, release_matches_pr:$release_matches_pr, + release_ready:$release_ready, + release_detection_confidence:$release_detection_confidence, + release_detection_signal:$release_detection_signal, + desired_branch:$desired_branch, pr_branch:$pr_branch, + pr:$pr, pr_state:$pr_state, pr_is_draft:$pr_is_draft, pr_recorded_sha:$pr_recorded_sha, + pr_recorded_release:$pr_recorded_release, classification:$classification, action:$action, + has_new_feedback:$has_new_feedback, + watermark:$watermark, run_started_at:$run_started_at}' >"$target_file" +} + +step_summary() { + # $1=classification $2=action $3=pr $4=pr_recorded_sha $5=new_feedback + { + echo "## Otel GenAI worker -- setup decision" + echo "" + echo "| field | value |" + echo "|---|---|" + echo "| upstream_repo | \`${UPSTREAM_REPO}\` |" + echo "| scan ref | \`${UPSTREAM_REF:-}\` |" + echo "| upstream SHA | \`${upstream_sha:-}\` |" + echo "| upstream release | \`${upstream_release}\` |" + echo "| upstream release commit | \`${upstream_release_commit:-}\` |" + echo "| release detection confidence | \`${release_detection_confidence}\` |" + echo "| release matches scanned SHA | ${release_matches_scan} |" + echo "| release matches PR SHA | ${release_matches_pr} |" + echo "| release ready | ${release_ready} |" + echo "| maintained PR | ${3:-} |" + echo "| maintained PR branch | \`${PR_BRANCH:-}\` |" + echo "| PR recorded SHA | \`${4:-}\` |" + echo "| classification | **${1}** |" + echo "| recommended action | **${2}** |" + echo "| new review activity | ${5} |" + [ "${feedback_query_failed:-false}" = "true" ] && echo "| review-activity query | **failed** -- wake gate opened, run will produce |" + [ "${tracking_valid_count:-0}" -gt 1 ] && echo "| valid tracking PR candidates | ${tracking_valid_count} (newest selected) |" + } >>"${GITHUB_STEP_SUMMARY:-/dev/null}" 2>/dev/null || true +} + +fetch_pr_body() { + local pr_number="$1" battempt + body="" body_ok="false" + for battempt in 1 2 3; do + if body="$(gh pr view "$pr_number" --repo "$REPO" --json body -q '.body' 2>/dev/null)"; then + body_ok="true"; return 0 + fi + [ "$battempt" -lt 3 ] && sleep 2 + done + return 0 +} + +tracking_value() { + local name="$1" + # Tolerate an optional leading markdown list/quote marker (-, *, +, >) before the field + # name. The identity guard that gates a PR write accepts the field in this same anchored, + # tolerant form, so anything it admits is recoverable here. + sed -n "s/^[[:space:]]*[-*+>]*[[:space:]]*${name}:[[:space:]]*//p" | + head -1 | tr -d '"'\''\r' | sed 's/[[:space:]]*#.*$//; s/[[:space:]]*$//' +} + +tracking_block() { + # Emit only the machine-managed state block -- the fenced yaml block the agent writes as + # the VERY LAST thing in the body, delimited by the visible `# ${STATE_MARKER}:state:begin` + # / `:state:end` comment lines (matched as whole comment lines: leading indent and a + # trailing CR are tolerated, but the "# " prefix and the ":state:begin"/":state:end" suffix + # must be exact). Take the LAST begin..end range so human prose anywhere above it -- a quoted + # or bulleted "field:" line, or an older state block a maintainer pastes in as a note -- + # cannot win tracking_value's first match and force a false caught-up no-op or silently + # suppress real reviewer feedback via a bogus watermark. A begin with no matching end (a + # human truncated the block) still yields its fields rather than an empty read. + awk -v b="# ${STATE_MARKER}:state:begin" -v e="# ${STATE_MARKER}:state:end" ' + { t=$0; sub(/\r$/,"",t); gsub(/^[[:space:]]+|[[:space:]]+$/,"",t) } + t == b { inb=1; buf=$0 ORS; next } + inb { buf=buf $0 ORS; if (t == e) { inb=0; last=buf } } + END { if (inb) last=buf; printf "%s", last }' +} + +body_has_state_marker() { + # true iff $1 carries the state block's begin delimiter as a whole yaml-comment line. + # Feed the body on stdin (not a jq --arg) so an oversized PR body can never hit an argv + # length limit, and trim each line's ends with \s -- the same whitespace set awk's + # [[:space:]] trims in tracking_block and the publish guardrail -- so ownership detection + # and block extraction can never disagree. Leading indent and a trailing CR are tolerated; + # the "# " prefix and ":state:begin" suffix must be exact. + printf '%s' "$1" | jq -Rrs --arg m "# ${STATE_MARKER}:state:begin" \ + 'split("\n") | any(gsub("^\\s+|\\s+$";"") == $m)' +} + +watermark="" +has_new_feedback="false" + +if [ -z "$REPO" ] || [ -z "${GH_TOKEN:-}" ]; then + echo "GITHUB_REPOSITORY or GH_TOKEN unset; cannot discover PR -- defaulting to produce (fresh)" + write_target "" "" false "" "" "none" "produce" + step_summary "none" "produce" "" "" "false" + exit 0 +fi + +# ---- 2. Discover the maintained PR and classify it --------------------------------- +# Stage 1: query the canonical branch server-side by exact head. This avoids the +# default `gh pr list` first-30 limit and preserves the invariant that a human-owned PR +# on the canonical branch blocks the automation. +BASE_OWNER="${REPO%%/*}" +pr="" PR_BRANCH="" pr_labels="" pr_is_draft="false" classification="none" body_ok="false" tracking_valid_count=0 +for attempt in 1 2 3; do + rows="$(gh api --method GET "repos/${REPO}/pulls" \ + -f state=open -f head="${BASE_OWNER}:${DESIRED_BRANCH}" -f base="${BASE_BRANCH}" -f per_page=100 2>/dev/null \ + | jq -c --arg owner "$BASE_OWNER" --arg branch "$DESIRED_BRANCH" --arg base "$BASE_BRANCH" \ + '[.[] | select((.head.repo.owner.login // "") == $owner and .head.ref == $branch and (.base.ref // "") == $base) + | {number, headRefName:.head.ref, headRepositoryOwner:{login:(.head.repo.owner.login // "")}, + isDraft:(.draft // false), labels:(.labels // []), updatedAt:.updated_at}] + | sort_by(.updatedAt) | reverse' 2>/dev/null || true)" + [ -n "$rows" ] && break + [ "$attempt" -lt 3 ] && sleep 2 +done +rows="${rows:-[]}" + +pr="$(jq -r '(.[0].number) // empty' <<<"$rows" 2>/dev/null || true)" +if [ -n "$pr" ]; then + pr_is_draft="$(jq -r '.[0].isDraft // false' <<<"$rows")" + PR_BRANCH="$(jq -r '.[0].headRefName // ""' <<<"$rows")" + has_automation="$(jq -r '[.[0].labels[]?.name] | (index("automation") != null) and (index("area-ai") != null)' <<<"$rows")" + # Fetch the PR body with retries. A transient blip must not drop our tracking + # marker -- that would misclassify our own PR as a fresh adopt and replay feedback + # off a phantom watermark; body_ok tells a real empty body from a failed fetch. + fetch_pr_body "$pr" + has_marker="$(body_has_state_marker "$body")" + if [ "$body_ok" != "true" ] && [ "$has_automation" = "true" ]; then + # Body unreadable after retries but our automation labels are present: almost + # certainly our own PR mid-blip. Treat as ours -- the agent re-reads the real + # body and reconciles -- rather than re-adopting or replaying stale feedback. + classification="ours" + elif [ "$has_automation" = "true" ] && [ "$has_marker" = "true" ]; then + classification="ours" + elif [ "$has_automation" = "true" ] && [ "$has_marker" != "true" ]; then + # Automation-labeled PR on our branch with no tracking marker yet: a human + # bootstrapped it -- adopt it (write the marker on this run's update). + classification="adopt" + else + # A PR occupies our branch but is not our automation PR: a human owns it. Stand down. + classification="blocked" + fi +else + # Stage 2: no canonical PR exists. Look for a prior automation tracking PR on the + # canonical branch or on a run-suffixed branch (`desired_branch_`). Keep this + # lightweight and fetch each candidate body individually so open PR history cannot + # grow into a truncated bulk-body parse. + for attempt in 1 2 3; do + rows="$(gh pr list --repo "$REPO" --state open --base "$BASE_BRANCH" --label automation --label area-ai --limit 1000 \ + --json number,headRefName,headRepositoryOwner,isDraft,labels,updatedAt,baseRefName 2>/dev/null \ + | jq -c --arg owner "$BASE_OWNER" --arg branch "$DESIRED_BRANCH" --arg base "$BASE_BRANCH" \ + 'map(select((.headRepositoryOwner.login // "") == $owner + and (.baseRefName // "") == $base + and (.headRefName == $branch + or ((.headRefName | startswith($branch + "_")) + and ((.headRefName | ltrimstr($branch + "_")) | test("^[0-9]+$")))))) + | sort_by(.updatedAt) | reverse' 2>/dev/null || true)" + [ -n "$rows" ] && break + [ "$attempt" -lt 3 ] && sleep 2 + done + rows="${rows:-[]}" + row_count="$(jq -r 'length' <<<"$rows" 2>/dev/null || echo 0)" + selected_body="" + for i in $(seq 0 $((row_count - 1)) 2>/dev/null); do + candidate_pr="$(jq -r ".[$i].number // empty" <<<"$rows")" + [ -n "$candidate_pr" ] || continue + fetch_pr_body "$candidate_pr" + [ "$body_ok" = "true" ] || continue + [ "$(body_has_state_marker "$body")" = "true" ] || continue + candidate_sha="$(printf '%s\n' "$body" | tracking_block | tracking_value "upstream-scan-ref")" + printf '%s' "$candidate_sha" | grep -Eq '^[0-9a-fA-F]{7,}$' || continue + tracking_valid_count=$((tracking_valid_count + 1)) + if [ -z "$pr" ]; then + pr="$candidate_pr" + pr_is_draft="$(jq -r ".[$i].isDraft // false" <<<"$rows")" + PR_BRANCH="$(jq -r ".[$i].headRefName // \"\"" <<<"$rows")" + classification="ours" + selected_body="$body" + fi + done + if [ -n "$pr" ]; then + body="$selected_body" + body_ok="true" + fi +fi + +# ---- 3. Read recorded state + compute the recommended action ----------------------- +pr_recorded_sha="" pr_recorded_release="" +if [ -n "$pr" ] && [ "$classification" != "blocked" ] && [ "$body_ok" = "true" ]; then + watermark="$(printf '%s\n' "$body" \ + | tracking_block | tracking_value "feedback-processed-through")" + pr_recorded_sha="$(printf '%s\n' "$body" \ + | tracking_block | tracking_value "upstream-scan-ref")" + pr_recorded_release="$(printf '%s\n' "$body" \ + | tracking_block | tracking_value "upstream-release")" +fi +[ -n "$upstream_release_commit" ] && [ -n "$pr_recorded_sha" ] && [ "$upstream_release_commit" = "$pr_recorded_sha" ] && release_matches_pr="true" +[ "$release_matches_pr" = "true" ] && release_ready="true" + +# ---- 4. Wake gate: is there any review activity newer than the watermark? ----------- +# Author-agnostic and body-free: this only decides whether a caught-up PR is worth waking +# the agent for. It does NOT read, trust, or filter comment content -- the agent collects +# the actual review feedback itself under gh-aw integrity filtering. Over-waking (e.g. for +# an un-endorsed external comment the agent will not act on) is harmless; under-waking is +# not: each query is retried, and if any still fails the real count is unknown, so the gate +# opens (has_new_feedback=true) and produces, letting the agent's own Step 3 analysis and +# independent feedback discovery stay authoritative instead of trusting a false no-op. +feedback_query_failed="false" +# Retry an activity query up to 3 times, appending its timestamps to $feedback_times only on +# a clean fetch. The discovery and PR-body reads above already retry; matching that here keeps +# a single transient API blip from opening the gate and forcing a needless produce. +fetch_activity() { + local endpoint="$1" jqexpr="$2" fattempt scratch + scratch="$(mktemp)" + for fattempt in 1 2 3; do + if gh api "$endpoint" --paginate -q "$jqexpr" >"$scratch" 2>/dev/null; then + cat "$scratch" >>"$feedback_times"; rm -f "$scratch"; return 0 + fi + [ "$fattempt" -lt 3 ] && sleep 2 + done + rm -f "$scratch"; return 1 +} +if [ -n "$pr" ] && [ "$classification" != "blocked" ]; then + feedback_times="$(mktemp)" + fetch_activity "repos/${REPO}/issues/${pr}/comments" '.[] | select(.user.type != "Bot") | .created_at' || feedback_query_failed="true" + fetch_activity "repos/${REPO}/pulls/${pr}/comments" '.[] | select(.user.type != "Bot") | .created_at' || feedback_query_failed="true" + fetch_activity "repos/${REPO}/pulls/${pr}/reviews" '.[] | select(.user.type != "Bot") | .submitted_at' || feedback_query_failed="true" + if [ "$feedback_query_failed" = "true" ]; then + echo "::warning::review-activity query failed for PR #${pr} after retries; opening the wake gate so this run produces instead of risking a false no-op" + has_new_feedback="true" + else + new_count="$(awk -v since="$watermark" 'NF && (since=="" || $0 > since)' "$feedback_times" | grep -c . || true)" + [ "${new_count:-0}" -gt 0 ] && has_new_feedback="true" + fi + rm -f "$feedback_times" +fi + +# ---- 5. Recommended lifecycle action ------------------------------------------------ +# Only a high-confidence caught-up state early-noops; every uncertain case produces so +# the agent can make the real zero-delta / release-gate decision in Steps 2-3. +action="produce" +case "$classification" in + blocked) + action="noop" ;; + none) + action="produce" ;; # fresh + ours|adopt) + release_changed="false" + [ "$release_ready" = "true" ] && [ "$upstream_release" != "$pr_recorded_release" ] && release_changed="true" + # A published release on an open draft still owes the Step 3 release gate its + # mark-ready (Step 6) transition, even once the tag is already recorded in the body + # (e.g. a torn prior run refreshed the body but never flipped the draft, or the PR + # was converted back to draft). Never let a caught-up state early-noop that pending + # transition: produce so the agent can flip the draft to Ready. + release_pending="false" + [ "$release_ready" = "true" ] && [ "$pr_is_draft" = "true" ] && release_pending="true" + if [ "$classification" = "ours" ] \ + && [ -n "$upstream_sha" ] && [ -n "$pr_recorded_sha" ] \ + && [ "$upstream_sha" = "$pr_recorded_sha" ] \ + && [ "$has_new_feedback" != "true" ] \ + && [ "$release_changed" != "true" ] \ + && [ "$release_pending" != "true" ]; then + action="noop" + else + action="produce" + fi ;; +esac + +pr_state="open"; [ -z "$pr" ] && pr_state="none"; write_target "$pr" "$pr_state" "$pr_is_draft" "$pr_recorded_sha" "$pr_recorded_release" "$classification" "$action" +step_summary "$classification" "$action" "${pr:-}" "$pr_recorded_sha" "$has_new_feedback" + +echo "Setup: classification=${classification} action=${action} pr=${pr:-} pr_branch=${PR_BRANCH:-} recorded_sha=${pr_recorded_sha:-} upstream_sha=${upstream_sha:-} release_ready=${release_ready} new_feedback=${has_new_feedback}" +echo "-- target.json --"; jq '.' "$target_file" diff --git a/.github/skills/agentic-workflow-designer/SKILL.md b/.github/skills/agentic-workflow-designer/SKILL.md new file mode 100644 index 00000000000..b7ee93f0ea5 --- /dev/null +++ b/.github/skills/agentic-workflow-designer/SKILL.md @@ -0,0 +1,390 @@ +--- +name: agentic-workflow-designer +description: Conversational skill that interviews users to design new agentic workflows +disable-model-invocation: true +--- + +# Workflow Designer + +Use this skill to run a structured interview with users who know their goal but not the workflow syntax yet, then generate one complete workflow `.md` file. + +## When to Use This Skill + +Use this before `.github/aw/create-agentic-workflow.md` when requirements are unclear or incomplete. + +- Use `skills/agentic-workflow-designer/SKILL.md` to discover and confirm requirements. +- Use `.github/aw/create-agentic-workflow.md` once requirements are clear and ready for implementation. +- Use `.github/aw/agentic-chat.md` when the user wants a specification/pseudo-code instead of a runnable workflow file. + +## Interview Framework + +Ask one question at a time. Move to the next phase only after the current phase is clear. + +### Phase 1: Goal + +Ask: **"What do you want to automate?"** + +Capture: +- Workflow name (kebab-case candidate) +- Brief description +- Optional emoji + +### Phase 2: Trigger + +Ask: **"When should this run?"** + +Follow up only if needed: +- Which event type(s)? +- Any filters (labels, branches, commands)? +- Scheduled cadence (daily/weekly/hourly)? + +Map to the `on:` block. + +### Phase 3: Scope (Read/Write) + +Ask: +- **"What should it read?"** (issues, PRs, code, discussions, CI data) +- **"What should it create or update?"** (comments, issues, PRs, labels) + +Map to: +- `permissions:` (keep read-only for agent job) +- `tools:` +- `safe-outputs:` + +### Phase 4: Data Strategy + +Ask: +- **"What data does the agent need to make decisions?"** +- Follow up: **"Can we pre-fetch and aggregate that data with shell commands so the agent only reads compact JSON?"** + +Capture: +- Whether `steps:` should pre-fetch GitHub data with `gh` + `jq` +- Output paths under `/tmp/gh-aw/data/` +- Whether batch work should use sub-agents + +Map to: +- `steps:` +- Prompt references to pre-computed file paths + +### Phase 5: Guardrails + +Ask: **"Should it block merging, just advise, or silently log?"** + +Capture: +- Visibility expectations (comment, issue, no visible output) +- No-op behavior expectation + +Guide toward safe output behavior and explicit `noop` instructions. + +### Phase 6: Context & Network + +Ask: **"Does it need external APIs, web access, package installs, or MCP servers?"** + +Follow up: +- **"Any third-party services or MCP servers to include (for example Slack, Jira, Datadog, custom internal MCP)?"** +- **"Are you deploying on GitHub.com, GHEC with custom endpoints, or GHES?"** +- For each integration, identify required auth from source docs and map it to GitHub Actions secrets + workflow env variables. +- Ask for exact external domains (FQDN/wildcard). + +Map to: +- `network.allowed` +- Optional MCP/GitHub tool usage in `tools:` +- `secrets:` / `env:` wiring for integration tokens +- GHES/GHEC settings such as `engine.api-target` and `aw.json` `ghes: true` (when applicable) + +### Phase 7: Engine (optional) + +Ask only if ambiguous: **"Any AI engine preference?"** + +If no preference, suggest default: +- "I'd suggest Copilot since you haven't mentioned a preference. Sound good?" + +Map to `engine:` only when not default. + +### Phase 8: Confirmation + +Present a structured summary and ask for approval before generation. + +## Decision Heuristics + +### Trigger Mapping + +| User says... | Maps to | +|---|---| +| "when someone opens a PR" | `on: pull_request:` with `types: [opened]` | +| "when a PR is updated" | `on: pull_request:` with `types: [opened, synchronize]` | +| "every morning", "daily" | fuzzy schedule shorthand `on: schedule: daily on weekdays` (compiler expands to cron) | +| "every Monday", "weekly" | fuzzy schedule shorthand `on: schedule: weekly` (compiler expands to cron) | +| "when I say /review" | `on: slash_command:` with `name: review` (or requested command) | +| "when an issue is labeled bug" | `on: issues:` with `types: [labeled]` and label filter guidance | +| "run when label ai-review is added" | `on: label_command:` with `name`/`names`, optional event scoping, and label-as-command semantics | +| "run on PRs from forks" | `on: pull_request:` plus explicit `forks:` allowlist and fork security guardrails | +| "sometimes automatic, sometimes manual" | semi-active pattern: combine `schedule`/event triggers with `workflow_dispatch` | +| "manually", "on demand" | `on: workflow_dispatch:` | +| "when a deployment fails" | `on: deployment_status:` | +| "when another workflow finishes" | `on: workflow_run:` | + +### Safe Output Mapping + +| User says... | Maps to | +|---|---| +| "post a comment" | `add-comment` | +| "create an issue" | `create-issue` | +| "update issue title/body" | `update-issue` | +| "close the issue" | `close-issue` | +| "assign someone", "remove assignment" | `assign-to-user`, `unassign-from-user` | +| "set issue type/field/milestone" | `set-issue-type`, `set-issue-field`, `assign-milestone` | +| "open a PR", "submit changes" | `create-pull-request` | +| "update PR description/title" | `update-pull-request` | +| "close the PR", "merge the PR" | `close-pull-request`, `merge-pull-request` | +| "mark PR ready", "sync PR branch" | `mark-pull-request-as-ready-for-review`, `update-branch` | +| "commit a fix to the PR branch" | `push-to-pull-request-branch` | +| "approve / request changes" | `submit-pull-request-review` | +| "inline review comment", "reply to review thread" | `create-pull-request-review-comment`, `reply-to-pull-request-review-comment`, `resolve-pull-request-review-thread` | +| "start or edit discussion", "close discussion" | `create-discussion`, `update-discussion`, `close-discussion` | +| "request reviewer", "hide comment" | `add-reviewer`, `hide-comment` | +| "create/update project", "project status update" | `create-project`, `update-project`, `create-project-status-update` | +| "update release", "upload release asset" | `update-release`, `upload-asset` | +| "trigger another workflow", "dispatch to workflow", "run another workflow" | `dispatch-workflow` | +| "create/auto-fix code scan alert" | `create-code-scanning-alert`, `autofix-code-scanning-alert` | +| "start an agent session", "assign to an agent" | `create-agent-session`, `assign-to-agent` | +| "store persistent memory comment" | `comment-memory` | +| "link a sub-issue" | `link-sub-issue` | +| "add labels", "remove labels" | `add-labels`, `remove-labels` | +| "replace a label with another" | `replace-label` | +| "nothing visible", "just analyze" | no safe outputs required | + +### Network Mapping + +| User says... | Maps to | +|---|---| +| "calls an external API" | ask for exact FQDN/wildcard, then add to `network.allowed` | +| "reads GitHub data / clones repos" | include `github` in `network.allowed` | +| "uses GitHub Actions artifacts or cache" | include `github-actions` in `network.allowed` | +| "installs npm packages" | include `node` in `network.allowed` | +| "runs pip install" | include `python` in `network.allowed` | +| "builds Go code" | include `go` in `network.allowed` | +| "installs gems / uses Bundler" | include `ruby` in `network.allowed` | +| "runs cargo build" | include `rust` in `network.allowed` | +| "uses NuGet / .NET restore" | include `dotnet` in `network.allowed` | +| "builds with Maven / Gradle" | include `java` in `network.allowed` | +| "uses Docker / pulls container images / pushes to GHCR" | include `containers` in `network.allowed` | +| "runs Playwright browser tests" | include `playwright` in `network.allowed` | +| "runs apt install / yum / apk" | include `linux-distros` in `network.allowed` | +| "uses Terraform / HashiCorp registry" | include `terraform` in `network.allowed` | +| "connects to localhost / loopback / local services" | include `local` in `network.allowed` | +| "uses Swift Package Manager" | include `swift` in `network.allowed` | +| "uses Composer / PHP packages" | include `php` in `network.allowed` | +| "uses pub.dev / Dart packages" | include `dart` in `network.allowed` | +| "uses Hackage / Haskell packages" | include `haskell` in `network.allowed` | +| "uses CPAN / Perl packages" | include `perl` in `network.allowed` | +| "serves or loads web fonts" | include `fonts` in `network.allowed` | +| "uses Deno or JSR packages" | include `deno` in `network.allowed` | +| "uses Elixir / Hex packages" | include `elixir` in `network.allowed` | +| "uses Bazel build" | include `bazel` in `network.allowed` | +| "uses Clojure / Clojars packages" | include `clojure` in `network.allowed` | +| "uses Julia packages" | include `julia` in `network.allowed` | +| "uses Kotlin / JetBrains packages" | include `kotlin` in `network.allowed` | +| "uses LuaRocks / Lua packages" | include `lua` in `network.allowed` | +| "uses node CDNs (jsdelivr, unpkg)" | include `node-cdns` in `network.allowed` | +| "uses OPAM / OCaml packages" | include `ocaml` in `network.allowed` | +| "uses PowerShell Gallery" | include `powershell` in `network.allowed` | +| "uses R / CRAN packages" | include `r` in `network.allowed` | +| "uses sbt / Scala packages" | include `scala` in `network.allowed` | +| "uses Zig packages" | include `zig` in `network.allowed` | +| "uses Renovate, Codecov, or other CI tools" | include `dev-tools` in `network.allowed` | +| "uses Chrome / Chromium downloads" | include `chrome` in `network.allowed` | +| "uses LaTeX / TeX / MiKTeX" | include `latex` in `network.allowed` | +| "uses Lean theorem prover" | include `lean` in `network.allowed` | +| "builds Python packages from source" | include `python-native` in `network.allowed` | +| "no external access" | `network.allowed: [defaults]` (or `[]` if explicitly zero network) | + +### Tool Mapping + +| User says... | Maps to | +|---|---| +| "read GitHub issues/PRs/workflows" | `tools.github` with `mode: gh-proxy` and minimal `toolsets` | +| "use full MCP server/tool definitions" | `tools.github` with `mode: local` | +| "use other MCP servers but keep token cost down" | `tools.cli-proxy: true` (hybrid CLI-proxy mode) | +| "edit files" | `edit` tool (default unless restricted) | +| "run commands/tests" | `bash` tool (default unless restricted) | +| "browse web pages/docs" | `web-fetch` and/or `web-search` | +| "test UI flows" | `playwright` | + +### Pattern Heuristics + +| User says... | Recommended named pattern | +|---|---| +| "triage issues automatically" | `IssueOps` | +| "run on /commands with human approval loops" | `ChatOps` | +| "run every weekday and keep improving" | `DailyOps` | +| "monitor workflow failures and trends" | `MonitorOps` | +| "process a big backlog in chunks" | `BatchOps` | +| "run manually with input parameters" | `DispatchOps` | +| "apply a label-based workflow" | `LabelOps` | +| "operate across multiple repositories" | `MultiRepoOps` | +| "coordinate multiple sub-agents" | `Orchestration` | +| "manage project board items" | `ProjectOps` | +| "research, plan, and assign issues" | `ResearchPlanAssignOps` | +| "self-correcting / retry on failure" | `CorrectionOps` | +| "run in a side/fork repo" | `SideRepoOps` | +| "write a spec before implementing" | `SpecOps` | +| "A/B test workflow variants" | `TrialOps` | +| "process items from a queue" | `WorkQueueOps` | +| "deterministic, no LLM needed" | `DeterministicOps` | +| "manage from a central repo" | `CentralRepoOps` | +| "track work via GitHub Projects" | `Monitoring with Projects` | + +### Integration Auth Mapping + +When the user names a third-party service or MCP server: + +1. Confirm whether native tool, MCP server, or safe-output job is the right integration path. +2. Look up the integration's auth requirements and required scopes before finalizing the design. +3. Provide a concrete setup checklist with: + - required GitHub Actions secrets (names to create) + - workflow env variables that consume those secrets + - minimum token scopes/permissions needed + +Output format to use: + +```text +Integration auth setup: +- : + - Secrets to create: , + - Workflow env vars: =${{ secrets. }} + - Required scopes/permissions: +``` + +Never suggest committing plaintext tokens. + +### Data Strategy Mapping + +| User says... | Maps to | +|---|---| +| "analyze PRs", "review issues", "check status" | add `steps:` that pre-fetch with `gh` + `jq` | +| "read the diff", "look at changed files" | add `steps:` using `gh pr diff` or `gh pr view --json files` | +| "search for patterns across repos" | add `steps:` using `gh search` + `jq` filters | +| "just respond to a comment" | no pre-fetch needed (event payload is enough) | +| "process each item individually" | suggest sub-agent pattern with `model: small` | + +## Token Optimization Defaults + +Apply these defaults unless the user explicitly asks otherwise: + +1. Use DataOps by default for GitHub reads: pre-fetch/aggregate with `gh` + `jq` in `steps:`, store compact JSON in `/tmp/gh-aw/data/`, and point the prompt to those files (see `.github/aw/token-optimization.md` for details). +2. Keep tool surface minimal: default to `tools.github.mode: gh-proxy`, include only required toolsets, and prefer `bash` + `gh` for simple reads. +3. For batch workloads, split items into compact data and suggest sub-agent processing with `model: small`. +4. Keep prompts compact: concise imperative instructions, explicit file paths, single-line `noop` guidance, and stable instructions before dynamic content. + +## Progressive Disclosure Rules + +1. Never dump all options at once; ask one targeted question at a time. +2. Skip questions when answers are inferable from prior user statements. +3. Offer smart defaults and request confirmation instead of over-questioning. +4. Ask at most 5 questions before presenting a summary; then ask "anything else?" if needed. +5. Detect done signals (`that's it`, `looks good`, `generate it`) and proceed to generation. + +## Confirmation Format + +Use this exact structure: + +```text +📋 Proposed workflow: +- Name: +- Trigger: +- Engine: +- Tools: +- Safe outputs: +- Network: +- Integrations/Auth: +- Deployment: +- Intent: +``` + +Then ask: **"Ready to generate, or want to adjust anything?"** + +## Generation Template + +After confirmation, generate one workflow file using the same skeleton style as `.github/aw/create-agentic-workflow.md`. + +```markdown +--- +emoji: +description: +on: + +permissions: + contents: read + issues: read + pull-requests: read +tools: + github: + mode: gh-proxy + toolsets: [default] +steps: + - name: + run: | + mkdir -p /tmp/gh-aw/data + +safe-outputs: + +network: + allowed: + - defaults + - +--- + +# + +## Task + + +If `steps:` includes pre-fetch commands, read the resulting `/tmp/gh-aw/data/*.json` files instead of broad live re-fetches. + +## Safe Outputs + +- Use configured safe outputs for all visible write actions. +- Call `noop` with a short reason when no action is needed. +``` + +## Validation Checklist + +Before final output, run this internal self-check: + +- [ ] Agent job permissions remain read-only (writes only via safe outputs) +- [ ] `safe-outputs:` covers every write action mentioned in prompt/instructions +- [ ] Network access is scoped; avoid blanket wildcard entries +- [ ] Trigger matches the user's intended activation event +- [ ] Prompt instructs agent to call `noop` when no action is needed +- [ ] Unnecessary defaults are omitted (for example `engine: copilot`) +- [ ] If reading GitHub data, `steps:` pre-fetches compact JSON (DataOps) +- [ ] `tools.github.mode` is `gh-proxy` unless broader MCP toolsets are explicitly needed +- [ ] Only required toolsets are listed (avoid blanket toolset lists) +- [ ] Prompt references specific pre-computed file paths +- [ ] For batch processing (>5 items), sub-agent pattern is suggested +- [ ] For each third-party service/MCP integration, required secrets/env vars are listed +- [ ] Auth guidance includes least-privilege token scope recommendations +- [ ] For GHEC/GHES deployments, `engine.api-target` and GHES compatibility guidance are included when needed + +## References (load only when needed) + +In-repo references: +- `.github/aw/syntax.md` (index → `.github/aw/syntax-core.md`, `.github/aw/syntax-agentic.md`, `.github/aw/syntax-tools-imports.md`) +- `.github/aw/safe-outputs.md` (index → `.github/aw/safe-outputs-content.md`, `.github/aw/safe-outputs-management.md`, `.github/aw/safe-outputs-automation.md`, `.github/aw/safe-outputs-runtime.md`) +- `.github/aw/network.md` +- `.github/aw/patterns.md` +- `.github/aw/subagents.md` +- `.github/aw/token-optimization.md` +- `.github/aw/triggers.md` +- `.github/aw/create-agentic-workflow.md` + +Portable HTTPS references: +- `https://github.com/github/gh-aw/blob/main/.github/aw/syntax.md` (index → `.../syntax-core.md`, `.../syntax-agentic.md`, `.../syntax-tools-imports.md`) +- `https://github.com/github/gh-aw/blob/main/.github/aw/safe-outputs.md` (index → `.../safe-outputs-content.md`, `.../safe-outputs-management.md`, `.../safe-outputs-automation.md`, `.../safe-outputs-runtime.md`) +- `https://github.com/github/gh-aw/blob/main/.github/aw/network.md` +- `https://github.com/github/gh-aw/blob/main/.github/aw/patterns.md` +- `https://github.com/github/gh-aw/blob/main/.github/aw/subagents.md` +- `https://github.com/github/gh-aw/blob/main/.github/aw/token-optimization.md` +- `https://github.com/github/gh-aw/blob/main/.github/aw/triggers.md` +- `https://github.com/github/gh-aw/blob/main/.github/aw/create-agentic-workflow.md` diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md new file mode 100644 index 00000000000..29be0f6fab9 --- /dev/null +++ b/.github/skills/agentic-workflows/SKILL.md @@ -0,0 +1,84 @@ +--- +name: agentic-workflows +description: Route gh-aw workflow design/create/debug/upgrade requests to the right prompts. +--- + +# Agentic Workflows Router + +Use this skill when a user asks to design, create, update, debug, or upgrade GitHub Agentic Workflows in this repository. + +This skill is a dispatcher: identify the task type, load the matching workflow prompt/skill file, and follow it directly. Keep responses concise and ask a clarifying question if the correct prompt is unclear. + +Read only the files you need: +Load these files from `github/gh-aw` (they are not available locally). +- `.github/aw/agentic-chat.md` +- `.github/aw/agentic-workflows-mcp.md` +- `.github/aw/asciicharts.md` +- `.github/aw/campaign.md` +- `.github/aw/charts-trending.md` +- `.github/aw/charts.md` +- `.github/aw/cli-commands.md` +- `.github/aw/context.md` +- `.github/aw/create-agentic-workflow.md` +- `.github/aw/create-shared-agentic-workflow.md` +- `.github/aw/debug-agentic-workflow.md` +- `.github/aw/dependabot.md` +- `.github/aw/deployment-status.md` +- `.github/aw/experiments.md` +- `.github/aw/github-agentic-workflows.md` +- `.github/aw/github-mcp-server.md` +- `.github/aw/instructions.md` +- `.github/aw/llms.md` +- `.github/aw/loop.md` +- `.github/aw/lsp.md` +- `.github/aw/mcp-clis.md` +- `.github/aw/memory-stateful-patterns.md` +- `.github/aw/memory.md` +- `.github/aw/messages.md` +- `.github/aw/network.md` +- `.github/aw/optimize-agentic-workflow.md` +- `.github/aw/patterns.md` +- `.github/aw/pr-reviewer.md` +- `.github/aw/report.md` +- `.github/aw/reuse.md` +- `.github/aw/safe-outputs-automation.md` +- `.github/aw/safe-outputs-content.md` +- `.github/aw/safe-outputs-management.md` +- `.github/aw/safe-outputs-runtime.md` +- `.github/aw/safe-outputs.md` +- `.github/aw/serena-tool.md` +- `.github/aw/shared-safe-jobs.md` +- `.github/aw/skills.md` +- `.github/aw/subagents.md` +- `.github/aw/syntax-agentic.md` +- `.github/aw/syntax-core.md` +- `.github/aw/syntax-tools-imports.md` +- `.github/aw/syntax.md` +- `.github/aw/test-coverage.md` +- `.github/aw/test-expression.md` +- `.github/aw/token-optimization.md` +- `.github/aw/triggers.md` +- `.github/aw/update-agentic-workflow.md` +- `.github/aw/upgrade-agentic-workflows.md` +- `.github/aw/visual-regression.md` +- `.github/aw/workflow-constraints.md` +- `.github/aw/workflow-editing.md` +- `.github/aw/workflow-patterns.md` + +- `.github/skills/agentic-workflow-designer/SKILL.md` +After loading the matching workflow prompt or skill, follow it directly: +- Design workflows from scratch via interview: `skills/agentic-workflow-designer/SKILL.md` +- Create new workflows: `.github/aw/create-agentic-workflow.md` +- Update existing workflows: `.github/aw/update-agentic-workflow.md` +- Debug, audit, or investigate workflows: `.github/aw/debug-agentic-workflow.md` +- Upgrade workflows and fix deprecations: `.github/aw/upgrade-agentic-workflows.md` +- Create shared components or MCP wrappers: `.github/aw/create-shared-agentic-workflow.md` +- Create report-generating workflows: `.github/aw/report.md` +- Fix Dependabot manifest PRs: `.github/aw/dependabot.md` +- Analyze coverage workflows: `.github/aw/test-coverage.md` +- Render compact markdown charts: `.github/aw/asciicharts.md` +- Map CLI commands to MCP usage: `.github/aw/cli-commands.md` +- Choose workflow architecture and patterns: `.github/aw/patterns.md` +- Optimize token usage and cost: `.github/aw/token-optimization.md` + +When the task involves OTEL, OTLP, traces, observability backends, or telemetry-driven analysis, also read and follow `skills/otel-queries/SKILL.md` after loading the matching workflow prompt or skill. diff --git a/.github/skills/update-agent-framework-template/SKILL.md b/.github/skills/update-agent-framework-template/SKILL.md new file mode 100644 index 00000000000..7fe00fb1095 --- /dev/null +++ b/.github/skills/update-agent-framework-template/SKILL.md @@ -0,0 +1,99 @@ +--- +name: update-agent-framework-template +description: >- + Keep the aiagent-webapi project template's Microsoft.Agents.AI package versions aligned with the + newest coherent Microsoft Agent Framework release published on the dnceng "dotnet-public" NuGet + feed. Use when asked to "update the Agent Framework template", "bump the aiagent-webapi template", + "align the project template with the latest Agent Framework", or when given an Agent Framework + release signal from agent-framework-discover.cs. Covers how to detect the release, map it onto the + template's package subset, CI-validate the bump, and format the maintained draft pull request. +agent: 'agent' +tools: ['github/*', 'bash'] +--- + +# Update Agent Framework Template + +Keep the `aiagent-webapi` project template shipped by `Microsoft.Agents.AI.ProjectTemplates` aligned +with the newest coherent **Microsoft Agent Framework** release. The template's package versions are +the single source of truth in `eng/packages/ProjectTemplates.props`; this skill is the authority for +**how** to detect the release, map it onto the template's packages, validate the bump, and format the +pull request. The `agent-framework-worker` workflow owns the **lifecycle** (which PR to touch and +what state to leave it in). + +## The release signal + +`.github/scripts/agent-framework-discover.cs` (a file-based C# app using the NuGet client SDK) is the +authoritative release detector. It reads the `dotnet-public` feed and emits, for the whole +`Microsoft.Agents.AI*` family, the newest coherent release: a `release_version` (the anchor +`Microsoft.Agents.AI` newest stable, e.g. `1.13.0`), a `release_date` (the shared `YYMMDD` stamp), and +per-package `at_release` versions. See [references/version-detection.md](references/version-detection.md). + +Drive the signal **only** off `dotnet-public` -- it is the feed the template's package restore +validates against, so a version detected there is guaranteed restorable by CI. Never hand-parse the +feed's flat-container JSON: the NuGet client SDK's `NuGetVersion`/`VersionComparer` gives correct +SemVer ordering, sidestepping the feed's descending sort order and the anomalous `0.0.1-preview.*` +entry. + +## Bumping the packages (in lockstep) + +The Agent Framework packages ship **as a set** for each release, so bump **every** +`Microsoft.Agents.AI*` package pinned in `eng/packages/ProjectTemplates.props` together -- do not bump +a subset. Discover the package ids from the props file itself (data-driven, so nothing is missed as +the family grows), and for each set its version to that package's `at_release` value from the signal: + +``` +desired[id] = signal.packages[id].at_release for each Microsoft.Agents.AI* id in the props +``` + +**Exclude `Microsoft.Agents.AI.ProjectTemplates`** -- the template package itself is on its own +cadence and is not part of the released set. + +Each package keeps its **own stabilization tier** -- do not force all packages to a stable version. +At the current release, `Microsoft.Agents.AI` / `.Abstractions` / `.OpenAI` / `.Workflows` / +`.Workflows.Generators` are `stable`, `.DevUI` / `.Hosting` / `.Foundry` / `.Foundry.Hosting` are +`-preview.*`, and `.Hosting.OpenAI` is `-alpha.*`. Tiers can change between releases (e.g. a package +that was stable can move to preview-only), which is exactly why each package's version comes from its +own `at_release` rather than a shared value. + +## What to change + +A routine release bump touches two files: + +1. `eng/packages/ProjectTemplates.props` -- update the `Version` attribute of every + `Microsoft.Agents.AI*` `` item (except `Microsoft.Agents.AI.ProjectTemplates`) to + its mapped `at_release` version. +2. `src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.csproj` + -- align the template NuGet package's own version with the release by setting ``, + ``, and `` to the release's Major.Minor.Patch. **Never** change + `` -- the prerelease label portion is left as-is (e.g. `1.3.0-preview` -> + `1.13.0-preview`). + +See [references/change-classification.md](references/change-classification.md) for when more is needed +(a newly split package tier, an added/removed package, or a framework API change that would require +editing the template's own source or other consumption). + +## Validate before publishing + +Every bump must be CI-validated before it is published. The worker's setup script does this host-side +and records the results in `target.json`: it restores, builds, and packs the +`Microsoft.Agents.AI.ProjectTemplates` package through the repo's Arcade build, then runs the +template's snapshot + execution tests. Never open or update a PR with an unvalidated +(`validated: false`) bump. Evaluating whether other Agent Framework consumption under +`src/Libraries/Microsoft.Extensions.AI*` needs updates is the **agent's** job (see below), not the +host build. See +[references/build-commands.md](references/build-commands.md) and +[references/testing.md](references/testing.md). + +## Evaluate changes across the release + +Beyond bumping versions, evaluate what changed in Agent Framework between the previously integrated +version and the target release, and bring the template and any other consumption up to the currently +prescribed patterns. The setup script gathers the `microsoft/agent-framework` `dotnet-*` release +notes for the range into `af-changes.md` for you. See +[references/evaluate-changes.md](references/evaluate-changes.md). + +## Pull request format + +The maintained PR is a single continuously-updated **draft** against `main`, labeled `automation` + +`area-ai-templates`, on the `update-agent-framework-template` branch, carrying a machine-readable +tracking block. See [references/pr-description.md](references/pr-description.md). diff --git a/.github/skills/update-agent-framework-template/references/build-commands.md b/.github/skills/update-agent-framework-template/references/build-commands.md new file mode 100644 index 00000000000..a6125f83c27 --- /dev/null +++ b/.github/skills/update-agent-framework-template/references/build-commands.md @@ -0,0 +1,42 @@ +# Build / CI-validation commands + +The bump is CI-validated through the repository's Arcade build -- a real restore, build, and **pack** +of the `Microsoft.Agents.AI.ProjectTemplates` package -- followed by the template's snapshot + +execution tests. The worker's setup script (`.github/scripts/agent-framework-worker-setup.sh`) runs +this host-side before the agent and records the result in `target.json` (`validated`, +`build_summary`, `tests_summary`, with the full log tails in `/tmp/gh-aw/agent/build.log` and +`/tmp/gh-aw/agent/tests.log`). + +## What it does + +1. Apply the mapped `desired_versions` to `eng/packages/ProjectTemplates.props` and the aligned + Major/Minor/Patch to the template package project. +2. Restore + build + pack the template package through the repo's Arcade build, producing the + `.nupkg` the execution tests install: + + ```bash + ./build.sh -ci -restore -build -pack \ + -projects "$PWD/src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.csproj" \ + -configuration Release + ``` + +3. A clean pack => the package validated. Any restore/build/pack failure => `validated: false`, and + the bump is **not** published. + +## Requirements + +- The repo-bootstrapped .NET SDK. `./build.sh --restore` provisions `.dotnet/dotnet` (the pinned SDK + from `global.json`); the setup script invokes that `dotnet` directly for the test pass. +- Package restore reaches `dotnet-public` (and `nuget.org` as a transitive-dependency fallback), the + domains allowed in the worker's `network.allowed`. +- The template's `.csproj-in` resolves its `${PackageVersion:*}` tokens from + `eng/packages/ProjectTemplates.props` at pack time, so a routine bump edits only that props file + and the template package project version -- never the template content. + +## Agent responsibility + +The agent does **not** build. It copies the already-validated +`/tmp/gh-aw/agent/ProjectTemplates.props.bumped` and +`/tmp/gh-aw/agent/ProjectTemplates.csproj.bumped` into place so what it publishes is exactly what was +validated, then performs only git + safe-output work. If `validated` is `false`, the agent must not +open or update the PR with the bump -- emit `report_incomplete` instead. diff --git a/.github/skills/update-agent-framework-template/references/change-classification.md b/.github/skills/update-agent-framework-template/references/change-classification.md new file mode 100644 index 00000000000..13dd7d20c9f --- /dev/null +++ b/.github/skills/update-agent-framework-template/references/change-classification.md @@ -0,0 +1,43 @@ +# Change classification + +Classify the delta between the newest release and what `eng/packages/ProjectTemplates.props` +currently pins, and change only what each class requires. + +## 🟢 Routine version bump (the common case) + +Every referenced package's major.minor.patch moved, but the set of packages and the template's own +source are unchanged. Change two things: + +1. `eng/packages/ProjectTemplates.props` -- update the `Version` attribute of each + `Microsoft.Agents.AI*` `` item to its mapped `at_release` version. +2. The template package project version -- align ``/``/`` + in `src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.csproj` + to the release, leaving `` unchanged. + +Nothing else needs to change; the template's `${PackageVersion:*}` tokens resolve from the props at +pack time, so the package packs and its snapshot + execution tests pass against the new versions. + +## 🟡 Structural change (occasional) + +A package's tier changed (e.g. a package that used to reuse the core token now trails on its own +version), or a package was added to / removed from the Agent Framework. In addition to the version +bump, update the template's `` list -- in the shipped template `.csproj-in` under +`src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/templates/AIAgentWebApi-CSharp/` -- to +add/remove the reference or point it at the right `${PackageVersion:*}` key. Because the template is a +`.csproj-in` (not a directly buildable project), changes to it are validated through the snapshot + +execution tests, which regenerate/compile the generated project. + +## 🔴 Framework API change (rare) + +The new Agent Framework version changed an API the template's source uses, so the generated project no +longer compiles. The CI validation (see build-commands.md) is what catches this: the execution tests +fail and `validated` will be `false`. Do **not** publish an unvalidated bump. A framework API change +requires editing the template's own source (`Program.cs` etc.) to match the new API and regenerating +the affected `.verified` snapshots -- treat it as a real change for human review rather than an +automatic version bump, and surface the failure in the PR body. + +## Not in scope + +Non-Agent-Framework packages in the props file (e.g. `OpenTelemetry.Api`) are outside this +automation's routine bump. Leave them unless review feedback explicitly asks otherwise and the change +stays within the allowed files and CI-validates. diff --git a/.github/skills/update-agent-framework-template/references/evaluate-changes.md b/.github/skills/update-agent-framework-template/references/evaluate-changes.md new file mode 100644 index 00000000000..57c039756ab --- /dev/null +++ b/.github/skills/update-agent-framework-template/references/evaluate-changes.md @@ -0,0 +1,33 @@ +# Evaluating Agent Framework changes across a release + +A version bump is not complete until the template and any other Agent Framework consumption in the +repository are confirmed to still work and to follow the currently prescribed patterns. + +## Inputs the host prepares + +The worker's setup script writes these into `/tmp/gh-aw/agent/` and records them in `target.json`: + +- `af_changes_path` (`af-changes.md`) -- the `microsoft/agent-framework` `dotnet-*` release notes for + every release published after `from_version` and up to `release_version` (`af_change_count` of + them). `from_version` is the version the repo already integrated (the maintained PR's recorded + version, or `main`'s current version). +- `build_summary` / `tests_summary` -- the host build + test results for the prospective bump. + +## What to evaluate + +1. Read `af-changes.md`. Identify: removed or renamed APIs, deprecations, behavioral changes, and any + newly recommended/prescribed patterns or replacements. +2. Enumerate the consumption: + - the template under `src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/**`, and + - other consumption under `src/Libraries/Microsoft.Extensions.AI*/**`. +3. For each affected usage, update it to the current prescribed pattern, staying within the allowed + files. Prefer the minimal change that adopts the new guidance. +4. If nothing is affected, that is a valid result -- record in the PR body that the changes were + reviewed and no consumption updates were required. + +## Scope discipline + +Only edit the allowed files: the template tree, the `Microsoft.Extensions.AI*` libraries, +`eng/packages/ProjectTemplates.props`, and the template integration-test tree. Anything else (or a +change that would require touching out-of-scope code) is out of scope -- note it in the PR body for a +human rather than editing it. diff --git a/.github/skills/update-agent-framework-template/references/pr-description.md b/.github/skills/update-agent-framework-template/references/pr-description.md new file mode 100644 index 00000000000..02c4e3f301d --- /dev/null +++ b/.github/skills/update-agent-framework-template/references/pr-description.md @@ -0,0 +1,48 @@ +# Pull request description + +The automation maintains a **single** continuously-updated draft PR against `main`: + +- **Branch**: `update-agent-framework-template` (evergreen; if the remote branch already exists from a + prior closed PR, suffix with the run id). +- **Labels**: `automation`, `area-ai-templates` (required for the automation to own and act on it). +- **Title**: `Update Agent Framework to `. +- **Draft**: yes. The automation never marks ready or merges; a human reviews and merges. + +## Body + +Regenerate the entire body on every full-body write. Keep it factual and short: + +1. One line: bumping the Microsoft.Agents.AI packages to Agent Framework `` + (released ``), sourced from the `` feed. +2. A table of version changes -- one row per entry in `desired_versions`, `old -> new`. +3. The **template package version** change: `template_pkg_old -> template_pkg_new` (the + `Microsoft.Agents.AI.ProjectTemplates` package's own version, aligned to the release + Major/Minor/Patch with its prerelease label unchanged). +4. **CI validation**: state that `eng/packages/ProjectTemplates.props` and the template package + version were bumped and the template package restored + built + packed successfully through the + repo's Arcade build (quote `build_summary`), and that the snapshot + execution tests passed (quote + `tests_summary`). +5. **Agent Framework changes reviewed**: note the `af_change_count` releases evaluated across + `from_version -> release_version`, and either the consumption updates made or that none were + required. +6. A note that the automation maintains this draft; a human reviews and merges. + +## Tracking block (required, verbatim, as the very last thing in the body) + +Wrap it in a `yaml` code fence -- the fence lines are **required** so the `#` marker lines render as +code instead of Markdown headings. Reproduce the fence and block exactly (opening ```` ```yaml ````, +the block, closing ```` ``` ````): + +```yaml +# agent-framework-template:state:begin +source-feed: +agent-framework-version: +agent-framework-release-date: +feedback-processed-through: +# agent-framework-template:state:end +``` + +The next run reads this block back to recover its place: `agent-framework-version` tells it which +release the PR is already at (caught-up vs behind), and `feedback-processed-through` is the watermark +below which review activity is considered already handled. The worker's post-run identity check fails +the run if a full-body PR write omits the markers or the `feedback-processed-through` watermark. diff --git a/.github/skills/update-agent-framework-template/references/testing.md b/.github/skills/update-agent-framework-template/references/testing.md new file mode 100644 index 00000000000..c1144efd848 --- /dev/null +++ b/.github/skills/update-agent-framework-template/references/testing.md @@ -0,0 +1,32 @@ +# Template snapshot and integration tests + +The `aiagent-webapi` template has snapshot + integration tests under +`test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests`. They must pass for +every published bump. The worker's setup script runs them host-side and records the result in +`target.json.tests_summary`. + +## Snapshot tests + +Each generated template file is compared, scrubbed to be version-agnostic (package versions become +`{VERSION}`), against a committed `.verified` snapshot under `Snapshots/aiagent-webapi/`. This means: + +- A **pure version bump** does not change the snapshots (versions are scrubbed) -- they keep passing. +- A change to the template's **content** (e.g. `Program.cs`, or adding/removing a `` + in the template project) does change the generated output, so the affected `.verified` snapshot + must be **regenerated** to match the intended new content, or the snapshot test fails. + +Regenerate a snapshot by copying the current (scrubbed) template file over its `.verified` counterpart +once the new content is intentional, then re-run the tests. + +## Integration (execution) tests + +The execution test packs the template, runs `dotnet new aiagent-webapi` in a sandbox, then restores +and builds the generated project against the Agent Framework versions currently pinned in +`eng/packages/ProjectTemplates.props`. It is the canary for a version that does not restore, or a +template that does not compile against the new Agent Framework surface. + +## Never publish red + +If the tests fail and you cannot make them pass within the allowed files (including regenerating +snapshots for intended content changes), do **not** open or update the PR with the change -- emit +`report_incomplete` explaining why, so the failure is surfaced instead of shipped. diff --git a/.github/skills/update-agent-framework-template/references/version-detection.md b/.github/skills/update-agent-framework-template/references/version-detection.md new file mode 100644 index 00000000000..ce2dac4f2dd --- /dev/null +++ b/.github/skills/update-agent-framework-template/references/version-detection.md @@ -0,0 +1,41 @@ +# Version detection (the `dotnet-public` feed signal) + +`.github/scripts/agent-framework-discover.cs` is the authoritative detector. It is a .NET 10 +file-based app (`dotnet run agent-framework-discover.cs -- `) that uses the NuGet client +SDK (`NuGet.Protocol` + `NuGet.Versioning`). + +## Feed + +- Service index: `https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json` + (unauthenticated). This is the same feed the template's package restore uses, so any version + detected here is guaranteed restorable by CI -- eliminating a nuget.org-vs-feed mirror race. + +## Family enumeration + +- Use `PackageSearchResource.SearchAsync("Microsoft.Agents.AI", new SearchFilter(includePrerelease: true), ...)`. + The client SDK sends `semVerLevel=2.0.0`, so preview/alpha-only packages are returned; a raw `curl` + on the `query2` endpoint without that parameter silently omits them. +- Page by returned count (`page.Count < take`) -- Azure DevOps always reports `totalHits: "0"`. +- Union the search results with a curated seed id list so a search hiccup never drops a known package; + exclude `Microsoft.Agents.AI.ProjectTemplates` (the consumer's own package, not a framework dep). +- Do **not** use `AutoCompleteResource`: AzDO exposes no `SearchAutocompleteService`, so + `IdStartsWith` throws at runtime even though `GetResourceAsync` returns non-null. + +## Version selection + +- `FindPackageByIdResource.GetAllVersionsAsync(id, ...)`, then select with `NuGetVersion`/ + `VersionComparer` -- order-independent and SemVer-2 correct, so the feed's descending return order + is irrelevant. +- Exclude the anomalous `0.0.1-preview.*` entry (filter `Major > 0`). `NuGetVersion.Max()` never + selects it anyway. +- `release_version` = newest **stable** of the anchor `Microsoft.Agents.AI`. +- Per package: `at_release` = newest version whose `major.minor.patch` matches `release_version` + (any tier). This is what the template pins. Also report `latest_stable` -- note that + `Microsoft.Agents.AI.Foundry`'s newest stable (`1.5.0`) lags its `at_release` preview, so + "newest stable per package" is the wrong selector. + +## Output + +A single-element JSON array (so the orchestrator matrix fans out over one target) whose element is the +framework signal: `source_feed`, `release_version`, `release_date`, and a `packages` map of +`{ id: { tier, at_release, latest, latest_stable } }`. diff --git a/.github/skills/update-otel-genai-conventions/SKILL.md b/.github/skills/update-otel-genai-conventions/SKILL.md index 1faee1f342e..c1ae7bcfe08 100644 --- a/.github/skills/update-otel-genai-conventions/SKILL.md +++ b/.github/skills/update-otel-genai-conventions/SKILL.md @@ -17,7 +17,7 @@ agent: 'agent' tools: ['github/*', 'sql'] --- -# Update OTel Gen-AI Conventions +# Update OTel GenAI Conventions Analyze OpenTelemetry GenAI semantic-conventions changes — PRs, changelog snapshots, date ranges, or releases — primarily from [`open-telemetry/semantic-conventions-genai`](https://github.com/open-telemetry/semantic-conventions-genai), and produce compensating updates in `dotnet/extensions`. See the [Migration Note](#migration-note) below for context, including where these conventions were previously managed. @@ -53,6 +53,12 @@ Implications for this skill: news fragments under `changelog.d/`, each named `..md` (types include `enhancement`, `bugfix`, `breaking`, `clarification`). Pin a snapshot via commit SHA / ref for reproducible audits. +- **Release tagging is not proven yet.** Until the repo ships real GenAI + releases, treat GitHub Releases plus the release tag's dereferenced commit + as the workflow baseline, but actively compare it with other durable signals + (CHANGELOG release headers, schema URLs, and observed tag naming). Report + confidence in the approach used, and surface a PR-body caution if another + release signal becomes clearly more dependable than the workflow baseline. - **GenAI version is now independent** of core semconv: it tracks its own version line. The schema URL `https://opentelemetry.io/schemas/gen-ai/X.Y.Z` is intended to carry the @@ -175,6 +181,43 @@ When PR numbers are given without a full URL, default to `semantic-conventions` repo only if the PR doesn't exist in `semantic-conventions-genai` or the user explicitly references it. +## Release Tagging Signal Analysis + +For every audit or implementation pass that creates or refreshes an upstream-scan +tracking PR, include a concise release-tagging analysis. The current workflow baseline +is: + +1. Read GitHub's latest release for `open-telemetry/semantic-conventions-genai`. +2. Resolve that release's `tag_name` through `refs/tags`, dereferencing annotated tags + to the commit they point at. +3. Treat the release as ready for a maintained PR only when that resolved commit equals + the PR body's recorded `upstream-scan-ref`. + +Compare that baseline with all durable release signals available at the scanned ref: + +- GitHub Releases and the resolved release tag commit. +- `CHANGELOG.md` release headers produced by Towncrier. +- Published schema URLs such as `opentelemetry.io/schemas/gen-ai/X.Y.Z`, once they + exist. +- Stable tag naming patterns, if the repo begins publishing tags without GitHub + Releases. + +Report confidence using these meanings: + +- `high`: the signal is published and tied to an exact commit or schema version that + matches the audited GenAI conventions. +- `medium`: the signal identifies a version, but the exact commit relationship is not + proven. +- `low`: the signal is inferred from non-release data such as fragments, core semconv + dependency versions, or naming guesses. +- `none`: no published GenAI release signal exists. + +If a different release signal becomes clearly dependable and conflicts with the +workflow baseline, keep the workflow behavior unchanged for that run, but add the +PR-description `> [!CAUTION]` block described in +[references/pr-description.md](references/pr-description.md). The block must name the +signal found and recommend the specific skill and/or workflow change needed. + ### In-scope areas The `semantic-conventions-genai` repo hosts conventions for several areas, all of which this skill @@ -207,7 +250,7 @@ Search using the requested release version, CHANGELOG ref, date range, or upstre Do not silently ignore search failures. If GitHub search/listing is unavailable, report the problem and ask the user whether to proceed without the preflight. -A standing **upstream-scan tracking PR** (one carrying the `otel-genai-tracking` state block) is the exception: it is the durable scan record, not a blocking duplicate. When the preflight surfaces it, refresh it per **Refreshing the tracking PR** in [references/pr-description.md](references/pr-description.md#refreshing-the-tracking-pr) instead of stopping. +A standing **upstream-scan tracking PR** (one carrying the `# meai-otel-genai-worker:state:begin` block) is the exception: it is the durable scan record, not a blocking duplicate. When the preflight surfaces it, continue rather than stopping -- the maintaining workflow owns how that PR is created and incremented. ### Analyzing the Release / PRs @@ -221,7 +264,7 @@ For Step 4, read the source files listed in [references/file-inventory.md](refer ### PR Title and Description Guidance -When creating or updating a PR after implementing GenAI semantic-conventions changes (from either repo), follow [references/pr-description.md](references/pr-description.md) for the title format and the changes-table shape. For a recurring **upstream-scan tracking PR** (the kind carrying the `otel-genai-tracking` state block), that reference also defines the full body template -- the implemented-changes table, the merged and in-flight applicability tables, and, at the very bottom, the machine-readable tracking state block (the body ends there). The refresh procedure for that PR lives in the skill itself, not in the PR body. +When creating or updating a PR after implementing GenAI semantic-conventions changes (from either repo), follow [references/pr-description.md](references/pr-description.md) for the title format and the changes-table shape. For a recurring **upstream-scan tracking PR** (the kind carrying the `# meai-otel-genai-worker:state:begin` block), that reference also defines the full body template -- the implemented-changes table, the merged and in-flight applicability tables, and, at the very bottom, the machine-readable tracking state block (the body ends there). --- @@ -234,6 +277,7 @@ Audit the current gen-ai semantic conventions implementation against the latest 3. **Check for version drift**: Verify every file with a gen-ai semantic conventions version reference uses the same version. Use the search command from [references/file-inventory.md](references/file-inventory.md#version-references). If files reference different versions, flag that as a critical gap requiring investigation. 4. **Fetch the latest convention spec**: Read the current conventions from the source of truth in [`open-telemetry/semantic-conventions-genai`](https://github.com/open-telemetry/semantic-conventions-genai): `docs/gen-ai/` for human-readable docs (for example `docs/gen-ai/gen-ai-spans.md`, `docs/gen-ai/openai.md`) and `model//` for the YAML registry (`gen-ai`, `mcp`, `openai`, `aws-bedrock`). Note the published page at `https://opentelemetry.io/docs/specs/semconv/gen-ai/` is currently a "Moved" stub and no longer renders the spec. There is no `schema-snapshot/` directory, and the schema URL (`opentelemetry.io/schemas/gen-ai/X.Y.Z`) is not published yet (the repo `README.md` `## Schema URL` section is `TODO`). The repo's `versions.env` holds only the core semconv dependency (`SEMCONV_VERSION`) and the Weaver version — not a GenAI version — so do not treat `SEMCONV_VERSION` as the GenAI version. Until a GenAI release or schema URL exists, there is no published GenAI version number; identify the update by its `changelog.d/` fragment snapshot (commit SHA / ref / date). The GenAI convention version is independent of core semconv. Until releases exist in `semantic-conventions-genai`, use the latest `changelog.d/` news fragments or recently merged PRs as the "latest release notes" equivalent. + Also complete the **Release Tagging Signal Analysis** above so any PR description states release-signal confidence and warns when a different dependable release signal appears. 5. **Read all current source files** listed in [references/file-inventory.md](references/file-inventory.md) to understand what is actually implemented 6. **Cross-reference**: For each attribute, metric, event, and operation name defined in the conventions: - Is the constant defined in `OpenTelemetryConsts.cs`? @@ -353,7 +397,7 @@ Critical knowledge from past PR reviews that should inform all modes: - **No CHANGELOGs**: This repository no longer maintains per-library CHANGELOG.md files. Do NOT create or update any CHANGELOG files. - **Source-generated JSON**: Adding new OTel part types requires: (1) new inner class, (2) `[JsonSerializable]` registration on `OtelContext`, (3) switch case in `SerializeChatMessages()`. - **LoggerMessage text**: When using `[LoggerMessage]`, the message text should match the OTel event name for console logger readability. -- **No orphan constants**: Never add a constant to `OpenTelemetryConsts.cs` unless the same PR also adds at least one emission site for it. If the convention defines an attribute that no current client populates, classify the change as 🟢 *Constant not yet emitted* and defer the constant — do not add it ahead of emission. Verify with `grep -rn NewConstantName src/Libraries/Microsoft.Extensions.AI/` before submitting. +- **No orphan constants**: Never add a constant to `OpenTelemetryConsts.cs` unless the same PR also adds at least one emission site for it. If the convention defines an attribute that no current client populates, classify the change as 🟢 *Constant not yet emitted* and defer the constant — do not add it ahead of emission. Verify with `grep -rn NewConstantName src/Libraries/Microsoft.Extensions.AI/` before submitting. This defer rule applies **only** to brand-new attributes/metrics that have no emission site. A change to a convention item the code **already emits** — a type/unit change (e.g. `gen_ai.request.top_k` `double` → `int`), a requiredness/scope change, a rename, a sampling-relevance change, or a new well-known value for an already-emitted attribute — is actionable and must be applied in the same pass, not deferred. Deferral marks individual constants; it is never a reason to skip the overall update. - **Area-aware constants**: Pick the nested class in `OpenTelemetryConsts.cs` based on the upstream area: `GenAI.*` for `gen-ai/*`, `MCP.*` for `mcp/*`. Provider-specific attributes (`openai.*`, `anthropic.*`, `aws-bedrock.*`, `azure-ai-inference.*`) generally belong in the **provider package's** constants file, not in `Microsoft.Extensions.AI/OpenTelemetryConsts.cs`. See [references/implementation-patterns.md §Area placement guidance](references/implementation-patterns.md#area-placement-guidance). ## Validation diff --git a/.github/skills/update-otel-genai-conventions/references/pr-description.md b/.github/skills/update-otel-genai-conventions/references/pr-description.md index fe8c40e7d62..83643430cb4 100644 --- a/.github/skills/update-otel-genai-conventions/references/pr-description.md +++ b/.github/skills/update-otel-genai-conventions/references/pr-description.md @@ -46,7 +46,7 @@ Keep release-specific findings in the PR description or implementation summary; ## Upstream-scan tracking PR body template -A recurring **upstream-scan tracking PR** records the state of the last upstream scan and lists every merged `Unreleased` change and every open upstream PR with its applicability to this repo. This skill is responsible for producing that PR's full title and body. Assemble the body in the order below, and **keep the machine-readable tracking state and the refresh instructions at the very bottom** so the human-facing content (what shipped, then the applicability tables) leads. +A recurring **upstream-scan tracking PR** records the state of the last upstream scan and lists every merged `Unreleased` change and every open upstream PR with its applicability to this repo. This skill is responsible for producing that PR's full title and body. Assemble the body in the order below, and **keep the machine-readable tracking state at the very bottom** so the human-facing content (what shipped, then the applicability tables) leads. ### 1. Status note (optional) @@ -58,7 +58,35 @@ While `semantic-conventions-genai` is unreleased, lead with a short blockquote n > Both conventions are **Development** stability and **unreleased**. Merge once they ship in a tagged release. ``` -### 2. What this PR implements +### 2. Release tagging confidence + +Include a short release-signal statement near the top of every upstream-scan tracking +PR body. Use the workflow's setup signal as the baseline: GitHub latest release +`tag_name`, resolved through `refs/tags`, with annotated tags dereferenced to their +commit. State the confidence from the skill's release-tagging analysis: + +```markdown +Release tagging confidence: `none` -- no published GenAI release signal exists yet. +``` + +or: + +```markdown +Release tagging confidence: `high` -- GitHub latest release `vX.Y.Z` resolves to commit ``. +``` + +If the analysis finds a clearly dependable release signal that differs from the +workflow baseline, include this block before the tracking tables: + +```markdown +> [!CAUTION] +> Release tagging signal may have changed: . +> Suggested follow-up: update to use instead of . +``` + +Omit the caution block when no alternate dependable signal was found. + +### 3. What this PR implements A compact table of the convention changes this PR actually implements, with the upstream PR link and the compensating change, followed by a one-line **Validation** summary (build TFMs + warning count, test counts, public-API-surface impact, and the doc-comment version reference state): @@ -72,7 +100,7 @@ A compact table of the convention changes this PR actually implements, with the Validation: build clean (net8.0/net9.0/net10.0, 0 warnings); N tests pass. No public API surface change; doc-comment version reference left at `vX.Y`. ``` -### 3. Upstream scan tracking tables +### 4. Upstream scan tracking tables Two tables -- merged `Unreleased` changes and in-flight open PRs -- using **one consistent column set** so they read the same way: @@ -90,7 +118,7 @@ Legend: 🔴 implemented here · ✅ already aligned · 🟡 watch/deferred · ### In-flight upstream changes (open PRs) -- applicability if merged -Filter: open PRs proposing convention changes (exclude pure dependency / CI / chore PRs; list the excluded numbers). +List **every** open PR in `open-telemetry/semantic-conventions-genai`, each assessed for applicability to this repo using the same columns and indicators as the merged table (dependency / CI / chore PRs are 🟢 *no convention impact*). If there are **no** open PRs, replace the table with the single line `No open PRs.` | Upstream PR | Area | Change | Applicability | Status | |---|---|---|:---:|---| @@ -110,32 +138,22 @@ Applicability legend (symbol-only in the Applicability column): - 🟡 watch / deferred - 🟢 not applicable (no client / docs-only / other repo) -### 4. Tracking state -- very bottom +### 5. Tracking state -- very bottom -Place the machine-readable scan state at the **very bottom** of the body, after the applicability tables. The body **ends with this block** -- do not append a refresh procedure or any other section after it. The state lives in an HTML-comment-delimited block so the next run can parse it: +Place the machine-readable scan state at the **very bottom** of the body, after the applicability tables. The body **ends with this block** -- do not append a refresh procedure or any other section after it. The state lives in a fenced `yaml` block whose first and last lines are `# meai-otel-genai-worker:state:begin` / `# meai-otel-genai-worker:state:end` sentinel comments so the next run can locate and parse it. The next run matches these as **whole** comment lines (leading indent and a trailing CR are tolerated, but the `# ` prefix and the `:state:begin`/`:state:end` suffix must be exact), so keep each sentinel on its own line. Keep both sentinels inside the code fence as `yaml` comments: GitHub Actions safe-output processing preserves fenced content, so `yaml`-comment delimiters survive round-trips and stay parseable: ````markdown ## Tracking state - ```yaml -Upstream-Repo: open-telemetry/semantic-conventions-genai -Upstream-Scan-Ref: # optional inline note on what changed since the prior ref -Upstream-Scan-Date: -Upstream-Release: none # Unreleased; Towncrier fragments under changelog.d/ -Core-Semconv-Dependency: vX.Y.Z # versions.env SEMCONV_VERSION (core dep, NOT the GenAI version) -DotnetExtensions-Implemented-Version: vX.Y # doc-comment version reference currently in source +# meai-otel-genai-worker:state:begin +upstream-repo: open-telemetry/semantic-conventions-genai +upstream-scan-ref: # optional inline note on what changed since the prior ref +upstream-scan-date: +upstream-release: none # Unreleased; Towncrier fragments under changelog.d/ +core-semconv-dependency: vX.Y.Z # versions.env SEMCONV_VERSION (core dep, NOT the GenAI version) +dotnet-extensions-implemented-version: vX.Y # doc-comment version reference currently in source +feedback-processed-through: # watermark: run start when PR review feedback was last processed +# meai-otel-genai-worker:state:end ``` - ```` - -## Refreshing the tracking PR - -The PR body carries only the tracking state block; the refresh logic lives in **this skill**, not in the PR body. During the skill's **Existing dotnet/extensions PR Preflight**, treat the tracking PR as the **scan record** -- not a blocking duplicate -- and refresh it as follows: - -1. Invoke the skill in **Mode 1: Audit** -- the skill owns this PR's title and body. -2. Read the `otel-genai-tracking` state block; take `Upstream-Scan-Ref` as the prior scan point. -3. Per the skill's **Input Handling**, run `git log Upstream-Scan-Ref..main` on `open-telemetry/semantic-conventions-genai` and re-list the `changelog.d/` fragments and the open PRs. -4. Classify each new or changed item with the [change-classification](change-classification.md) framework against the current `Microsoft.Extensions.AI` instrumentation, and update both applicability tables. -5. Advance `Upstream-Scan-Ref` / `Upstream-Scan-Date` (and `Core-Semconv-Dependency` / `DotnetExtensions-Implemented-Version` if they moved) in the state block. -6. If the GenAI repo cut a release, follow the skill's version-reference migration (Gotchas + [file-inventory.md §Version References](file-inventory.md#version-references)) to bump and migrate the doc-comment version in lockstep. diff --git a/.github/workflows/agent-framework-orchestrator.yml b/.github/workflows/agent-framework-orchestrator.yml new file mode 100644 index 00000000000..717e86fe537 --- /dev/null +++ b/.github/workflows/agent-framework-orchestrator.yml @@ -0,0 +1,99 @@ +name: Agent Framework Template Orchestrator + +# Orchestration only: resolve the newest coherent Microsoft Agent Framework release on the +# dnceng "dotnet-public" NuGet feed and dispatch the Agent Framework Template Worker for it. +# Deterministic discovery (no agent) via a C# file-based app, so this is a plain workflow. The +# worker is invoked as a reusable workflow (workflow_call) so it runs in this run's context and +# inherits the orchestrator's actor, satisfying the worker's gh-aw activation role check. (A +# GITHUB_TOKEN workflow_dispatch would run as github-actions[bot], which has no repo role and +# fails activation.) + +on: + schedule: + - cron: "40 9 * * *" # daily, ~09:40 UTC + workflow_dispatch: + inputs: + dry_run: + description: "Dry-Run: compute and print the target without dispatching the worker." + required: false + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: agent-framework-orchestrator + cancel-in-progress: false + +jobs: + discover: + name: Discover the Agent Framework release + # Only run on a schedule for the canonical (non-fork) repository; allow manual dispatch + # anywhere (e.g. for testing in a fork). + if: ${{ github.event_name == 'workflow_dispatch' || !github.event.repository.fork }} + runs-on: ubuntu-latest + outputs: + targets: ${{ steps.discover.outputs.targets }} + count: ${{ steps.discover.outputs.count }} + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Setup .NET + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 + with: + dotnet-version: "10.0.x" + - name: Discover the newest Agent Framework release from the NuGet feed + id: discover + run: | + set -euo pipefail + # The C# app writes the JSON array to this file so the "dotnet run" + # build/restore output on stdout never contaminates the captured value. + out="${RUNNER_TEMP}/agent-framework-targets.json" + # ImportDirectoryBuild{Props,Targets}=false isolates this file-based app from the repo's + # Directory.Build.props (which injects analyzer PackageReferences that break the standalone + # build). These must be command-line globals -- a #:property lands too late to skip the import. + dotnet run .github/scripts/agent-framework-discover.cs \ + --property:ImportDirectoryBuildProps=false --property:ImportDirectoryBuildTargets=false \ + -- "$out" + targets="$(cat "$out")" + count="$(jq 'length' <<<"$targets")" + echo "Discovered ${count} target(s):" + jq '.' <<<"$targets" + { + echo "targets<<__EOF__"; jq -c '.' <<<"$targets"; echo "__EOF__" + echo "count=${count}" + } >> "$GITHUB_OUTPUT" + { + echo "## Discovered ${count} Agent Framework release target(s)" + echo '```json'; jq '.' <<<"$targets"; echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + maintain: + name: Maintain the aiagent-webapi template + needs: discover + if: ${{ needs.discover.outputs.count != '0' && !inputs.dry_run }} + permissions: + contents: write + pull-requests: write + issues: write + # The worker's compiled reusable workflow requests discussions: write on its gh-aw-generated + # jobs (the compiler grants the union of all safe-output scopes, even ones we don't use). A + # reusable workflow can't exceed the caller's permissions, so this must be granted here even + # though we never touch discussions. + discussions: write + actions: read + strategy: + fail-fast: false + # The worker shares one concurrency group (cancel-in-progress false), so distinct targets + # serialize regardless of this value. Dispatch one at a time so a queued target is never + # dropped by concurrency's pending-run-replacement rule. + max-parallel: 1 + matrix: + target: ${{ fromJSON(needs.discover.outputs.targets) }} + # Invoke the worker via workflow_call so it runs in this run's context and inherits the + # orchestrator's actor, satisfying the worker's gh-aw role check. + uses: ./.github/workflows/agent-framework-worker.lock.yml + with: + target: ${{ toJSON(matrix.target) }} + secrets: inherit diff --git a/.github/workflows/agent-framework-worker.lock.yml b/.github/workflows/agent-framework-worker.lock.yml new file mode 100644 index 00000000000..67d77974df5 --- /dev/null +++ b/.github/workflows/agent-framework-worker.lock.yml @@ -0,0 +1,2025 @@ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7b0eec47833a95eab622be3be40e493cccc98d9e7de177b4a188335d54213316","body_hash":"87913e347468a71a1b8860bec2f35eb4c4e1a647e25650ffee7bc067c97796e5","compiler_version":"v0.81.6","strict":true,"agent_id":"copilot","detection_agent_id":"copilot","engine_versions":{"copilot":"1.0.65"},"agent_image_runner":"ubuntu-latest"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-dotnet","sha":"9a946fdbd5fb07b82b2f5a4466058b876ab72bb2","version":"v5.3.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba6380cc6e5be5d21677bebe04d52fb48e3abec7","version":"v0.81.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.11","digest":"sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.11@sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11","digest":"sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11@sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.11"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.11","digest":"sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.11@sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.30","digest":"sha256:35625d1a2269b1238606078c879f59a91cffc4ac33eb54bf39c6418822c1a8be","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.30@sha256:35625d1a2269b1238606078c879f59a91cffc4ac33eb54bf39c6418822c1a8be"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.4.0","digest":"sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036","pinned_image":"ghcr.io/github/github-mcp-server:v1.4.0@sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036"}]} +# This file was automatically generated by gh-aw (v0.81.6). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Maintain a single draft pull request that bumps the aiagent-webapi project template's Microsoft.Agents.AI package versions in eng/packages/ProjectTemplates.props to the newest coherent Agent Framework release on the dotnet-public feed, CI-validated by restoring, building, and packing the template package through the repo's Arcade build and running its snapshot + execution tests. Invoked per-target by the Agent Framework Template Orchestrator (workflow_call) or manually (workflow_dispatch). +# +# Resolved workflow manifest: +# Imports: +# - shared/pat_pool.md +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - COPILOT_PAT_0 +# - COPILOT_PAT_1 +# - COPILOT_PAT_2 +# - COPILOT_PAT_3 +# - COPILOT_PAT_4 +# - COPILOT_PAT_5 +# - COPILOT_PAT_6 +# - COPILOT_PAT_7 +# - COPILOT_PAT_8 +# - COPILOT_PAT_9 +# - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.27.11@sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11@sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d +# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.11 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.11@sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d +# - ghcr.io/github/gh-aw-mcpg:v0.3.30@sha256:35625d1a2269b1238606078c879f59a91cffc4ac33eb54bf39c6418822c1a8be +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.4.0@sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036 + +name: "Agent Framework Template Worker" +on: + workflow_call: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + target: + description: Framework release signal (JSON) from agent-framework-discover.cs. + required: true + type: string + outputs: + comment_id: + description: ID of the first added comment + value: ${{ jobs.safe_outputs.outputs.comment_id }} + comment_url: + description: URL of the first added comment + value: ${{ jobs.safe_outputs.outputs.comment_url }} + created_pr_number: + description: Number of the first created pull request + value: ${{ jobs.safe_outputs.outputs.created_pr_number }} + created_pr_url: + description: URL of the first created pull request + value: ${{ jobs.safe_outputs.outputs.created_pr_url }} + push_commit_sha: + description: SHA of the pushed commit + value: ${{ jobs.safe_outputs.outputs.push_commit_sha }} + push_commit_url: + description: URL of the pushed commit + value: ${{ jobs.safe_outputs.outputs.push_commit_url }} + secrets: + COPILOT_GITHUB_TOKEN: + required: false + COPILOT_PAT_0: + required: false + COPILOT_PAT_1: + required: false + COPILOT_PAT_2: + required: false + COPILOT_PAT_3: + required: false + COPILOT_PAT_4: + required: false + COPILOT_PAT_5: + required: false + COPILOT_PAT_6: + required: false + COPILOT_PAT_7: + required: false + COPILOT_PAT_8: + required: false + COPILOT_PAT_9: + required: false + GH_AW_CI_TRIGGER_TOKEN: + required: false + GH_AW_GITHUB_MCP_SERVER_TOKEN: + required: false + GH_AW_GITHUB_TOKEN: + required: false + workflow_dispatch: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + target: + description: Framework release signal (JSON). Leave empty to resolve from the feed. + required: false + type: string + +permissions: {} + +concurrency: + cancel-in-progress: false + group: agent-framework-worker + +run-name: "Agent Framework Template Worker" + +jobs: + activation: + needs: + - pat_pool + - pre_activation + if: > + needs.pre_activation.outputs.activated == 'true' && (github.event_name == 'workflow_dispatch' || !github.event.repository.fork) + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + outputs: + artifact_prefix: ${{ steps.artifact-prefix.outputs.prefix }} + comment_id: "" + comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} + daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + target_checkout_ref: ${{ steps.resolve-host-repo.outputs.target_checkout_ref }} + target_ref: ${{ steps.resolve-host-repo.outputs.target_ref }} + target_repo: ${{ steps.resolve-host-repo.outputs.target_repo }} + target_repo_name: ${{ steps.resolve-host-repo.outputs.target_repo_name }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/agent-framework-worker.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Resolve host repo for activation checkout + id: resolve-host-repo + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + JOB_WORKFLOW_REPOSITORY: ${{ job.workflow_repository }} + JOB_WORKFLOW_SHA: ${{ job.workflow_sha }} + JOB_WORKFLOW_REF: ${{ job.workflow_ref }} + JOB_WORKFLOW_FILE_PATH: ${{ job.workflow_file_path }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs'); + await main(); + - name: Compute artifact prefix + id: artifact-prefix + env: + INPUTS_JSON: ${{ toJSON(inputs) }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/compute_artifact_prefix.sh" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AGENT_VERSION: "1.0.65" + GH_AW_INFO_CLI_VERSION: "v0.81.6" + GH_AW_INFO_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + GH_AW_INFO_TARGET_REPO: ${{ steps.resolve-host-repo.outputs.target_repo }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: agentic-workflow-usage-agentframeworkworker-${{ github.run_id }} + restore-keys: agentic-workflow-usage-agentframeworkworker- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_WORKFLOW_ID: "agent-framework-worker" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + await main(); + - name: Print cross-repo setup guidance + if: failure() && steps.resolve-host-repo.outputs.target_repo != github.repository + run: | + echo "::error::COPILOT_GITHUB_TOKEN must be configured in the CALLER repository's secrets." + echo "::error::For cross-repo workflow_call, secrets must be set in the repository that triggers the workflow." + echo "::error::See: https://github.github.com/gh-aw/patterns/central-repo-ops/#cross-repo-setup" + - name: Checkout .github and .agents folders + if: steps.resolve-host-repo.outputs.target_repo == github.repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + repository: ${{ steps.resolve-host-repo.outputs.target_repo }} + ref: ${{ steps.resolve-host-repo.outputs.target_checkout_ref }} + sparse-checkout: | + .github + .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "agent-framework-worker.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.81.6" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Log runtime features + if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_a76200ded5088bd0_EOF' + + GH_AW_PROMPT_a76200ded5088bd0_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_a76200ded5088bd0_EOF' + + Tools: add_comment, create_pull_request, update_pull_request, mark_pull_request_as_ready_for_review, push_to_pull_request_branch, missing_tool, missing_data, noop + GH_AW_PROMPT_a76200ded5088bd0_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" + cat << 'GH_AW_PROMPT_a76200ded5088bd0_EOF' + + GH_AW_PROMPT_a76200ded5088bd0_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_a76200ded5088bd0_EOF' + + The following GitHub context information is available for this workflow: + {{#if github.actor}} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if github.repository}} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if github.workspace}} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ + {{/if}} + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ + {{/if}} + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ + {{/if}} + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ + {{/if}} + {{#if github.run_id}} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + - **checkouts**: The following repositories have been checked out and are available in the workspace: + - repo `__GH_AW_GITHUB_REPOSITORY__` → `$GITHUB_WORKSPACE` (cwd) [full history, all branches available as remote-tracking refs] [additional refs fetched: *] + - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). + - **Warning: No git credentials are available to the agent.** Credentials are + intentionally removed after the checkout step for security. This means any git + operation that needs to authenticate to the remote will fail. In private repositories, that includes: + - `git fetch`, `git pull`, `git clone`, and `git push` (direct push, not via safe-output tools) + - Checking out or switching to a remote branch that is not already fetched + - Deepening a shallow clone (`git fetch --unshallow`) + - On-demand blob fetches in partial/blobless clones (operations on files not in the initial checkout) + Do NOT attempt to configure credentials, run `git credential fill`, or modify `.gitconfig` — + authentication will not succeed. If you encounter credential prompts or authentication errors, + stop immediately and report the limitation rather than spending turns trying to work around it. + + + GH_AW_PROMPT_a76200ded5088bd0_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_a76200ded5088bd0_EOF' + + {{#runtime-import .github/workflows/agent-framework-worker.md}} + GH_AW_PROMPT_a76200ded5088bd0_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ steps.artifact-prefix.outputs.prefix }}activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/models.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: + - activation + - pat_pool + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' + runs-on: ubuntu-latest + environment: copilot-pat-pool + permissions: + actions: read + contents: read + issues: read + pull-requests: read + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + GH_AW_WORKFLOW_ID_SANITIZED: agentframeworkworker + outputs: + agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} + artifact_prefix: ${{ needs.activation.outputs.artifact_prefix }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/agent-framework-worker.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + fetch-depth: 0 + - name: Fetch additional refs + env: + GH_AW_FETCH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + header=$(printf "x-access-token:%s" "${GH_AW_FETCH_TOKEN}" | base64 -w 0) + git -c "http.extraheader=Authorization: Basic ${header}" fetch origin '+refs/heads/*:refs/remotes/origin/*' + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Start DIFC Proxy + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_SERVER_URL: ${{ github.server_url }} + DIFC_PROXY_POLICY: '{"allow-only":{"disapproval-reactions":["THUMBS_DOWN","CONFUSED"],"endorsement-reactions":["THUMBS_UP","HEART"],"min-integrity":"approved","repos":"all"}}' + DIFC_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.30' + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/start_difc_proxy.sh" + - name: Setup .NET + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 + env: + GH_HOST: ${{ env.GH_HOST || 'github.com' }} + GH_REPO: ${{ github.repository }} + GITHUB_API_URL: https://localhost:18443/api/v3 + GITHUB_GRAPHQL_URL: https://localhost:18443/api/graphql + NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt + with: + dotnet-version: 10.0.x + - name: Set up the run context and CI-validate the bump + run: bash .github/scripts/agent-framework-worker-setup.sh + env: + GH_HOST: ${{ env.GH_HOST || 'github.com' }} + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + GITHUB_API_URL: https://localhost:18443/api/v3 + GITHUB_GRAPHQL_URL: https://localhost:18443/api/graphql + NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt + TARGET_JSON: ${{ inputs.target }} + - name: Configure Git credentials + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.65 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.11 + - name: Parse integrity filter lists + id: parse-guard-vars + env: + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" + - name: Stop DIFC Proxy + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_difc_proxy.sh" + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.11@sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11@sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.11 ghcr.io/github/gh-aw-firewall/squid:0.27.11@sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d ghcr.io/github/gh-aw-mcpg:v0.3.30@sha256:35625d1a2269b1238606078c879f59a91cffc4ac33eb54bf39c6418822c1a8be ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.4.0@sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036 + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_519e34b28048db20_EOF' + {"add_comment":{"max":1,"required_labels":["automation","area-ai-templates"],"target":"*"},"create_pull_request":{"allowed_files":["eng/packages/ProjectTemplates.props","src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/**","src/Libraries/Microsoft.Extensions.AI*/**","test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/**"],"base_branch":"main","draft":true,"if_no_changes":"warn","labels":["automation","area-ai-templates"],"max":1,"max_patch_files":100,"max_patch_size":4096,"preserve_branch_name":true,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review"},"create_report_incomplete_issue":{},"mark_pull_request_as_ready_for_review":{"required_labels":["automation","area-ai-templates"],"target":"*"},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_to_pull_request_branch":{"allowed_files":["eng/packages/ProjectTemplates.props","src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/**","src/Libraries/Microsoft.Extensions.AI*/**","test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/**"],"if_no_changes":"ignore","max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"required_labels":["automation","area-ai-templates"],"target":"*"},"report_incomplete":{},"update_pull_request":{"allow_body":true,"allow_title":true,"max":1,"target":"*","update_branch":false}} + GH_AW_SAFE_OUTPUTS_CONFIG_519e34b28048db20_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Labels [\"automation\" \"area-ai-templates\"] will be automatically added. PRs will be created as drafts.", + "update_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be updated. Target: *." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + }, + "reply_to_id": { + "type": "string", + "maxLength": 256 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "create_pull_request": { + "defaultMax": 1, + "fields": { + "base": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "mark_pull_request_as_ready_for_review": { + "defaultMax": 1, + "fields": { + "pull_request_number": { + "issueOrPRNumber": true + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "push_to_pull_request_branch": { + "defaultMax": 1, + "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "pull_request_number": { + "issueOrPRNumber": true + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + }, + "update_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "draft": { + "type": "boolean" + }, + "operation": { + "type": "string", + "enum": [ + "replace", + "append", + "prepend" + ] + }, + "pull_request_number": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "update_branch": { + "type": "boolean" + } + }, + "customValidation": "requiresOneOf:title,body,update_branch" + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --name awmg-mcpg --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.3.30' + + mkdir -p "$HOME/.copilot" + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_4470e57135d8a598_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "safeoutputs": { + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", + "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", + "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_4470e57135d8a598_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Start CLI Proxy + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_SERVER_URL: ${{ github.server_url }} + CLI_PROXY_POLICY: '{"allow-only":{"disapproval-reactions":["THUMBS_DOWN","CONFUSED"],"endorsement-reactions":["THUMBS_UP","HEART"],"min-integrity":"approved","repos":"all"}}' + CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.30' + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 180 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.11/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.vsblob.vsassets.io\",\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.nuget.org\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"azuresearch-usnc.nuget.org\",\"azuresearch-ussc.nuget.org\",\"builds.dotnet.microsoft.com\",\"ci.dot.net\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"dc.services.visualstudio.com\",\"dist.nuget.org\",\"dot.net\",\"dotnet.microsoft.com\",\"dotnetcli.blob.core.windows.net\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"nuget.org\",\"nuget.pkg.github.com\",\"nugetregistryv2prod.blob.core.windows.net\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"oneocsp.microsoft.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"pkgs.dev.azure.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\",\"www.microsoft.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.5\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.11,squid=sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d,agent=sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7,api-proxy=sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH=/tmp/gh-aw GH_AW_CHROOT_IDENTITY_HOME=/tmp/gh-aw/home node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2086 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_LLM_PROVIDER: github + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_TIMEOUT_MINUTES: 180 + GH_AW_VERSION: v0.81.6 + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }} + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + RUNNER_TEMP: ${{ runner.temp }} + TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} + - name: Stop CLI Proxy + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_cli_proxy.sh" + - name: Detect agent errors + if: always() + id: detect-agent-errors + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + - name: Configure Git credentials + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Validate agent output identity + run: "set -euo pipefail\nout=/tmp/gh-aw/agent_output.json\nif [ ! -f \"$out\" ]; then\n echo \"::notice::No agent output to validate\"; exit 0\nfi\nidx=$(jq -r '(.items // []) | to_entries[]\n | select(.value.type==\"create_pull_request\"\n or (.value.type==\"update_pull_request\" and ((.value.operation // \"replace\")==\"replace\")))\n | select((.value.body // \"\") != \"\")\n | .key' \"$out\" 2>/dev/null || true)\nif [ -z \"$idx\" ]; then\n echo \"::notice::No full-body PR-writing items -- nothing to validate\"; exit 0\nfi\nrc=0\nwhile IFS= read -r i; do\n [ -n \"$i\" ] || continue\n typ=$(jq -r \".items[$i].type\" \"$out\")\n body=$(jq -r \".items[$i].body\" \"$out\")\n miss=\"\"\n printf '%s' \"$body\" | grep -q '# agent-framework-template:state:begin' || miss=\"$miss begin-marker\"\n printf '%s' \"$body\" | grep -q '# agent-framework-template:state:end' || miss=\"$miss end-marker\"\n printf '%s' \"$body\" | grep -Eq 'feedback-processed-through:[[:space:]]*[0-9]' || miss=\"$miss feedback-processed-through\"\n if [ -n \"$miss\" ]; then\n echo \"::error::$typ item #$i is missing required tracking identity:$miss\"\n rc=1\n fi\ndone <<< \"$idx\"\n[ \"$rc\" -eq 0 ] && echo \"Agent output identity validated.\"\nexit $rc\n" + + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - pat_pool + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + runs-on: ubuntu-slim + environment: copilot-pat-pool + permissions: + contents: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-agent-framework-worker" + cancel-in-progress: false + queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/agent-framework-worker.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Collect usage artifact files + if: always() + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection + echo "Usage artifact source file status:" + for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do + [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" + done + [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true + [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true + [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true + [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true + [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true + [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true + [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl + [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl + mkdir -p /tmp/gh-aw/usage/activity + node ${{ runner.temp }}/gh-aw/actions/generate_usage_activity_summary.cjs + find /tmp/gh-aw/usage -type f -print | sort + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: agentic-workflow-usage-agentframeworkworker-${{ github.run_id }} + restore-keys: agentic-workflow-usage-agentframeworkworker- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: agentic-workflow-usage-agentframeworkworker-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/agent-framework-worker.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "agent-framework-worker" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/agent-framework-worker.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/agent-framework-worker.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/agent-framework-worker.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/agent-framework-worker.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "agent-framework-worker" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "false" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "180" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: always() && needs.agent.result != 'skipped' + runs-on: ubuntu-latest + environment: copilot-pat-pool + permissions: + contents: read + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/agent-framework-worker.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.agent.outputs.artifact_prefix }}agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.11@sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11@sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.11 ghcr.io/github/gh-aw-firewall/squid:0.27.11@sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f "$HOME/.copilot/mcp-config.json" + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + rm -f /tmp/gh-aw/agent_usage.json + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then + echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." + fi + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "Agent Framework Template Worker" + WORKFLOW_DESCRIPTION: "Maintain a single draft pull request that bumps the aiagent-webapi project template's Microsoft.Agents.AI package versions in eng/packages/ProjectTemplates.props to the newest coherent Agent Framework release on the dotnet-public feed, CI-validated by restoring, building, and packing the template package through the repo's Arcade build and running its snapshot + execution tests. Invoked per-target by the Agent Framework Template Orchestrator (workflow_call) or manually (workflow_dispatch)." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.65 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.11 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.11/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5},\"container\":{\"imageTag\":\"0.27.11,squid=sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d,agent=sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7,api-proxy=sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + _GH_AW_CHROOT_JSON=$(jq -c --arg src /tmp/gh-aw --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home /tmp/gh-aw/home '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "/tmp/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2086 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_0 || secrets.COPILOT_PAT_1 || secrets.COPILOT_PAT_2 || secrets.COPILOT_PAT_3 || secrets.COPILOT_PAT_4 || secrets.COPILOT_PAT_5 || secrets.COPILOT_PAT_6 || secrets.COPILOT_PAT_7 || secrets.COPILOT_PAT_8 || secrets.COPILOT_PAT_9 || secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_LLM_PROVIDER: github + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.81.6 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + RUNNER_TEMP: ${{ runner.temp }} + TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.agent.outputs.artifact_prefix }}detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + pat_pool: + needs: pre_activation + runs-on: ubuntu-slim + environment: copilot-pat-pool + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_9. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 9); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_9)" + echo "$warning_message" + exit 0 + fi + + # Select a random index. Seed only when a non-negative integer is provided, + # normalized to Bash's 0-32767 $RANDOM seed range -- assigning a non-integer to + # $RANDOM would break the arithmetic expansion below. + if printf '%s' "$RANDOM_SEED" | grep -Eq '^[0-9]+$'; then + RANDOM=$(( RANDOM_SEED % 32768 )) + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|8|9|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + + pre_activation: + if: github.event_name == 'workflow_dispatch' || !github.event.repository.fork + runs-on: ubuntu-slim + environment: copilot-pat-pool + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + matched_command: '' + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/agent-framework-worker.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); + await main(); + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + environment: copilot-pat-pool + permissions: + contents: write + issues: write + pull-requests: write + timeout-minutes: 45 + env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/agent-framework-worker" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.65" + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_WORKFLOW_ID: "agent-framework-worker" + GH_AW_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/agent-framework-worker.md" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} + comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} + push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Agent Framework Template Worker" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/agent-framework-worker.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent + path: /tmp/gh-aw/ + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: true + fetch-depth: 0 + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + - name: Fetch additional refs + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + env: + GH_AW_FETCH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + header=$(printf "x-access-token:%s" "${GH_AW_FETCH_TOKEN}" | base64 -w 0) + git -c "http.extraheader=Authorization: Basic ${header}" fetch origin '+refs/heads/*:refs/remotes/origin/*' + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_ALLOWED_DOMAINS: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"required_labels\":[\"automation\",\"area-ai-templates\"],\"target\":\"*\"},\"create_pull_request\":{\"allowed_files\":[\"eng/packages/ProjectTemplates.props\",\"src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/**\",\"src/Libraries/Microsoft.Extensions.AI*/**\",\"test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/**\"],\"base_branch\":\"main\",\"draft\":true,\"if_no_changes\":\"warn\",\"labels\":[\"automation\",\"area-ai-templates\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":4096,\"preserve_branch_name\":true,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\"},\"create_report_incomplete_issue\":{},\"mark_pull_request_as_ready_for_review\":{\"required_labels\":[\"automation\",\"area-ai-templates\"],\"target\":\"*\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"eng/packages/ProjectTemplates.props\",\"src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/**\",\"src/Libraries/Microsoft.Extensions.AI*/**\",\"test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/**\"],\"if_no_changes\":\"ignore\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"required_labels\":[\"automation\",\"area-ai-templates\"],\"target\":\"*\"},\"report_incomplete\":{},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":true,\"max\":1,\"target\":\"*\",\"update_branch\":false}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + diff --git a/.github/workflows/agent-framework-worker.md b/.github/workflows/agent-framework-worker.md new file mode 100644 index 00000000000..e91ff05b2d1 --- /dev/null +++ b/.github/workflows/agent-framework-worker.md @@ -0,0 +1,424 @@ +--- +name: "Agent Framework Template Worker" +description: >- + Maintain a single draft pull request that bumps the aiagent-webapi project template's + Microsoft.Agents.AI package versions in eng/packages/ProjectTemplates.props to the newest + coherent Agent Framework release on the dotnet-public feed, CI-validated by restoring, building, + and packing the template package through the repo's Arcade build and running its snapshot + + execution tests. Invoked per-target by the Agent Framework Template Orchestrator (workflow_call) + or manually (workflow_dispatch). + +# Only run on a schedule for the canonical (non-fork) repository; allow manual dispatch anywhere. +if: ${{ github.event_name == 'workflow_dispatch' || !github.event.repository.fork }} + +permissions: + actions: read + contents: read + pull-requests: read + issues: read + +safe-outputs: + # Threat detection screens the untrusted reviewer feedback this agent incorporates. It runs in a + # separate gh-aw job that authenticates via the coalescing token below. + threat-detection: + engine: + id: copilot + env: + # Workaround for github/gh-aw#43917: the detection job's `needs` omit `pat_pool`, so the + # main engine's case(needs.pat_pool...) token can't resolve here. Authenticate by coalescing + # the pool PAT secrets directly -- the first non-empty one wins. + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_0 || secrets.COPILOT_PAT_1 || secrets.COPILOT_PAT_2 || secrets.COPILOT_PAT_3 || secrets.COPILOT_PAT_4 || secrets.COPILOT_PAT_5 || secrets.COPILOT_PAT_6 || secrets.COPILOT_PAT_7 || secrets.COPILOT_PAT_8 || secrets.COPILOT_PAT_9 || secrets.COPILOT_GITHUB_TOKEN }} + # The maintained PR is created, updated, and pushed to entirely through native gh-aw safe outputs + # -- the agent job stays read-only and every write runs in a separate, permission-controlled job. + # allowed-files restricts every write to the template's version file and template tree. + create-pull-request: + draft: true + labels: [automation, area-ai-templates] + base-branch: main + preserve-branch-name: true + if-no-changes: "warn" + allowed-files: + - "eng/packages/ProjectTemplates.props" + - "src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/**" + - "src/Libraries/Microsoft.Extensions.AI*/**" + - "test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/**" + push-to-pull-request-branch: + target: "*" + required-labels: [automation, area-ai-templates] + if-no-changes: "ignore" + allowed-files: + - "eng/packages/ProjectTemplates.props" + - "src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/**" + - "src/Libraries/Microsoft.Extensions.AI*/**" + - "test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/**" + update-pull-request: + target: "*" + required-labels: [automation, area-ai-templates] + title: true + body: true + add-comment: + target: "*" + required-labels: [automation, area-ai-templates] + max: 1 + mark-pull-request-as-ready-for-review: + target: "*" + required-labels: [automation, area-ai-templates] + noop: + report-as-issue: false + report-failure-as-issue: false + +network: + allowed: + - defaults + - dotnet + +features: + # Let a maintainer promote an external contributor's comment to "approved" by adding an + # endorsement reaction (👍/❤️), so the agent then sees and can act on it. Requires the gh-proxy + # mode (below) to attribute the reacting user. + integrity-reactions: true + +tools: + github: + mode: gh-proxy + toolsets: [default] + # Only content at "approved" integrity or higher reaches the agent. Write-access authors + # (OWNER/MEMBER/COLLABORATOR) are approved automatically; feedback from anyone else is filtered + # out unless a maintainer endorses it. This is the single source of truth for feedback trust. + min-integrity: approved + # Full bash: the agent applies the host-validated props file, runs git to position the PR branch, + # and commits. The AWF sandbox + firewall, the read-only agent permissions, and the safe-outputs + # allow-lists are the boundaries. + bash: [":*"] + +runs-on: ubuntu-latest +timeout-minutes: 180 + +# The agent commits the version bump and checks out the maintained PR branch for incremental +# appends. fetch: ["*"] pre-fetches every branch during the credentialed host checkout so the agent +# reaches the PR branch from an already-local ref. +checkout: + fetch: ["*"] + fetch-depth: 0 + +concurrency: + group: agent-framework-worker + cancel-in-progress: false + +on: + # Invoked per-target by the orchestrator as a reusable workflow so this runs in the orchestrator's + # run context and inherits its actor, satisfying gh-aw activation's role check. + workflow_call: + inputs: + target: + description: "Framework release signal (JSON) from agent-framework-discover.cs." + required: true + type: string + # Manual dispatch for standalone testing: provide a target JSON, or leave empty to have the setup + # script run discovery itself. + workflow_dispatch: + inputs: + target: + description: "Framework release signal (JSON). Leave empty to resolve from the feed." + required: false + type: string + permissions: {} + +# Before the agent runs, deterministically prepare the run: resolve the target Agent Framework +# release, map it onto the template's package subset, CI-validate the prospective bump with a real +# restore + build, discover and classify the maintained draft PR, and compute the recommended +# lifecycle action. The agent consumes target.json instead of discovering or building anything. +steps: + - name: Setup .NET + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 + with: + dotnet-version: "10.0.x" + - name: Set up the run context and CI-validate the bump + env: + GH_TOKEN: ${{ github.token }} + TARGET_JSON: ${{ inputs.target }} + run: bash .github/scripts/agent-framework-worker-setup.sh + +# After the agent runs, verify each full-body pull-request write still carries the tracking block the +# next run reads back to resume: the state block markers and a non-empty feedback-processed-through +# watermark. A body missing them would leave the next run unable to recover its place. +post-steps: + - name: Validate agent output identity + run: | + set -euo pipefail + out=/tmp/gh-aw/agent_output.json + if [ ! -f "$out" ]; then + echo "::notice::No agent output to validate"; exit 0 + fi + idx=$(jq -r '(.items // []) | to_entries[] + | select(.value.type=="create_pull_request" + or (.value.type=="update_pull_request" and ((.value.operation // "replace")=="replace"))) + | select((.value.body // "") != "") + | .key' "$out" 2>/dev/null || true) + if [ -z "$idx" ]; then + echo "::notice::No full-body PR-writing items -- nothing to validate"; exit 0 + fi + rc=0 + while IFS= read -r i; do + [ -n "$i" ] || continue + typ=$(jq -r ".items[$i].type" "$out") + body=$(jq -r ".items[$i].body" "$out") + miss="" + printf '%s' "$body" | grep -q '# agent-framework-template:state:begin' || miss="$miss begin-marker" + printf '%s' "$body" | grep -q '# agent-framework-template:state:end' || miss="$miss end-marker" + printf '%s' "$body" | grep -Eq 'feedback-processed-through:[[:space:]]*[0-9]' || miss="$miss feedback-processed-through" + if [ -n "$miss" ]; then + echo "::error::$typ item #$i is missing required tracking identity:$miss" + rc=1 + fi + done <<< "$idx" + [ "$rc" -eq 0 ] && echo "Agent output identity validated." + exit $rc + +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# Run agentic jobs in an isolated `copilot-pat-pool` environment. +# +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - uses: shared/pat_pool.md + with: + environment: copilot-pat-pool + +environment: copilot-pat-pool + +engine: + id: copilot + env: + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} +--- + +# Agent Framework Template Worker + +## Goal + +Keep the `aiagent-webapi` project template aligned with the newest coherent Microsoft Agent +Framework release by continuously maintaining a **single draft pull request** against `main` that +bumps the Agent Framework package versions in `eng/packages/ProjectTemplates.props`. Each run +consumes the host-prepared `target.json`, positions and updates that one PR, and leaves it in a +correct, idempotent state. The automation **never merges** -- humans review and merge. + +The repository skill `update-agent-framework-template` (in `.github/skills/`) is the authority for +**how** to map the release signal onto the template's packages and format the PR. This workflow +governs **lifecycle/idempotency** (which PR to touch and what state to leave it in). + +## Step 0 -- Read the run context (authoritative) + +Read `/tmp/gh-aw/agent/target.json` (written by the setup script -- do **not** re-discover or +re-build). Key fields: + +- `release_version`, `release_date`, `source_feed` -- the target Agent Framework release. +- `desired_versions` -- the exact `{ packageId: version }` the template must pin (already mapped to + the template's subset; OpenAI tracks the core version). +- `current_version`, `main_needs_bump` -- what `main` pins today and whether it differs. +- `validated`, `build_summary` -- whether the prospective bump built through the repo's Arcade build: + restore + build + **pack** of the `Microsoft.Agents.AI.ProjectTemplates` package (the `.nupkg` the + execution tests install). The exact validated props content is at + `/tmp/gh-aw/agent/ProjectTemplates.props.bumped` and the bumped template package project at + `/tmp/gh-aw/agent/ProjectTemplates.csproj.bumped`. +- `pr`, `pr_state`, `pr_is_draft`, `pr_branch` -- the maintained PR (if any). +- `pr_recorded_version` -- the release the maintained PR already bumped to (from its tracking block). +- `classification` -- `ours` / `adopt` / `blocked` / `none` (see below). +- `has_new_feedback`, `watermark`, `run_started_at` -- review-activity wake gate and watermarks. +- `action` -- the recommended lifecycle action (`produce` or `noop`). +- `desired_branch` (`update-agent-framework-template`), `base_branch` (`main`), `props_path`. + +Classifications: +- **`ours`** -- an open `automation`+`area-ai-templates` PR on `pr_branch` carrying the + `agent-framework-template:state` block. Maintain it. +- **`adopt`** -- an open `automation`+`area-ai-templates` **draft** PR on `desired_branch` with **no** + tracking block yet (a human bootstrapped it). It passes both the label and draft gates: take it + over and write the full tracking block this run. +- **`blocked`** -- a PR occupies `desired_branch` but fails a takeover gate: it is missing the + automation labels, or it is a labeled non-draft (ready) PR a human is finalizing. + **Emit `noop` and stand down** -- a human owns it. +- **`none`** -- no open maintained PR. + +## Step 1 -- Guard rails + +- If `action` is `noop` **or** `classification` is `blocked`: emit a `noop` safe output. Write the + reason to the step summary. Do nothing else. +- If `validated` is `false`: the prospective bump did **not** build. **Never** open or update a PR + with unvalidated versions. If a maintained PR exists (`ours`/`adopt`), post a single `add-comment` + noting the build failure (quote the `build_summary`) and stop; otherwise emit `noop`. Do not emit + `create-pull-request`, `push-to-pull-request-branch`, or `update-pull-request`. + +## Step 2 -- Choose the action + +With `validated: true`, pick the path from `classification` and the PR state: + +| classification | PR state | Action | +|---|---|---| +| `none` | -- | **Fresh PR** (Step 3a) | +| `ours` / `adopt` | behind (`pr_recorded_version` != `release_version`), **draft** | **Incremental update** (Step 3b) | +| `ours` / `adopt` | caught up (`pr_recorded_version` == `release_version`), `has_new_feedback` = true | **Feedback update** (Step 3c) | +| `ours` / `adopt` | caught up, `has_new_feedback` = false | **No-op** (already handled by `action: noop`) | +| `ours` | behind, **non-draft** | **Advisory only** (Step 3d) | + +For `adopt`, additionally write the **full tracking block** into the body this run so the PR becomes +`ours`. + +## Step 3 -- Apply and publish + +Apply the changes by copying the host-prepared files into place -- never hand-edit versions, so what +you publish is exactly what was CI-validated: + +```bash +# The Agent Framework package versions (lockstep, all Microsoft.Agents.AI packages): +cp /tmp/gh-aw/agent/ProjectTemplates.props.bumped eng/packages/ProjectTemplates.props +# The template NuGet package's own version, aligned to the release (Major/Minor/Patch; label kept): +cp /tmp/gh-aw/agent/ProjectTemplates.csproj.bumped "$(jq -r '.template_pkg_proj' /tmp/gh-aw/agent/target.json)" +``` + +Stage both files together in every commit. `target.json.template_pkg_old` -> `template_pkg_new` is the +template package version change (e.g. `1.3.0-preview` -> `1.13.0-preview`); the prerelease label is +never changed by the automation. + +### 3a. Fresh PR (`classification: none`) + +The run is checked out on `main` (the base). To avoid advancing `main` or producing an empty patch, +**detach HEAD first**, then apply, commit, and create a branch ref without switching: + +```bash +git checkout --detach +cp /tmp/gh-aw/agent/ProjectTemplates.props.bumped eng/packages/ProjectTemplates.props +cp /tmp/gh-aw/agent/ProjectTemplates.csproj.bumped "$(jq -r '.template_pkg_proj' /tmp/gh-aw/agent/target.json)" +git add eng/packages/ProjectTemplates.props "$(jq -r '.template_pkg_proj' /tmp/gh-aw/agent/target.json)" +git commit -m "Update Agent Framework to " +``` + +Choose the branch name: use `desired_branch`. Check the remote first +(`git ls-remote --heads origin "$DESIRED_BRANCH"`); if it already exists, suffix with the run id +(`{desired_branch}_{GITHUB_RUN_ID}`) and note the deviation in the body. Create the ref **without +switching** (`git branch {branch} HEAD`), then emit **`create-pull-request`** with: +- branch `{branch}`, title `Update Agent Framework to `, +- the body from Step 4 (including the tracking block), draft (configured by the safe output). + +### 3b. Incremental update (behind, draft, `ours`/`adopt`) + +The PR branch already exists on the remote. Fetch and check it out, apply, and **append** a commit +(never amend/rebase/reset/squash/force-push): + +```bash +git fetch origin "$PR_BRANCH" && git checkout "$PR_BRANCH" +cp /tmp/gh-aw/agent/ProjectTemplates.props.bumped eng/packages/ProjectTemplates.props +cp /tmp/gh-aw/agent/ProjectTemplates.csproj.bumped "$(jq -r '.template_pkg_proj' /tmp/gh-aw/agent/target.json)" +git add eng/packages/ProjectTemplates.props "$(jq -r '.template_pkg_proj' /tmp/gh-aw/agent/target.json)" +git commit -m "Update Agent Framework to " +``` + +Emit **`push-to-pull-request-branch`** (target the PR), **`update-pull-request`** (full-body replace +per Step 4; for `adopt`, this adds the tracking block), and one **`add-comment`** summarizing the +delta (old -> new versions). + +### 3c. Feedback update (caught up, `has_new_feedback`) + +Address the approved reviewer feedback visible on the PR (the framework's `min-integrity: approved` +filtering already screens who is trusted). Only act on feedback **created after `watermark`**, and +only within the allowed files (`eng/packages/ProjectTemplates.props`, +`src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/**`). Reject out-of-scope requests with a +brief explanation. If the feedback yields a file change, checkout `pr_branch`, apply it, and +`push-to-pull-request-branch`. Always **refresh the body** (`update-pull-request`, advancing +`feedback-processed-through` to `run_started_at`) and post one **`add-comment`** summarizing what you +did. If nothing was actionable, still refresh the body so the watermark advances. + +### 3d. Advisory only (behind, non-draft, `ours`) + +Do **not** implement. Post one **`add-comment`** noting that a newer Agent Framework release +(`release_version`) is available and that re-marking the PR as draft lets the next scheduled run +apply it. Do not update the body. + +### 3e. Evaluate Agent Framework changes and keep consumption current (fresh / incremental paths) + +Bumping versions is not the whole job: the Agent Framework surface can change between releases, so on +the fresh and incremental paths you must also confirm the template and any **other** Agent Framework +consumption in the repo still work and follow the currently prescribed patterns. + +1. **Read the changes.** The host wrote the Agent Framework release notes spanning + `target.json.from_version` -> `release_version` to + `/tmp/gh-aw/agent/` (`target.json.af_change_count` releases). Read it + and identify API changes, removals/deprecations, renames, and newly recommended patterns. +2. **Find the consumption.** The template lives under + `src/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates/**`; other consumption is under + `src/Libraries/Microsoft.Extensions.AI*/**`. Use the skill's + guidance to locate every place these consume Agent Framework types/APIs. +3. **Apply required updates.** For any consumption affected by the changes, update it to the current + prescribed pattern -- but only within the allowed files (the template tree, the + `Microsoft.Extensions.AI*` libraries, `eng/packages/ProjectTemplates.props`, and the template + integration-test tree). Commit these alongside the version bump. Reject or skip anything outside + that scope and note it in the PR body. +4. **Ensure the tests pass.** The host already ran the template's snapshot + execution tests + against the bump (`target.json.tests_summary`). If you changed template **content** (not just + versions), the snapshot tests will no longer match -- regenerate the affected `.verified` + snapshots under + `test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/Snapshots/**` so they + reflect the intended new content, and re-run the tests. Never publish with failing tests: if you + cannot make them pass within the allowed files, emit `report_incomplete` describing why. +5. If your evaluation finds **no** required consumption changes, that is a valid outcome -- record in + the PR body that the changes were reviewed and no consumption updates were needed. + +## Step 4 -- PR body and tracking block + +Regenerate the **entire** body on every full-body write. Include, succinctly: + +1. A one-line summary: bumping the Microsoft.Agents.AI packages to Agent Framework `release_version`. +2. A table of the package version changes (old -> new) for **every** entry in `desired_versions` + (all Microsoft.Agents.AI packages move together; note each package keeps its own tier, so some + rows are `-preview.*` or `-alpha.*`). +3. The **template package version** change: `template_pkg_old` -> `template_pkg_new` (the + `Microsoft.Agents.AI.ProjectTemplates` package's own version, aligned to the release + Major/Minor/Patch with its prerelease label unchanged). +4. **CI validation**: state that `eng/packages/ProjectTemplates.props` and the template package + version were bumped and the `Microsoft.Agents.AI.ProjectTemplates` package restored, built, and + packed successfully through the repo's Arcade build (quote `build_summary`), and that the + snapshot + execution tests passed (quote `tests_summary`). +5. **Agent Framework changes reviewed**: note the `af_change_count` releases evaluated across + `from_version` -> `release_version`, and either the consumption updates you made or that no + consumption changes were required. +6. A note that the automation maintains this draft; a human reviews and merges. + +End the body with the machine-readable tracking block as the very last thing, wrapped in a `yaml` +code fence. The fence lines are **required** -- without them the `#` marker lines render as Markdown +headings instead of code. Reproduce the fence and the block exactly as shown (opening ```` ```yaml ````, +the block, closing ```` ``` ````), substituting the values: + +```yaml +# agent-framework-template:state:begin +source-feed: +agent-framework-version: +agent-framework-release-date: +feedback-processed-through: +# agent-framework-template:state:end +``` + +The post-run identity check fails the run if a full-body PR write is missing these markers or the +`feedback-processed-through` watermark, so always include them. + +## Notes + +- The automation keeps the PR a **draft**; `mark-pull-request-as-ready-for-review` is reserved for a + future code-complete signal and is not emitted in the normal flow. +- Never touch files outside the allowed list. Never edit `.github/**`, `global.json`, `nuget.config`, + `Directory.Packages.props`, or any solution file. If a build produced `bin/`/`obj/`, do not stage + them. +- Settle contradictory feedback by recency (latest timestamp wins). diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000000..87bc91eeb9d --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,26 @@ +name: "Copilot Setup Steps" + +# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set minimal permissions for setup steps + # Copilot Agent receives its own token with appropriate permissions + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - name: Install gh-aw extension + uses: github/gh-aw-actions/setup-cli@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + version: v0.81.6 diff --git a/.github/workflows/meai-otel-genai-orchestrator.yml b/.github/workflows/meai-otel-genai-orchestrator.yml new file mode 100644 index 00000000000..5fd42177665 --- /dev/null +++ b/.github/workflows/meai-otel-genai-orchestrator.yml @@ -0,0 +1,93 @@ +name: "MEAI: Otel GenAI Orchestrator" + +# Orchestration only: resolve the upstream open-telemetry/semantic-conventions-genai +# ref to scan and produce a single API/convention integration target, then run the +# MEAI Otel GenAI Worker for it. Deterministic discovery (no agent), so this is a +# plain workflow. The worker is invoked as a reusable workflow (workflow_call) so it +# runs in this run's context and inherits the orchestrator's actor, satisfying the +# worker's gh-aw activation role check. (A GITHUB_TOKEN workflow_dispatch would run +# as github-actions[bot], which has no repo role and fails activation.) + +on: + schedule: + - cron: "23 8 * * *" # daily at 08:23 UTC (00:23 PST / 01:23 PDT) + workflow_dispatch: + inputs: + upstream_ref: + description: >- + Optional git ref (branch, tag, or commit SHA) in + open-telemetry/semantic-conventions-genai to scan instead of the + default-branch HEAD. Leave empty to scan the default-branch HEAD. + required: false + type: string + dry_run: + description: "Compute and print the target without running the worker." + required: false + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: meai-otel-genai-orchestrator + cancel-in-progress: false + +jobs: + discover: + name: Discover upstream target + # Only run on a schedule for the canonical (non-fork) repository; allow manual + # dispatch anywhere (e.g. for testing in a fork). + if: ${{ !github.event.repository.fork || github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-latest + outputs: + targets: ${{ steps.discover.outputs.targets }} + count: ${{ steps.discover.outputs.count }} + env: + GH_TOKEN: ${{ github.token }} + UPSTREAM_REF: ${{ github.event.inputs.upstream_ref }} + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - name: Discover the upstream gen-ai integration target + id: discover + run: | + set -euo pipefail + targets="$(bash .github/scripts/meai-otel-genai-orchestrator-discover.sh)" + count="$(jq 'length' <<<"$targets")" + echo "Discovered ${count} target(s):" + jq '.' <<<"$targets" + { + echo "targets<<__EOF__"; jq -c '.' <<<"$targets"; echo "__EOF__" + echo "count=${count}" + } >> "$GITHUB_OUTPUT" + { + echo "## Discovered ${count} gen-ai integration target(s)" + echo '```json'; jq '.' <<<"$targets"; echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + produce: + name: Produce gen-ai integration + needs: discover + if: ${{ needs.discover.outputs.count != '0' && !inputs.dry_run }} + permissions: + contents: write + pull-requests: write + issues: write + # The worker's compiled reusable workflow requests discussions: write on its + # gh-aw-generated jobs (the compiler grants the union of all safe-output scopes, + # even ones we don't use). A reusable workflow can't exceed the caller's + # permissions, so this must be granted here even though we never touch discussions. + discussions: write + actions: read + strategy: + fail-fast: false + max-parallel: 1 + matrix: + target: ${{ fromJSON(needs.discover.outputs.targets) }} + # Call the worker as a reusable workflow so it runs in this run's context and + # inherits the orchestrator's actor, satisfying the worker's gh-aw role check. + uses: ./.github/workflows/meai-otel-genai-worker.lock.yml + with: + target: ${{ toJSON(matrix.target) }} + secrets: inherit diff --git a/.github/workflows/meai-otel-genai-worker.lock.yml b/.github/workflows/meai-otel-genai-worker.lock.yml new file mode 100644 index 00000000000..6ebe2ee4a67 --- /dev/null +++ b/.github/workflows/meai-otel-genai-worker.lock.yml @@ -0,0 +1,2017 @@ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"04d593b327b59ec9ca51033d35441ca57a97db347e6f2a1c446259f33102c608","body_hash":"47916971b47536eb2dbd5c34a5fa9d2147114e4109c2ee445bdb119b48313dda","compiler_version":"v0.81.6","strict":true,"agent_id":"copilot","detection_agent_id":"copilot","engine_versions":{"copilot":"1.0.65"},"agent_image_runner":"ubuntu-latest"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba6380cc6e5be5d21677bebe04d52fb48e3abec7","version":"v0.81.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.11","digest":"sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.11@sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11","digest":"sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11@sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.11"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.11","digest":"sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.11@sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.30","digest":"sha256:35625d1a2269b1238606078c879f59a91cffc4ac33eb54bf39c6418822c1a8be","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.30@sha256:35625d1a2269b1238606078c879f59a91cffc4ac33eb54bf39c6418822c1a8be"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.4.0","digest":"sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036","pinned_image":"ghcr.io/github/github-mcp-server:v1.4.0@sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036"}]} +# This file was automatically generated by gh-aw (v0.81.6). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Produce or refresh the single draft pull request that integrates OpenTelemetry gen-ai semantic-conventions updates into Microsoft.Extensions.AI. Invoked per-target by the MEAI Otel GenAI Orchestrator (workflow_call) or manually (workflow_dispatch). +# +# Resolved workflow manifest: +# Imports: +# - shared/pat_pool.md +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - COPILOT_PAT_0 +# - COPILOT_PAT_1 +# - COPILOT_PAT_2 +# - COPILOT_PAT_3 +# - COPILOT_PAT_4 +# - COPILOT_PAT_5 +# - COPILOT_PAT_6 +# - COPILOT_PAT_7 +# - COPILOT_PAT_8 +# - COPILOT_PAT_9 +# - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.27.11@sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11@sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d +# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.11 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.11@sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d +# - ghcr.io/github/gh-aw-mcpg:v0.3.30@sha256:35625d1a2269b1238606078c879f59a91cffc4ac33eb54bf39c6418822c1a8be +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.4.0@sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036 + +name: "MEAI: Otel GenAI Worker" +on: + workflow_call: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + target: + description: Single discovery target (JSON) from meai-otel-genai-orchestrator-discover.sh. + required: true + type: string + outputs: + comment_id: + description: ID of the first added comment + value: ${{ jobs.safe_outputs.outputs.comment_id }} + comment_url: + description: URL of the first added comment + value: ${{ jobs.safe_outputs.outputs.comment_url }} + created_pr_number: + description: Number of the first created pull request + value: ${{ jobs.safe_outputs.outputs.created_pr_number }} + created_pr_url: + description: URL of the first created pull request + value: ${{ jobs.safe_outputs.outputs.created_pr_url }} + push_commit_sha: + description: SHA of the pushed commit + value: ${{ jobs.safe_outputs.outputs.push_commit_sha }} + push_commit_url: + description: URL of the pushed commit + value: ${{ jobs.safe_outputs.outputs.push_commit_url }} + secrets: + COPILOT_GITHUB_TOKEN: + required: false + COPILOT_PAT_0: + required: false + COPILOT_PAT_1: + required: false + COPILOT_PAT_2: + required: false + COPILOT_PAT_3: + required: false + COPILOT_PAT_4: + required: false + COPILOT_PAT_5: + required: false + COPILOT_PAT_6: + required: false + COPILOT_PAT_7: + required: false + COPILOT_PAT_8: + required: false + COPILOT_PAT_9: + required: false + GH_AW_CI_TRIGGER_TOKEN: + required: false + GH_AW_GITHUB_MCP_SERVER_TOKEN: + required: false + GH_AW_GITHUB_TOKEN: + required: false + workflow_dispatch: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + target: + description: Single discovery target (JSON). Leave empty to resolve from upstream_ref. + required: false + type: string + upstream_ref: + description: Optional git ref (branch, tag, or commit SHA) in open-telemetry/semantic-conventions-genai to scan instead of the default-branch HEAD. Leave empty to scan the default-branch HEAD. + required: false + type: string + +permissions: {} + +concurrency: + cancel-in-progress: false + group: meai-otel-genai-worker + +run-name: "MEAI: Otel GenAI Worker" + +jobs: + activation: + needs: + - pat_pool + - pre_activation + if: needs.pre_activation.outputs.activated == 'true' + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + outputs: + artifact_prefix: ${{ steps.artifact-prefix.outputs.prefix }} + comment_id: "" + comment_repo: "" + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} + daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + target_checkout_ref: ${{ steps.resolve-host-repo.outputs.target_checkout_ref }} + target_ref: ${{ steps.resolve-host-repo.outputs.target_ref }} + target_repo: ${{ steps.resolve-host-repo.outputs.target_repo }} + target_repo_name: ${{ steps.resolve-host-repo.outputs.target_repo_name }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/meai-otel-genai-worker.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Resolve host repo for activation checkout + id: resolve-host-repo + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + JOB_WORKFLOW_REPOSITORY: ${{ job.workflow_repository }} + JOB_WORKFLOW_SHA: ${{ job.workflow_sha }} + JOB_WORKFLOW_REF: ${{ job.workflow_ref }} + JOB_WORKFLOW_FILE_PATH: ${{ job.workflow_file_path }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs'); + await main(); + - name: Compute artifact prefix + id: artifact-prefix + env: + INPUTS_JSON: ${{ toJSON(inputs) }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/compute_artifact_prefix.sh" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AGENT_VERSION: "1.0.65" + GH_AW_INFO_CLI_VERSION: "v0.81.6" + GH_AW_INFO_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet","github","opentelemetry.io","*.opentelemetry.io","*.azureedge.net"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + GH_AW_INFO_TARGET_REPO: ${{ steps.resolve-host-repo.outputs.target_repo }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: agentic-workflow-usage-meaiotelgenaiworker-${{ github.run_id }} + restore-keys: agentic-workflow-usage-meaiotelgenaiworker- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Restore daily AIC usage cache (artifact fallback) + id: restore-daily-aic-cache-fallback + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} + GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + await main(); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_WORKFLOW_ID: "meai-otel-genai-worker" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_HAS_SLASH_COMMAND: "false" + GH_AW_HAS_LABEL_COMMAND: "false" + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + await main(); + - name: Print cross-repo setup guidance + if: failure() && steps.resolve-host-repo.outputs.target_repo != github.repository + run: | + echo "::error::COPILOT_GITHUB_TOKEN must be configured in the CALLER repository's secrets." + echo "::error::For cross-repo workflow_call, secrets must be set in the repository that triggers the workflow." + echo "::error::See: https://github.github.com/gh-aw/patterns/central-repo-ops/#cross-repo-setup" + - name: Checkout .github and .agents folders + if: steps.resolve-host-repo.outputs.target_repo == github.repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + repository: ${{ steps.resolve-host-repo.outputs.target_repo }} + ref: ${{ steps.resolve-host-repo.outputs.target_checkout_ref }} + sparse-checkout: | + .github + .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "meai-otel-genai-worker.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.81.6" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Log runtime features + if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_cb295b5135321f64_EOF' + + GH_AW_PROMPT_cb295b5135321f64_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_cb295b5135321f64_EOF' + + Tools: add_comment, create_pull_request, update_pull_request, mark_pull_request_as_ready_for_review, push_to_pull_request_branch, missing_tool, missing_data, noop + GH_AW_PROMPT_cb295b5135321f64_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" + cat << 'GH_AW_PROMPT_cb295b5135321f64_EOF' + + GH_AW_PROMPT_cb295b5135321f64_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_cb295b5135321f64_EOF' + + The following GitHub context information is available for this workflow: + {{#if github.actor}} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if github.repository}} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if github.workspace}} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ + {{/if}} + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ + {{/if}} + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ + {{/if}} + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ + {{/if}} + {{#if github.run_id}} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + - **checkouts**: The following repositories have been checked out and are available in the workspace: + - repo `__GH_AW_GITHUB_REPOSITORY__` → `$GITHUB_WORKSPACE` (cwd) [full history, all branches available as remote-tracking refs] [additional refs fetched: *] + - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). + - **Warning: No git credentials are available to the agent.** Credentials are + intentionally removed after the checkout step for security. This means any git + operation that needs to authenticate to the remote will fail. In private repositories, that includes: + - `git fetch`, `git pull`, `git clone`, and `git push` (direct push, not via safe-output tools) + - Checking out or switching to a remote branch that is not already fetched + - Deepening a shallow clone (`git fetch --unshallow`) + - On-demand blob fetches in partial/blobless clones (operations on files not in the initial checkout) + Do NOT attempt to configure credentials, run `git credential fill`, or modify `.gitconfig` — + authentication will not succeed. If you encounter credential prompts or authentication errors, + stop immediately and report the limitation rather than spending turns trying to work around it. + + + GH_AW_PROMPT_cb295b5135321f64_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_cb295b5135321f64_EOF' + + {{#runtime-import .github/workflows/meai-otel-genai-worker.md}} + GH_AW_PROMPT_cb295b5135321f64_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ steps.artifact-prefix.outputs.prefix }}activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/models.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: + - activation + - pat_pool + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' + runs-on: ubuntu-latest + environment: copilot-pat-pool + permissions: + actions: read + contents: read + issues: read + pull-requests: read + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + GH_AW_WORKFLOW_ID_SANITIZED: meaiotelgenaiworker + outputs: + agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} + artifact_prefix: ${{ needs.activation.outputs.artifact_prefix }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/meai-otel-genai-worker.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + fetch-depth: 0 + - name: Fetch additional refs + env: + GH_AW_FETCH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + header=$(printf "x-access-token:%s" "${GH_AW_FETCH_TOKEN}" | base64 -w 0) + git -c "http.extraheader=Authorization: Basic ${header}" fetch origin '+refs/heads/*:refs/remotes/origin/*' + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Start DIFC Proxy + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_SERVER_URL: ${{ github.server_url }} + DIFC_PROXY_POLICY: '{"allow-only":{"disapproval-reactions":["THUMBS_DOWN","CONFUSED"],"endorsement-reactions":["THUMBS_UP","HEART"],"min-integrity":"approved","repos":"all"}}' + DIFC_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.30' + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/start_difc_proxy.sh" + - name: Set up the run context + run: bash .github/scripts/meai-otel-genai-worker-setup.sh + env: + GH_HOST: ${{ env.GH_HOST || 'github.com' }} + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + GITHUB_API_URL: https://localhost:18443/api/v3 + GITHUB_GRAPHQL_URL: https://localhost:18443/api/graphql + NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt + TARGET_JSON: ${{ inputs.target }} + UPSTREAM_REF: ${{ inputs.upstream_ref }} + - name: Configure Git credentials + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.65 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.11 + - name: Parse integrity filter lists + id: parse-guard-vars + env: + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" + - name: Stop DIFC Proxy + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_difc_proxy.sh" + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.11@sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11@sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.11 ghcr.io/github/gh-aw-firewall/squid:0.27.11@sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d ghcr.io/github/gh-aw-mcpg:v0.3.30@sha256:35625d1a2269b1238606078c879f59a91cffc4ac33eb54bf39c6418822c1a8be ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.4.0@sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036 + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_5718f446b1211086_EOF' + {"add_comment":{"max":1,"required_labels":["automation","area-ai"],"target":"*"},"create_pull_request":{"allowed_files":["src/Libraries/Microsoft.Extensions.AI*/**","test/Libraries/Microsoft.Extensions.AI*/**","docs/**"],"base_branch":"main","draft":true,"if_no_changes":"warn","labels":["automation","area-ai"],"max":1,"max_patch_files":100,"max_patch_size":4096,"preserve_branch_name":true,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review"},"create_report_incomplete_issue":{},"mark_pull_request_as_ready_for_review":{"required_labels":["automation","area-ai"],"target":"*"},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_to_pull_request_branch":{"allowed_files":["src/Libraries/Microsoft.Extensions.AI*/**","test/Libraries/Microsoft.Extensions.AI*/**","docs/**"],"if_no_changes":"ignore","max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"required_labels":["automation","area-ai"],"target":"*"},"report_incomplete":{},"update_pull_request":{"allow_body":true,"allow_title":true,"max":1,"target":"*","update_branch":false}} + GH_AW_SAFE_OUTPUTS_CONFIG_5718f446b1211086_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Labels [\"automation\" \"area-ai\"] will be automatically added. PRs will be created as drafts.", + "update_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be updated. Target: *." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + }, + "reply_to_id": { + "type": "string", + "maxLength": 256 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "create_pull_request": { + "defaultMax": 1, + "fields": { + "base": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "mark_pull_request_as_ready_for_review": { + "defaultMax": 1, + "fields": { + "pull_request_number": { + "issueOrPRNumber": true + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "push_to_pull_request_branch": { + "defaultMax": 1, + "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "pull_request_number": { + "issueOrPRNumber": true + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + }, + "update_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "draft": { + "type": "boolean" + }, + "operation": { + "type": "string", + "enum": [ + "replace", + "append", + "prepend" + ] + }, + "pull_request_number": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "update_branch": { + "type": "boolean" + } + }, + "customValidation": "requiresOneOf:title,body,update_branch" + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --name awmg-mcpg --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.3.30' + + mkdir -p "$HOME/.copilot" + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_4470e57135d8a598_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "safeoutputs": { + "type": "stdio", + "container": "ghcr.io/github/gh-aw-node", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["-w", "\${GITHUB_WORKSPACE}"], + "entrypoint": "sh", + "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], + "env": { + "DEBUG": "*", + "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", + "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", + "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", + "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", + "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", + "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", + "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", + "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", + "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_4470e57135d8a598_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Start CLI Proxy + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_SERVER_URL: ${{ github.server_url }} + CLI_PROXY_POLICY: '{"allow-only":{"disapproval-reactions":["THUMBS_DOWN","CONFUSED"],"endorsement-reactions":["THUMBS_UP","HEART"],"min-integrity":"approved","repos":"all"}}' + CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.30' + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 350 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.11/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.azureedge.net\",\"*.githubusercontent.com\",\"*.opentelemetry.io\",\"*.vsblob.vsassets.io\",\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.nuget.org\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"azuresearch-usnc.nuget.org\",\"azuresearch-ussc.nuget.org\",\"builds.dotnet.microsoft.com\",\"ci.dot.net\",\"codeload.github.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"dc.services.visualstudio.com\",\"dist.nuget.org\",\"docs.github.com\",\"dot.net\",\"dotnet.microsoft.com\",\"dotnetcli.blob.core.windows.net\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"nuget.org\",\"nuget.pkg.github.com\",\"nugetregistryv2prod.blob.core.windows.net\",\"objects.githubusercontent.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"oneocsp.microsoft.com\",\"opentelemetry.io\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"patch-diff.githubusercontent.com\",\"pkgs.dev.azure.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\",\"www.microsoft.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.5\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.11,squid=sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d,agent=sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7,api-proxy=sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + GH_AW_CHROOT_BINARIES_SOURCE_PATH=/tmp/gh-aw GH_AW_CHROOT_IDENTITY_HOME=/tmp/gh-aw/home node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2086 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_LLM_PROVIDER: github + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_TIMEOUT_MINUTES: 350 + GH_AW_VERSION: v0.81.6 + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }} + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + RUNNER_TEMP: ${{ runner.temp }} + TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} + - name: Stop CLI Proxy + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_cli_proxy.sh" + - name: Detect agent errors + if: always() + id: detect-agent-errors + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + - name: Configure Git credentials + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "*.azureedge.net,*.githubusercontent.com,*.opentelemetry.io,*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,dist.nuget.org,docs.github.com,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,opentelemetry.io,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Validate agent output identity + run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\nout=/tmp/gh-aw/agent_output.json\nif [ ! -f \"$out\" ]; then\n echo \"::notice::No agent output to validate\"; exit 0\nfi\n# Only full-body PR writes carry the tracking block. Skip append/prepend updates\n# (partial bodies) and non-PR items (comments, no-op).\nidx=$(jq -r '(.items // []) | to_entries[]\n | select(.value.type==\"create_pull_request\"\n or (.value.type==\"update_pull_request\"\n and ((.value.operation // \"replace\")==\"replace\")))\n | select((.value.body // \"\") != \"\")\n | .key' \"$out\" 2>/dev/null || true)\nif [ -z \"$idx\" ]; then\n echo \"::notice::No full-body PR-writing items -- nothing to validate\"; exit 0\nfi\nrc=0\nwhile IFS= read -r i; do\n [ -n \"$i\" ] || continue\n typ=$(jq -r \".items[$i].type\" \"$out\")\n body=$(jq -r \".items[$i].body\" \"$out\")\n miss=\"\"\n # Validate the state block the same way the next run's setup script reads it: the block\n # is delimited by whole `# meai-otel-genai-worker:state:begin` / `:state:end` comment\n # lines (leading indent / trailing CR tolerated, \"# \" prefix and \":state:\" suffix exact),\n # and the required fields are extracted anchored-to-line-start WITHIN that block (the LAST\n # begin..end range, since the body ends with it). Matching the reader exactly means\n # anything this guard admits is recoverable next run; a body missing the markers or a\n # field would parse to empty state and wedge the machine into a re-produce loop.\n blk=$(printf '%s' \"$body\" | awk '{t=$0;sub(/\\r$/,\"\",t);gsub(/^[[:space:]]+|[[:space:]]+$/,\"\",t)} t==\"# meai-otel-genai-worker:state:begin\"{inb=1;buf=$0 ORS;next} inb{buf=buf $0 ORS; if(t==\"# meai-otel-genai-worker:state:end\"){inb=0;last=buf}} END{if(inb)last=buf; printf \"%s\", last}')\n printf '%s\\n' \"$body\" | awk '{t=$0;sub(/\\r$/,\"\",t);gsub(/^[[:space:]]+|[[:space:]]+$/,\"\",t)} t==\"# meai-otel-genai-worker:state:begin\"{f=1} END{exit !f}' || miss=\"$miss begin-marker\"\n printf '%s\\n' \"$body\" | awk '{t=$0;sub(/\\r$/,\"\",t);gsub(/^[[:space:]]+|[[:space:]]+$/,\"\",t)} t==\"# meai-otel-genai-worker:state:end\"{f=1} END{exit !f}' || miss=\"$miss end-marker\"\n printf '%s' \"$blk\" | grep -Eq '^[[:space:]]*[-*+>]*[[:space:]]*upstream-scan-ref:[[:space:]]*\"?[0-9a-fA-F]{7,}' || miss=\"$miss upstream-scan-ref\"\n printf '%s' \"$blk\" | grep -Eq '^[[:space:]]*[-*+>]*[[:space:]]*feedback-processed-through:[[:space:]]*\"?[^[:space:]\"]' || miss=\"$miss feedback-processed-through\"\n if [ -n \"$miss\" ]; then\n echo \"::error::$typ item #$i is missing required tracking identity:$miss\"\n rc=1\n fi\ndone <<< \"$idx\"\n[ \"$rc\" -eq 0 ] && echo \"Agent output identity validated.\"\nexit $rc\n" + + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - pat_pool + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + runs-on: ubuntu-slim + environment: copilot-pat-pool + permissions: + contents: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-meai-otel-genai-worker" + cancel-in-progress: false + queue: max + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/meai-otel-genai-worker.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Collect usage artifact files + if: always() + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection + echo "Usage artifact source file status:" + for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do + [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" + done + [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true + [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true + [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true + [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true + [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true + [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true + [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl + [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl + mkdir -p /tmp/gh-aw/usage/activity + node ${{ runner.temp }}/gh-aw/actions/generate_usage_activity_summary.cjs + find /tmp/gh-aw/usage -type f -print | sort + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}usage + path: | + /tmp/gh-aw/usage/aw_info.json + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.json + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/github_rate_limits.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + /tmp/gh-aw/usage/activity/summary.json + if-no-files-found: ignore + - name: Restore daily AIC usage cache + id: restore-daily-aic-cache-conclusion + if: always() + continue-on-error: true + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: agentic-workflow-usage-meaiotelgenaiworker-${{ github.run_id }} + restore-keys: agentic-workflow-usage-meaiotelgenaiworker- + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Write daily AIC usage cache entry + id: write-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ github.token }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context); + const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + await main(); + - name: Save daily AIC usage cache + id: save-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: agentic-workflow-usage-meaiotelgenaiworker-${{ github.run_id }} + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + - name: Upload daily AIC usage cache artifact + id: upload-daily-aic-cache + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: aic-usage-cache + path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl + if-no-files-found: ignore + retention-days: 7 + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/meai-otel-genai-worker.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "meai-otel-genai-worker" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/meai-otel-genai-worker.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/meai-otel-genai-worker.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/meai-otel-genai-worker.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/meai-otel-genai-worker.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "meai-otel-genai-worker" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "false" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "350" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: always() && needs.agent.result != 'skipped' + runs-on: ubuntu-latest + environment: copilot-pat-pool + permissions: + contents: read + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/meai-otel-genai-worker.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.agent.outputs.artifact_prefix }}agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.11@sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11@sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.11 ghcr.io/github/gh-aw-firewall/squid:0.27.11@sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f "$HOME/.copilot/mcp-config.json" + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + rm -f /tmp/gh-aw/agent_usage.json + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then + echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." + fi + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + WORKFLOW_DESCRIPTION: "Produce or refresh the single draft pull request that integrates OpenTelemetry gen-ai semantic-conventions updates into Microsoft.Extensions.AI. Invoked per-target by the MEAI Otel GenAI Orchestrator (workflow_call) or manually (workflow_dispatch)." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.65 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.11 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.11/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5},\"container\":{\"imageTag\":\"0.27.11,squid=sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d,agent=sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7,api-proxy=sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + _GH_AW_CHROOT_JSON=$(jq -c --arg src /tmp/gh-aw --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home /tmp/gh-aw/home '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "/tmp/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2086 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_0 || secrets.COPILOT_PAT_1 || secrets.COPILOT_PAT_2 || secrets.COPILOT_PAT_3 || secrets.COPILOT_PAT_4 || secrets.COPILOT_PAT_5 || secrets.COPILOT_PAT_6 || secrets.COPILOT_PAT_7 || secrets.COPILOT_PAT_8 || secrets.COPILOT_PAT_9 || secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_LLM_PROVIDER: github + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.81.6 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + RUNNER_TEMP: ${{ runner.temp }} + TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.agent.outputs.artifact_prefix }}detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + pat_pool: + needs: pre_activation + runs-on: ubuntu-slim + environment: copilot-pat-pool + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_9. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 9); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_9)" + echo "$warning_message" + exit 0 + fi + + # Select a random index. Seed only when a non-negative integer is provided, + # normalized to Bash's 0-32767 $RANDOM seed range -- assigning a non-integer to + # $RANDOM would break the arithmetic expansion below. + if printf '%s' "$RANDOM_SEED" | grep -Eq '^[0-9]+$'; then + RANDOM=$(( RANDOM_SEED % 32768 )) + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|8|9|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + + pre_activation: + runs-on: ubuntu-slim + environment: copilot-pat-pool + env: + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + matched_command: '' + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/meai-otel-genai-worker.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); + await main(); + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + environment: copilot-pat-pool + permissions: + contents: write + issues: write + pull-requests: write + timeout-minutes: 45 + env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/meai-otel-genai-worker" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.65" + GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_WORKFLOW_ID: "meai-otel-genai-worker" + GH_AW_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/meai-otel-genai-worker.md" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} + comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} + push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "MEAI: Otel GenAI Worker" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/meai-otel-genai-worker.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.65" + GH_AW_INFO_AWF_VERSION: "v0.27.11" + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent + path: /tmp/gh-aw/ + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: true + fetch-depth: 0 + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + - name: Fetch additional refs + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + env: + GH_AW_FETCH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + header=$(printf "x-access-token:%s" "${GH_AW_FETCH_TOKEN}" | base64 -w 0) + git -c "http.extraheader=Authorization: Basic ${header}" fetch origin '+refs/heads/*:refs/remotes/origin/*' + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_ALLOWED_DOMAINS: "*.azureedge.net,*.githubusercontent.com,*.opentelemetry.io,*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,dist.nuget.org,docs.github.com,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,opentelemetry.io,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"required_labels\":[\"automation\",\"area-ai\"],\"target\":\"*\"},\"create_pull_request\":{\"allowed_files\":[\"src/Libraries/Microsoft.Extensions.AI*/**\",\"test/Libraries/Microsoft.Extensions.AI*/**\",\"docs/**\"],\"base_branch\":\"main\",\"draft\":true,\"if_no_changes\":\"warn\",\"labels\":[\"automation\",\"area-ai\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":4096,\"preserve_branch_name\":true,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\"},\"create_report_incomplete_issue\":{},\"mark_pull_request_as_ready_for_review\":{\"required_labels\":[\"automation\",\"area-ai\"],\"target\":\"*\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"src/Libraries/Microsoft.Extensions.AI*/**\",\"test/Libraries/Microsoft.Extensions.AI*/**\",\"docs/**\"],\"if_no_changes\":\"ignore\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"required_labels\":[\"automation\",\"area-ai\"],\"target\":\"*\"},\"report_incomplete\":{},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":true,\"max\":1,\"target\":\"*\",\"update_branch\":false}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + diff --git a/.github/workflows/meai-otel-genai-worker.md b/.github/workflows/meai-otel-genai-worker.md new file mode 100644 index 00000000000..488c04b2da7 --- /dev/null +++ b/.github/workflows/meai-otel-genai-worker.md @@ -0,0 +1,754 @@ +--- +name: "MEAI: Otel GenAI Worker" +description: >- + Produce or refresh the single draft pull request that integrates OpenTelemetry + gen-ai semantic-conventions updates into Microsoft.Extensions.AI. Invoked per-target + by the MEAI Otel GenAI Orchestrator (workflow_call) or manually (workflow_dispatch). + +permissions: + contents: read + pull-requests: read + issues: read + actions: read + +safe-outputs: + # Threat detection screens the untrusted upstream conventions and reviewer feedback this + # agent integrates. It runs in a separate gh-aw job that authenticates via the coalescing + # token below. + threat-detection: + engine: + id: copilot + env: + # Workaround for github/gh-aw#43917: the detection job's `needs` omit `pat_pool`, so the + # main engine's case(needs.pat_pool...) token can't resolve here. Authenticate by + # coalescing the pool PAT secrets directly -- the first non-empty one wins. + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_0 || secrets.COPILOT_PAT_1 || secrets.COPILOT_PAT_2 || secrets.COPILOT_PAT_3 || secrets.COPILOT_PAT_4 || secrets.COPILOT_PAT_5 || secrets.COPILOT_PAT_6 || secrets.COPILOT_PAT_7 || secrets.COPILOT_PAT_8 || secrets.COPILOT_PAT_9 || secrets.COPILOT_GITHUB_TOKEN }} + create-pull-request: + draft: true + labels: [automation, area-ai] + base-branch: main + preserve-branch-name: true + if-no-changes: "warn" + allowed-files: + - "src/Libraries/Microsoft.Extensions.AI*/**" + - "test/Libraries/Microsoft.Extensions.AI*/**" + - "docs/**" + push-to-pull-request-branch: + target: "*" + required-labels: [automation, area-ai] + if-no-changes: "ignore" + allowed-files: + - "src/Libraries/Microsoft.Extensions.AI*/**" + - "test/Libraries/Microsoft.Extensions.AI*/**" + - "docs/**" + update-pull-request: + target: "*" + required-labels: [automation, area-ai] + title: true + body: true + add-comment: + target: "*" + required-labels: [automation, area-ai] + max: 1 + mark-pull-request-as-ready-for-review: + target: "*" + required-labels: [automation, area-ai] + noop: + report-as-issue: false + report-failure-as-issue: false + +network: + allowed: + - defaults + - dotnet + - github + - opentelemetry.io + - "*.opentelemetry.io" + - "*.azureedge.net" + +tools: + github: + mode: gh-proxy + toolsets: [default] + # Only content at "approved" integrity or higher reaches the agent. Write-access + # authors (OWNER/MEMBER/COLLABORATOR) are approved automatically; feedback from + # anyone else is filtered out unless a maintainer endorses it (see integrity-reactions), + # which promotes that item to approved. This replaces any hand-rolled trust handling. + min-integrity: approved + +features: + # Let a maintainer promote an external contributor's comment to "approved" by adding an + # endorsement reaction (👍/❤️), so the agent then sees and can act on it. Requires + # gh-proxy mode (above) to attribute the reacting user. + integrity-reactions: true + +runs-on: ubuntu-latest +timeout-minutes: 350 + +checkout: + fetch: ["*"] + fetch-depth: 0 + +concurrency: + group: meai-otel-genai-worker + cancel-in-progress: false + +on: + # Invoked per-target by the MEAI Otel GenAI Orchestrator as a reusable + # workflow so this runs in the orchestrator's run context and inherits its actor, + # satisfying gh-aw activation's role check. + workflow_call: + inputs: + target: + description: "Single discovery target (JSON) from meai-otel-genai-orchestrator-discover.sh." + required: true + type: string + # Manual dispatch for standalone testing: provide a target JSON, or just an + # upstream_ref (the setup resolves the rest), or nothing (scan default-branch HEAD). + workflow_dispatch: + inputs: + target: + description: "Single discovery target (JSON). Leave empty to resolve from upstream_ref." + required: false + type: string + upstream_ref: + description: >- + Optional git ref (branch, tag, or commit SHA) in + open-telemetry/semantic-conventions-genai to scan instead of the + default-branch HEAD. Leave empty to scan the default-branch HEAD. + required: false + type: string + permissions: {} + +# Before the agent runs, deterministically prepare the run: resolve the upstream scan +# target (from the orchestrator's `target` input, or a standalone dispatch's upstream_ref / +# default HEAD), discover and classify the maintained draft PR (ours / adopt / blocked / +# none), and compute the recommended lifecycle action. The agent consumes target.json +# instead of discovering that state itself, and stamps target.json's run_started_at back as +# the new feedback-processed-through watermark. target.json is uploaded in the agent artifact. +steps: + - name: Set up the run context + env: + GH_TOKEN: ${{ github.token }} + TARGET_JSON: ${{ inputs.target }} + UPSTREAM_REF: ${{ inputs.upstream_ref }} + run: bash .github/scripts/meai-otel-genai-worker-setup.sh + +# After the agent runs, verify each full-body pull-request write still carries the state +# block that the next run reads back to resume: the tracking block, a non-empty +# upstream-scan-ref, and a feedback-processed-through watermark. A PR/update body missing +# any of these would leave the next run unable to recover its place, so fail before it can publish. +post-steps: + - name: Validate agent output identity + run: | + set -euo pipefail + mkdir -p /tmp/gh-aw/agent + out=/tmp/gh-aw/agent_output.json + if [ ! -f "$out" ]; then + echo "::notice::No agent output to validate"; exit 0 + fi + # Only full-body PR writes carry the tracking block. Skip append/prepend updates + # (partial bodies) and non-PR items (comments, no-op). + idx=$(jq -r '(.items // []) | to_entries[] + | select(.value.type=="create_pull_request" + or (.value.type=="update_pull_request" + and ((.value.operation // "replace")=="replace"))) + | select((.value.body // "") != "") + | .key' "$out" 2>/dev/null || true) + if [ -z "$idx" ]; then + echo "::notice::No full-body PR-writing items -- nothing to validate"; exit 0 + fi + rc=0 + while IFS= read -r i; do + [ -n "$i" ] || continue + typ=$(jq -r ".items[$i].type" "$out") + body=$(jq -r ".items[$i].body" "$out") + miss="" + # Validate the state block the same way the next run's setup script reads it: the block + # is delimited by whole `# meai-otel-genai-worker:state:begin` / `:state:end` comment + # lines (leading indent / trailing CR tolerated, "# " prefix and ":state:" suffix exact), + # and the required fields are extracted anchored-to-line-start WITHIN that block (the LAST + # begin..end range, since the body ends with it). Matching the reader exactly means + # anything this guard admits is recoverable next run; a body missing the markers or a + # field would parse to empty state and wedge the machine into a re-produce loop. + blk=$(printf '%s' "$body" | awk '{t=$0;sub(/\r$/,"",t);gsub(/^[[:space:]]+|[[:space:]]+$/,"",t)} t=="# meai-otel-genai-worker:state:begin"{inb=1;buf=$0 ORS;next} inb{buf=buf $0 ORS; if(t=="# meai-otel-genai-worker:state:end"){inb=0;last=buf}} END{if(inb)last=buf; printf "%s", last}') + printf '%s\n' "$body" | awk '{t=$0;sub(/\r$/,"",t);gsub(/^[[:space:]]+|[[:space:]]+$/,"",t)} t=="# meai-otel-genai-worker:state:begin"{f=1} END{exit !f}' || miss="$miss begin-marker" + printf '%s\n' "$body" | awk '{t=$0;sub(/\r$/,"",t);gsub(/^[[:space:]]+|[[:space:]]+$/,"",t)} t=="# meai-otel-genai-worker:state:end"{f=1} END{exit !f}' || miss="$miss end-marker" + printf '%s' "$blk" | grep -Eq '^[[:space:]]*[-*+>]*[[:space:]]*upstream-scan-ref:[[:space:]]*"?[0-9a-fA-F]{7,}' || miss="$miss upstream-scan-ref" + printf '%s' "$blk" | grep -Eq '^[[:space:]]*[-*+>]*[[:space:]]*feedback-processed-through:[[:space:]]*"?[^[:space:]"]' || miss="$miss feedback-processed-through" + if [ -n "$miss" ]; then + echo "::error::$typ item #$i is missing required tracking identity:$miss" + rc=1 + fi + done <<< "$idx" + [ "$rc" -eq 0 ] && echo "Agent output identity validated." + exit $rc + +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# Run agentic jobs in an isolated `copilot-pat-pool` environment. +# +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - uses: shared/pat_pool.md + with: + environment: copilot-pat-pool + +environment: copilot-pat-pool + +engine: + id: copilot + env: + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} +--- + +# MEAI: Otel GenAI Worker + +## Goal + +Keep `Microsoft.Extensions.AI` aligned with the OpenTelemetry gen-ai semantic +conventions by continuously maintaining a **single draft pull request** against +`main` in this repository. Each daily run compares the upstream +`open-telemetry/semantic-conventions-genai` repository against `main`, produces or +refreshes an integration plan, implements it, validates it, and keeps the PR up to +date until the upstream release is published -- at which point the PR is marked +**Ready for Review**. + +**The gen-ai conventions being unreleased is the normal, expected steady state.** +These conventions live in `Development` stability and are integrated from merged +upstream commits *before* any tagged release exists. The draft PR is built and +maintained from those unreleased upstream changes -- the absence of a published +release is **never** a reason to skip work. Publishing a release only flips the +finished draft PR to Ready for Review (Step 6); it does not gate whether the +integration happens. Do **not** no-op merely because `upstream-release` is `none`. + +The repository skill `update-otel-genai-conventions` (in `.github/skills/`) is the +authority for **how** to analyze conventions and implement changes. This workflow +governs **lifecycle/idempotency** (which PR to touch and what state to leave it in). + +## Step 0 -- Read the run context (authoritative) + +Before you do anything, read the run context the host setup wrote to +`/tmp/gh-aw/agent/target.json` (the authoritative input for this run -- do not +re-discover this state yourself): + +- **`target.json`** -- the resolved scan target and PR discovery: + - `upstream_repo`, `upstream_ref`, `upstream_sha` -- the exact upstream commit to + scan (already resolved; use `upstream_sha` as `upstream-scan-ref`, do not re-resolve). + - `upstream_release` -- the latest published gen-ai release tag, or `none`. + - `upstream_release_commit` -- the commit reached by resolving `upstream_release` + through `refs/tags`, including annotated-tag dereferencing, or empty when no + release commit could be resolved. + - `release_matches_scan` -- `true` when `upstream_release_commit` equals the + current `upstream_sha` being scanned. + - `release_matches_pr` / `release_ready` -- `true` only when the latest release tag + resolves to the maintained PR's recorded `upstream-scan-ref`. This is the only + release signal allowed to route an existing draft PR to Step 6. + - `release_detection_confidence`, `release_detection_signal` -- the setup's + confidence and evidence for how the latest release tag was identified. + - `desired_branch` -- the evergreen canonical PR branch name + (`update-otel-genai-to-latest`). + - `pr_branch` -- the actual head branch of the maintained PR, or empty when none + exists. This can be `desired_branch` or a run-suffixed branch created when the + canonical branch already existed but no open PR used it. + - `pr` -- the maintained PR number, or empty when none exists. + - `pr_state`, `pr_is_draft`, `pr_recorded_sha`, `pr_recorded_release` -- the + discovered PR's state and the `upstream-scan-ref` / `upstream-release` it records. + - `watermark` -- the PR body's current `feedback-processed-through` value (empty on a + fresh PR, or when the host could not read the body -- see "Recover an unread body + first" below); the cutoff for which review feedback is new this run (see "Honoring + reviewer feedback"). + - `run_started_at` -- this run's start time, captured before the run began. Stamp this + back as the new `feedback-processed-through` watermark in the tracking block (Step 5). + - `has_new_feedback` -- `true` when the setup's author-agnostic wake gate saw review + activity newer than `watermark` on the maintained PR (it never reads comment bodies). + When the PR is caught up on the upstream SHA but this is `true`, run the feedback-only + pass in Step 3 instead of no-opping, so the watermark advances and the wake clears. + - `classification` -- one of: + - `ours` -- an open `automation`+`area-ai` PR on `pr_branch` that already carries + our `meai-otel-genai-worker:state` block. Maintain it. + - `adopt` -- an open `automation`+`area-ai` PR on `desired_branch` that a human + **bootstrapped** but that has **no** tracking block yet. **Take it over**: + treat it like an incremental update, and write the full tracking block into its + body this run so future runs classify it as `ours`. + - `blocked` -- a PR occupies `desired_branch` but is **not** our automation PR (a + human owns it). **Stand down**: emit a `noop` explaining a human-owned PR holds + the branch, and make no other output. + - `none` -- no open canonical PR and no valid open suffixed tracking PR; the fresh + path applies. + - `action` -- the setup's recommended lifecycle action (`produce` or `noop`). When + it is `noop` **and** `classification` is `ours` (caught up on the SHA, no new review + activity, and no release action is pending), you may **early-exit**: emit a single `noop` with the + reason and do **not** run the expensive build. For every other case run the full + analysis and let your Step 3 decision be authoritative -- `action` is only a hint. + +**Recover an unread body first.** The host reads `pr_recorded_sha` and `watermark` +from the maintained PR body, so a transient fetch failure can leave both empty even +though `pr` is non-empty. When `classification` is `ours` but `pr_recorded_sha` (and +usually `watermark`) is empty, do **not** treat the PR as fresh: fetch the PR body +yourself once, locate its `# meai-otel-genai-worker:state:begin` .. `:state:end` +block, and read `upstream-scan-ref` and `feedback-processed-through` from it before +scoping feedback ("Honoring reviewer feedback") or computing the differential. If your +own read also fails, treat `feedback-processed-through` as this run's `run_started_at` +(process no prior feedback this run) rather than reprocessing every earlier comment -- +the next run recovers the real watermark and picks up anything genuinely new. + +If `classification` is `blocked`, stop now with a `noop`. If `action` is `noop` and +`classification` is `ours`, stop now with a `noop`. Otherwise continue. + +## Step 1 -- Capture the upstream state + +**Which upstream ref to scan.** The host setup already resolved this into +`target.json`: scan `upstream_sha` (the concrete commit for the orchestrator's target, a +standalone `upstream_ref` dispatch, or the default-branch HEAD). Record `upstream_sha` +as `upstream-scan-ref`; all downstream logic (the SHA comparison in Step 3, the +tracking block) uses that resolved SHA. Do **not** re-resolve the ref yourself. + +1. Read the state of `open-telemetry/semantic-conventions-genai` at the scanned ref + (the `upstream_ref` override when provided, otherwise its default branch): + - The scanned ref's **commit SHA** (`upstream-scan-ref`). + - Whether a tagged **release** exists/is published (`upstream-release`) and the + setup's resolved `upstream_release_commit` / `release_detection_confidence`. + Record `none` while the conventions are still unreleased (Development stability). + A value of `none` is the expected normal case and must **not** cause a no-op or + short-circuit -- continue to Step 2 and integrate the unreleased changes. + - The **core semantic-conventions dependency version** the gen-ai conventions + target (`core-semconv-dependency`). +2. Derive the **naming token** `{target}` for the branch and title: + - If a gen-ai release is published **and** `target.json.release_matches_scan` is + true, use that release version with a `v` prefix (e.g. `v1.42.0`). + - Otherwise -- the normal case while the conventions are unreleased -- use the + literal token `latest`. Do **not** substitute the core semantic-conventions + dependency version (`core-semconv-dependency`) here; that is the core semconv + dependency, **not** the gen-ai conventions version. Identify the unreleased + update by its upstream commit SHA and scan date in the body instead. + - The branch name is `update-otel-genai-to-{target}` and the PR title is + `Update open-telemetry/semantic-conventions-genai to {target}` (e.g. + `...to latest` while unreleased, `...to v1.42.0` once a release is published). + The title carries **no** `[automation]` prefix; the `automation` label conveys + that instead. + +## Step 2 -- Build the plan via the skill + +Invoke the `update-otel-genai-conventions` skill for its analysis only -- build the +plan **in working memory**. Because this is an unattended scheduled run, do **not** +use the skill's interactive Plan-then-Implement checkpoint: do **not** write a +`plan.md` file and do **not** pause for human approval between analysis and +implementation. Drive the work from the audit table and the ordered work-item list the +skill produces, then implement directly in Step 4. + +Do the **full** convention cross-reference -- compare the gen-ai attributes, +metrics, events, and operation names defined at the scanned upstream commit against what the +code actually emits (the implemented version from the doc comments). Drive this from +the conventions themselves, not from the upstream CHANGELOG: a `Development`-stability +upstream commonly shows `Unreleased`/no tag while still carrying merged convention +changes that are ahead of the implemented version. An empty CHANGELOG section or the +lack of a release tag is **not** evidence that there is nothing to integrate -- only +a completed cross-reference that finds zero deltas vs. the implemented version is. + +Note: the skill's own "existing PR preflight" tells it to stop when an open PR +exists. **Override that for this workflow** -- do not stop; instead follow the +PR-handling decision in Step 3. Use the skill purely for the analysis, change +classification, implementation patterns, testing guidance, and validation commands. + +Also ask the skill for its **release-tagging analysis**. Use the setup's +`release_detection_*` fields as the baseline release signal. The skill should compare +the observed upstream release signals (GitHub latest release tag, resolved tag commit, +CHANGELOG release headers, schema URL, and any durable tag naming convention it can +verify) and report a confidence level. If the skill finds a clearly dependable release +signal that differs from the setup's GitHub-release tag dereferencing, preserve that +finding for the PR body caution block in Step 5; do not change the Step 3 release gate +inside the run. + +### What counts as work -- track merged upstream changes aggressively + +This workflow tracks **merged upstream gen-ai convention changes regardless of +whether a release is published.** Each merged upstream PR (e.g. every +`changelog.d/*.md` fragment, or any convention change present at the scanned upstream commit +but not yet reflected in the implemented version) is a tracked change. Treat the +set of such changes ahead of the implemented version as the work to integrate. + +Classify each tracked change as **actionable now** or **deferred**, and do **not** +let deferral collapse the run into a no-op: + +- **Actionable now (must be implemented in this run):** any change that touches a + convention item the code **already emits** -- a type/unit change (e.g. + `gen_ai.request.top_k` `double` -> `int`), a requiredness or scope change, a + rename, a sampling-relevance change, a new well-known value for an + already-emitted attribute (e.g. a new `gen_ai.provider.name` value), or a new + attribute/metric that maps onto a capability M.E.AI already instruments. These + are integrated immediately; they are never deferred. +- **Deferred (tracked, not coded yet):** a brand-new attribute/metric/event/span + with **no** current emission site, or a capability M.E.AI does not yet instrument + (e.g. memory, retrieval, workflow, agent-framework spans), or a documentation-only + clarification with no code impact. The skill's "no-orphan-constants" rule means + you do **not** add the constant yet -- but you **still** record the item in the + PR's changes table (🟢) so the draft PR documents the full upstream delta. + +The "defer" classification applies to **individual constants**, never to the run as +a whole. As long as **one or more** tracked changes exist ahead of the implemented +version, there is a non-zero delta: open or maintain the draft PR (Step 3/4), +implement every actionable-now item, and document every deferred item in the changes +table. A run only no-ops when there are genuinely **zero** unintegrated upstream +gen-ai changes, or the tracked PR is already caught up on the upstream SHA (Step 3). + +## Step 3 -- Choose the action from the setup's discovery + +The host setup already discovered the maintained PR and recorded it in `target.json` +(Step 0) -- do **not** re-list or bulk-fetch PR bodies yourself. Use its fields: + +- `classification` = `none` -> no open PR on `desired_branch`, and no valid open + suffixed tracking PR discovered by the setup: the **Fresh PR** path. + For a nicer body you may do **one** lightweight lookup of a prior *closed/merged* + `automation`+`area-ai` PR on `update-otel-genai-to-latest` to reference (query only + number/state/mergedAt -- never bulk-fetch bodies); if the lookup is unavailable, skip + the reference rather than failing. +- `classification` = `blocked` -> a human owns a PR on `desired_branch`: emit a `noop` + saying so and make no other output. +- `classification` = `ours` or `adopt` -> compare `pr_recorded_sha` to `upstream_sha` + and pick the row below. For `adopt`, additionally write the **full tracking block** + into the body this run (the bootstrapped PR has none yet) so it becomes `ours`. + When you need to fetch, check out, or push the maintained PR branch, use + `target.json.pr_branch` (the actual branch), not `target.json.desired_branch` + (the canonical branch). + +First check the **release gate**: if `target.json.release_ready` is `true` and the PR +has a pending release action, go straight to **Step 6** regardless of the current +`upstream_sha` comparison. A release action is pending when the PR is still draft, or +when its tracking block's `upstream-release` does not equal `target.json.upstream_release`. +`release_ready` means the latest published release tag resolved to the same commit +recorded in the PR's `upstream-scan-ref`; do **not** use `upstream_release != none` as +the gate. + +Otherwise compare `pr_recorded_sha` to `upstream_sha` and pick the matching action. The +SHA comparison is the primary decision; the PR's draft state only matters when behind. + +| If the maintained PR is... | ...and it is | Action | +|---|---|---| +| caught up with the upstream SHA | open (draft or not) **or** merged, `has_new_feedback` = false | **No-op** -- no comment, report, issue, or PR; write the reason to the step summary (see no-op rules). (The release gate above already diverted any pending release action to Step 6, and the feedback-only row below handles new review activity, so this row is reached only when no release title/body or mark-ready transition is pending and there is no new review activity.) | +| caught up with the upstream SHA | open (draft or not) with `has_new_feedback` = true | **Feedback-only update.** Run the reviewer-feedback pass (see "Honoring reviewer feedback"): address the approved feedback, push any resulting commit(s), refresh the PR body/tracking block, advance `feedback-processed-through` to `run_started_at`, and post the single summary comment. If the pass finds nothing actionable, still refresh the body so the watermark advances and the wake does not repeat. | +| **behind** the upstream SHA | open **draft** (`ours` or `adopt`) | **Incremental update.** Re-analyze against `main` plus what the branch already integrates; push one batch of commit(s) to the PR branch; refresh the PR body/tracking block (for `adopt`, add the full tracking block); comment summarizing the delta. | +| **behind** the upstream SHA | open **non-draft** | **Advisory only -- do not implement.** Comment capturing the additional upstream changes to consider; note that re-marking the PR as draft lets the next scheduled run implement them, and that the workflow can be dispatched manually to run immediately. | +| `classification` = `none`, or a prior PR is **closed without merging** / **merged-but-behind** | absent, closed, or merged-but-behind | **Fresh PR** (Step 4). For a merged-but-behind PR you referenced, describe the updates layered on top. | + +If the situation does not cleanly match a row, use judgment toward the overall goal: +**keep one draft PR continuously updated until the upstream release publishes.** + +Once a row other than No-op is selected (Fresh PR, Incremental, or Advisory), +proceed to carry it out. Do **not** re-derive a no-op afterward from the CHANGELOG +state or from `upstream-release: none` -- the work decision is driven by the SHA +comparison and the zero-delta check from Step 2. A *published* release never causes a +no-op by itself; only `release_ready` can escalate a pending release action into Step 6 +(handled by the release gate above). + +## Step 4 -- Implement + +Follow the skill's **Implementation Procedure** for each work item. Then validate. + +### File scope -- what this workflow may change + +The pull request may modify **only** files under these paths (the safe output +enforces this as an allow-list, and the run fails if the patch touches anything +else): +- `src/Libraries/Microsoft.Extensions.AI*/**` +- `test/Libraries/Microsoft.Extensions.AI*/**` +- `docs/**` + +**Never** edit, stage, or commit anything outside that set. In particular do **not** +touch: this workflow and its generated lock (`.github/**`, including +`.github/workflows/meai-otel-genai-orchestrator.*`, `.github/workflows/meai-otel-genai-worker.*`, +`.github/scripts/meai-otel-genai-*`, and `.github/aw/**`), `global.json`, +`NuGet.config`, `Directory.Packages.props`, any `*.sln`/`SDK.sln*` solution files, +or dependency lockfiles. If a build step generates such files (e.g. `SDK.sln`), +delete them before producing output so they cannot leak into the patch. + +### How to produce the patch (critical -- avoids fork base-resolution failures) + +The patch the safe output turns into a PR is generated by diffing your commits +against the **exact commit that was checked out** (`GITHUB_SHA`). This stays clean +only if `HEAD` never **switches** onto another branch. Do **not** run +`git checkout -b update-otel-genai-to-{target}` or `git switch -c ...`: switching +branches makes patch generation fall back to a `merge-base` against the remote +default branch, which on a fork sweeps the entire fork divergence into the patch +(hundreds of unrelated files) and fails the run. Creating a branch **ref** that +points at the current commit without switching to it is safe (and required -- see +below). So: + +- **Fresh path** (merged-but-behind, closed, or no PR): the run is checked out **on the + base branch** (`main`), so committing now would advance `main` itself and leave the PR + branch you create pointing at the **same commit as its base** -- the generated patch is + then **empty** (and, with threat-detection enabled, the run hard-fails because no + patch/bundle is produced for the detection job to screen). **First detach `HEAD` at the + checked-out commit** so your commit advances a detached `HEAD` and leaves `main` where it + is: `git checkout --detach`. Detaching at the current commit is safe -- it does not switch + to a different branch or commit and does not trigger the fork merge-base fallback. Do + **not** use `git checkout -b`/`git switch -c` (switching to a *new* branch blows up patch + generation), and do **not** run `git fetch`, `git reset`, `git rebase`, or `git merge`. + Make your edits, + delete any generated solution/build artifacts (`SDK.sln*`, `artifacts/`) so they + cannot leak, then stage **only** the in-scope paths. If the scan produced code or + documentation edits, commit them as a single commit on the current `HEAD`: + `git add -- src/Libraries/Microsoft.Extensions.AI* test/Libraries/Microsoft.Extensions.AI* docs && if ! git diff --cached --quiet; then git commit -m "Update open-telemetry/semantic-conventions-genai to {target}"; fi`. + If every tracked upstream change is deferred and there are **no** staged file changes, + there is nothing to integrate yet: do **not** invent a placeholder file, an empty commit, + or an empty PR, and do **not** call `create-pull-request`. An empty (patchless) PR cannot + be threat-screened and would ship a content-free branch, so `create-pull-request` no longer + permits empty branches. Emit a `noop` safe output instead and stop; the tracking PR is + opened on the first run that integrates a real change. + Now choose the PR branch name. The canonical name is `desired_branch` from + `target.json` (`update-otel-genai-to-latest`). Before using it, check whether a branch + with that name already exists on the remote -- a stale branch can linger from a + previously closed PR: + `git ls-remote --heads origin "$DESIRED_BRANCH"` (where `$DESIRED_BRANCH` is + `target.json`'s `desired_branch`). + - If the remote has **no** such branch, use `desired_branch`. + - If the branch **already exists**, do **not** overwrite, delete, or force-recreate + it. Instead suffix the name with the current workflow run id (the `$GITHUB_RUN_ID` + environment variable) as `{desired_branch}_{run_id}`, and remember + that you deviated so Step 5 can record it in the PR body. + Call the chosen name `{branch}`. Create the local branch **ref** for `{branch}` + pointing at that commit **without switching to it** -- the `create-pull-request` + safe output pins this ref to build the bundle and fails with + `Needed a single revision` if it is absent: + `git branch {branch} HEAD`. + Because `main` still points at the checked-out commit while `{branch}` points at your + commit on top of it, the generated patch contains **only** your commit. + Then emit a `create-pull-request` safe output with: + - branch `{branch}` (the ref you just created -- the safe output pushes it to the + remote), + - title `Update open-telemetry/semantic-conventions-genai to {target}`, + - the body described in Step 5, + - draft state (configured by the safe output). +- **Incremental path** (behind open draft, `ours` or `adopt`): the PR branch already + exists on the remote (`target.json`'s `pr_branch`), so here you **do** fetch and + check out that existing branch + (`PR_BRANCH="$(jq -r '.pr_branch' /tmp/gh-aw/agent/target.json)" && git fetch origin "$PR_BRANCH" && git checkout "$PR_BRANCH"`), + apply only the differential work items on top of what is already integrated, stage + the in-scope paths, and commit one batch of one or more **new** commits on top of the + branch's current tip. Every update **appends** to the branch -- never `git commit --amend`, + `git rebase`, `git reset`, squash, or otherwise rewrite commits already on the PR branch, + and never force-push -- so the maintained branch keeps a stable, growing trail of + incremental commits. Then emit a + `push-to-pull-request-branch` safe output targeting that PR, an `update-pull-request` + that fully regenerates the body to reflect the current state (see Step 5 -- a + full-body replace, including the refreshed tracking block; for an `adopt` PR this + also adds the full tracking block the bootstrapped PR was missing), and a single + `add-comment` summarizing the delta. + +When the skill clones or fetches the upstream `semantic-conventions` repository for +analysis, do it **outside** this repository's working tree (e.g. under `/tmp`), never +inside the checkout, so upstream files never enter the patch. + +For **both** implementation paths: +- The build must remain clean (no new warnings) and tests must pass. Use the skill's + build/test commands (Linux/macOS form). Run a full `./build.sh -vs AI` restore, + then `./build.sh -build -test`; remove any stale `SDK.sln*` first. +- If restore/build **cannot run** because the internal Azure DevOps feeds are + unreachable (e.g. `pkgs.dev.azure.com` returns 401/403, or no `project.assets.json` + is produced), do **not** fall back to a manual review and do **not** open or update a + PR with code you could not compile. Treat it as a hard failure: emit a + `report_incomplete` safe output whose `reason` states that the internal NuGet feeds + were unreachable and the change could not be built or tested, and emit **no** + `create-pull-request`, `push-to-pull-request-branch`, `update-pull-request`, + `add-comment`, or `noop` output. The `report_incomplete` signal fails the workflow + run so the outage is surfaced for investigation instead of shipping unvalidated code. +- Ensure **sufficient test coverage** for every new attribute/metric/emission -- + augment existing tests where possible rather than adding parallel test methods. +- Update any affected **docs** in the repo so they reflect the new + conventions. +- If the public API surface changed, regenerate API baselines and keep only the + baseline updates for the libraries actually changed. +- Review the result thoroughly against the skill's review checklist before emitting + output. + +### Honoring reviewer feedback on the maintained draft PR + +Whenever a matching open draft PR exists and there is new review activity +(`target.json.has_new_feedback` = true) -- on the incremental path (behind the SHA) or on +the feedback-only path (caught up on the SHA) -- address reviewer feedback on that PR as +part of this run, before you commit any differential update. + +**Trust is handled by the framework, not by you.** This workflow runs under GitHub +integrity filtering (`min-integrity: approved`): the only PR reviews and review comments +your GitHub tools can see are those from write-access reviewers +(`OWNER`/`MEMBER`/`COLLABORATOR`) plus any external comment a maintainer has explicitly +endorsed with a 👍/❤️ reaction (which promotes that item to approved). Feedback from anyone +else is filtered out before it reaches you. So treat **every** review comment you can read +as trusted reviewer guidance -- you do not need to sanitize it, infer its author's trust +level, or reason about prompt-injection. Integrity filtering governs **who** may give you +feedback; the scope limits below still govern **what** you may act on, so an authorized +comment is still rejected when it asks for out-of-scope work. If you never see a comment, +it was not endorsed; do not go looking for it or try to work around the filter. + +- **Collect the feedback.** Use your GitHub tools to read the maintained PR's submitted + reviews, inline review-comment threads, and standard PR timeline comments (e.g. list + the pull request's reviews, review comments, and issue comments for `pr`). Consider + all of these as review feedback. +- **Scope to what is new.** `target.json.watermark` is the PR body's current + `feedback-processed-through` value. Act only on review feedback **created after** that + watermark; read anything at or before it for context only (e.g. to understand a terse + follow-up that builds on an earlier comment), and do not re-process it. Ignore your + own summary comments and any other bot comments. On a genuinely fresh PR the watermark + is empty and there is no prior feedback; but an empty `watermark` on an existing PR + (`pr` non-empty) means the host could not read the body -- recover it as described in + Step 0 rather than treating all prior feedback as new. (An endorsement that promotes an + external comment older than the watermark will not by itself reopen it; if a maintainer + wants such a comment addressed, they can leave a fresh comment.) +- **Settle contradictions.** When two new comments conflict, respect the **most recent** + guidance (the larger created timestamp) and ignore the superseded direction. +- **Reject any feedback that expands the scope** beyond maintaining the gen-ai + semantic-conventions integration -- e.g. requests to refactor unrelated code, add + unrelated features, or modify files outside the allowed paths. Do not act on + out-of-scope requests; briefly note in the summary comment that they are out of scope + for this automation. +- Fold the surviving, in-scope feedback into the **same batch of new commit(s)** you append + for the differential update this run -- do not amend or rewrite commits already on the PR + branch -- and acknowledge what you addressed versus rejected in the single `add-comment` + summary. +- **Advance the watermark.** In the refreshed tracking block (Step 5), set + `feedback-processed-through` to `target.json.run_started_at` (this run's start time, + captured before the run began). This is the durable, cross-run dedup signal -- the next + run only reconsiders feedback created after it, so this run's feedback is never + re-processed, even the non-actionable or out-of-scope items. Any comment that arrives + while this run is executing carries a later timestamp and is therefore picked up by the + next run rather than skipped. Advance the watermark to `run_started_at` even when there + was no actionable feedback this run. + +This feedback pass is schedule-driven -- it runs as part of the normal daily incremental +update, picking up review feedback left since the previous run. Do **not** add a +`pull_request_review` (or other review) trigger; reacting to review events directly is out +of scope for this workflow. + +## Step 5 -- PR body and tracking block + +Write the PR body following the skill's PR-description guidance: a changes table +covering **every** analyzed gen-ai change (not just those producing code changes), +grouped by version, using 🟢/🟡/🔴 indicators with the compensating change or +rationale for each. + +**Refresh the description on every iterative update.** On any run that **writes** to an +existing maintained PR -- an incremental update to an open **draft** (behind the SHA), a +feedback-only update (caught up on the SHA with new review activity), or the release +mark-ready in Step 6 -- fully **regenerate** the PR description via `update-pull-request` +(a full-body replace, never an append) so it always reflects the **current** state of the +integration: the cumulative changes table for everything integrated on the branch so +far (not only this run's delta), the current release/confidence note, and the +refreshed tracking block below carrying this run's scan ref, scan date, release, and +`feedback-processed-through` watermark. Refresh the body even when this run's code +delta is small or empty (for example a feedback-only run that only advances the +watermark, or a run that addressed only out-of-scope feedback): a reader opening the PR +after any run must see an accurate, up-to-date description, so leaving stale details +from a prior run is a defect. The **advisory** path (behind the SHA on a **non-draft** +PR, Step 3) is the exception: it only posts a comment and must **not** update or +regenerate the body, so a PR a human has taken into review is left untouched. + +If you had to suffix the PR branch name with the run id because the canonical +`update-otel-genai-to-{target}` branch already existed on the remote (see Step 4's +fresh path), add a `> [!NOTE]` block near the top of the PR body stating that the +canonical branch name was already in use by a lingering branch, so this PR uses the +`update-otel-genai-to-{target}_{run_id}` branch instead. Omit the block entirely when +the canonical name was used. + +Include the skill's release-tagging confidence in the PR body. If the setup's +`release_detection_confidence` is `none`, say no published release signal exists yet. +If it is `high`, state that the workflow resolved the GitHub latest release tag to +`target.json.upstream_release_commit` and whether it matches the current scan and/or +the maintained PR. If the skill found a different release-tagging approach that is +clearly dependable, add this block near the top of the body, before the tracking tables: + +```markdown +> [!CAUTION] +> Release tagging signal may have changed: . +> Suggested follow-up: update to use instead of . +``` + +Embed the machine-readable tracking block verbatim (so future runs can read prior +state). Fill every field from Step 1: + +```yaml +# meai-otel-genai-worker:state:begin +upstream-repo: open-telemetry/semantic-conventions-genai +upstream-scan-ref: +upstream-scan-date: +upstream-release: +core-semconv-dependency: +dotnet-extensions-implemented-version: +feedback-processed-through: +# meai-otel-genai-worker:state:end +``` + +On every incremental update, refresh `upstream-scan-ref` and `upstream-scan-date` to +the values from the current run. Set `feedback-processed-through` to the `run_started_at` +value from `/tmp/gh-aw/agent/target.json` (this run's start time, captured before the run +began) whenever a maintained draft PR exists -- see Step 4's "Honoring reviewer feedback"; +on a **fresh** PR there is no prior feedback, so initialize it to the same `run_started_at`. +Carry the value forward unchanged only when there is no PR to maintain. + +## Step 6 -- When the upstream release is published + +If `target.json.release_ready` is `true` and a matching open PR has a pending release +action (the release gate in Step 3 routes here): +- Ensure the integration is complete and validated for the released version. The PR + already lives on its existing branch -- do **not** create a new branch; keep + pushing to it if final touch-ups are needed. +- Update the PR **title** so `{target}` resolves to the published `v{release}` + (e.g. `...to v1.42.0`) and set `upstream-release` to that version in the body. +- If the PR is still draft, mark it **Ready for Review** + (`mark-pull-request-as-ready-for-review`). If it is already ready, do not emit that + safe output. +- Add a comment stating the upstream release is published and the integration should + now be reviewed and merged. + +## Safe outputs and no-op rules + +- Use `create-pull-request` only on the fresh path. +- Use `push-to-pull-request-branch` only on the incremental path (behind open draft). It + **appends** the run's new commit(s) to the PR branch -- the maintained branch is only ever + added to, never force-pushed, amended, rebased, or reset, so its history stays a stable, + growing trail of incremental updates. +- Use `update-pull-request` on the incremental (open draft), feedback-only, and release + mark-ready paths to fully refresh (replace) an existing PR's description and title so the + body always reflects the current integrated state; do not skip the body refresh because + the code delta is small or empty. Do **not** use it on the behind non-draft **advisory** + path -- that path only comments, leaving a PR a human has taken into review untouched. +- Use `add-comment` for incremental summaries, advisory notes on behind non-draft + PRs, and the release-published note (Step 6). At most **one** comment per run. +- Use `mark-pull-request-as-ready-for-review` only when `target.json.release_ready` is + `true` and the PR is still draft. +- When the matching PR is already caught up with the upstream SHA (or any run needs + no visible change), do **not** post a no-op report, comment, issue, or PR. Emit the + `noop` safe output, and **also** write a short explanation to the GitHub Actions + **step summary** (see below). Do not create any repository-visible artifact. +- A no-op is valid in only two cases: (a) a matching PR's recorded `upstream-scan-ref` + equals the scanned upstream SHA **and** no release action is pending for that PR, or + (b) the completed Step 2 cross-reference + finds zero merged upstream gen-ai convention changes ahead of the implemented version. Case (b) + means **nothing upstream is unintegrated** -- it is **not** satisfied when upstream + carries merged changes that you classified as deferred (new attributes without an + emission site, uninstrumented capabilities, or doc-only clarifications). Those + deferred items are a non-zero delta: they require an open/maintained draft PR that + documents them, even though no constant is added for them yet. An unreleased upstream + (`upstream-release: none`), an `Unreleased` CHANGELOG section, or the absence of a + release tag are **never**, on their own, valid reasons to no-op. +- **No-op step summary:** whenever the run no-ops, append a concise Markdown + explanation to the file at `$GITHUB_STEP_SUMMARY` (for example + `echo "..." >> "$GITHUB_STEP_SUMMARY"`). This summary is attached to the workflow + run only -- it is **not** a repository-visible report. Include: the target + `{target}`, the scanned upstream SHA, the scan timestamp, which of the two no-op + conditions was met, and -- when condition (a) applies -- the matched PR number/URL + and its state (open draft / open / merged). diff --git a/.github/workflows/shared/pat_pool.README.md b/.github/workflows/shared/pat_pool.README.md new file mode 100644 index 00000000000..25cbabbc6a2 --- /dev/null +++ b/.github/workflows/shared/pat_pool.README.md @@ -0,0 +1,226 @@ +# PAT Pool + +Selects a random Copilot PAT from a numbered pool of secrets. This addresses limitations that arise from having a single PAT shared across all agentic workflows, such as rate-limiting. + +**This is a stop-gap workaround.** As soon as organization/enterprise billing is available to the dotnet org, this approach will be removed from our workflows. + +## Repository Onboarding + +To use Agentic Workflows in a dotnet org repository: + +1. Follow the instructions for [Configuring Your Repository | Agentic Authoring | GitHub Agentic Workflows][configure-repo]. Use `gh aw` **v0.71.5 or newer**, which supports the agent job dependencies required for this implementation. +2. Copy the `pat_pool.md` and `pat_pool.README.md` files into the repository under `.github/workflows/shared`. +3. Merge those additions into the repository and then follow the instructions for the PAT Creation and Usage below. + +**Install or upgrade the `gh aw` CLI and check the version** + +```sh +gh extension install github/gh-aw --force +gh aw --version +``` + +### Environment + +Create an environment for the agentic workflows: + - _Configuring these settings requires repo admin permission_ + - https://github.com/dotnet/{repo}/settings/environments + - Recommended Name: **copilot-pat-pool** + - Recommended Deployment branches and tags: **Protected branches only** + +This environment is used for all agentic workflows, restricting agentic workflows to the repo's protected branches and preventing the workflows from accessing secrets defined for other environments. + +## PAT Management + +Team members provide PATs into the pool with secret names matching the pattern of `{pool-name}_{0-9}`, such as `COPILOT_PAT_0`. + +[Use this link to prefill the PAT creation form with the required settings][create-pat]: + +1. **Resource owner** is your **user account**, not an organization. +2. **Copilot Requests (Read)** must be the only permission granted. +3. **8-day expiration** must be used, which enforces a weekly renewal. +4. **Repository access** set to **Public repositories** only. + +The **Token Name** _does not_ need to match the secret name and is only visible to the owner of the PAT. It's recommended to use a token name indicating the PAT is used for dotnet org agentic workflows. The **Description** is also only used for your own reference. + +Team members providing PATs for workflows should set weekly recurring reminders to regenerate and update their PATs in the PAT pool. With an 8-day expiration, renewal can be done on the same day each week. + +## PAT Pool Secrets + +For a PAT pool that is specific to an environment, PATs can be added to repositories as **Environment Secrets** for the environment created above. _This requires repo admin permission_. + +* **Settings** > + * **Environments** > + * **copilot-pat-pool** (or other environment name) > + * **Add environment secret** (or edit your existing secret) + * Enter your secret name of `COPILOT_PAT_{0-9}` and paste in your PAT + +This can also be accomplished using the `gh` CLI, specifying the repo and environment arguments. + +```sh +# Register the PAT secret. This will prompt for you to paste the PAT. +gh secret set "_<0-9>" --repo / --env "copilot-pat-pool" +``` + +It's also helpful to record who owns each PAT within the pool. To capture which team member is associated with each PAT, a `_<0-9>_` "sidecar secret" can be added alongside the PAT secret to make the username for the PAT pool entry visible. This sidecar secret must have a non-empty value, but it's never consumed, so any value is sufficient. + +```sh +# Record a sidecar secret that presents who owns this PAT. +gh secret set "_<0-9>_" --body "" --repo / --env "copilot-pat-pool" +``` + +## Workflow Output Attribution + +Team members' PATs are _only_ used for the Copilot requests from within the agentic portion of the workflow. All outputs from the workflow use the `github-actions[bot]` account token. Issues, PRs, comments, and all other content generated by the workflow will be attributed to `github-actions[bot]`--not the team member's account or token. + +## Usage + +The [`pat_pool.md`](./pat_pool.md) workflow import defines a custom job with a `pat_number` output. Consuming workflows need two additions to their frontmatter to import this job and use the PAT number to override the `COPILOT_GITHUB_TOKEN` passed to the workflow's agent job. + +```yml +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# Run agentic jobs in an isolated `copilot-pat-pool` environment. +# +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - uses: shared/pat_pool.md + with: + environment: copilot-pat-pool + +environment: copilot-pat-pool + +engine: + id: copilot + env: + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} +``` + +The `COPILOT_GITHUB_TOKEN` expression can be collapsed onto a single line if desired. `gh-aw compile` automatically wires `pat_pool` into the activation and agent jobs' `needs:` graph because of the `needs.pat_pool.` references within the `engine.env` property. + +```sh +gh aw compile --schedule-seed / +``` + +### Specifying the environment + +The `environment` must be specified both to the `pat_pool.md` import and to the containing workflow to ensure both jobs access the PAT pool from the same environment. The `copilot-pat-pool` environment name is recommended as the isolated environment for agentic workflows that use the PAT pool. + +### Customizing the pool + +The import declares 10 optional inputs (`COPILOT_PAT_0` through `COPILOT_PAT_9`), each defaulting to `secrets.COPILOT_PAT_#` of the matching number. To point a workflow at a different pool of repository secrets, pass the substitute secrets as the `COPILOT_PAT_#` inputs to the shared workflow. + +```yml +imports: + - uses: shared/pat_pool.md + with: + environment: copilot-pat-pool + COPILOT_PAT_0: ${{ secrets.MY_TEAM_PAT_0 }} + COPILOT_PAT_1: ${{ secrets.MY_TEAM_PAT_1 }} + # Unspecified inputs default to `secrets.COPILOT_PAT_#` lookups +``` + +The secrets passed via `with:` must match the secrets referenced in the consuming workflow's `case` expression that overrides `COPILOT_GITHUB_TOKEN`--both sides need to agree on which secret backs each `COPILOT_PAT_#` slot. Update the `case` expression accordingly: + +```yml +engine: + id: copilot + env: + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.MY_TEAM_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.MY_TEAM_PAT_1, ..., 'NO COPILOT PAT AVAILABLE') }} +``` + +This approach aligns with GitHub's documented guidance for [passing secrets][passing-secrets] between workflows, where the `pat_pool` job returns a PAT number and the `case` statement acts as a secret store to look the PAT secret up based on the selected number. + +## Design / Security + +There are several details of this implementation that keep our workflows and repositories safe. + +1. **Secrets adhere to existing trust boundaries.** The pool of PAT secrets is + provided to a dedicated step within the `pat_pool` job. That job runs + after `pre_activation` and contains only the trusted checkout and action + steps--no untrusted context or input is within scope. The + `select-pat-number` action only references the secret values to determine + which are non-empty, filtering the secret numbers to those with values. +1. **The `pat_pool` job emits only a number, never a secret.** Its sole output, + `pat_number`, is the 0-9 index of the selected PAT (or empty when the pool + is empty). The actual secret materializes only later, in the activation + job's `engine.env` mapping, where the `case()` expression resolves the + number to the matching secret. This follows GitHub's guidance for + [passing secrets][passing-secrets] between jobs or workflows, with the + `case` statement acting as a very simple secret store. +1. **The `select-pat-number` action does not require any permissions.** It + reads only the `COPILOT_PAT_#` environment variables passed to it and writes + only to `GITHUB_OUTPUT`. The job that hosts it sets `permissions:` to the + workflow defaults (no elevated scopes). +1. **The implementation uses supported Agentic Workflow extensibility hooks.** + Defining a custom job inside an [imported workflow file][imports] is + supported by `gh aw compile`. gh-aw automatically + wires `pat_pool` into the activation job's `needs:` graph based on the + `needs.pat_pool.outputs.pat_number` references in `engine.env`. The + [secret override][secret-override] capability supplies the `COPILOT_GITHUB_TOKEN` + value via `engine.env` rather than the default secret of the same name. + +Each of the references below contributed to the design and implementation to ensure a secure and reliable design. + +## Known Issues + +The `pat_pool` import integration requires that the workflow's compilation results in a `pre_activation` job. If nothing in your workflow definition produces a `pre_activation` job, a compilation error will be received. + +```text +✗ Failed workflows: + ✗ .md + +.github\workflows\.md:1:1: error: failed to generate YAML: failed to build and validate jobs: job dependency validation failed: job 'pat_pool' depends on non-existent job 'pre_activation' +``` + +To work around this, add `on.permissions: {}` to your workflow, which forces a no-op `pre_activation` job to be generated. + +```yml +on: + permissions: {} +``` + +See: [Activation 'needs' does not incorporate jobs in engine.env expressions (github/gh-aw#30790)](https://github.com/github/gh-aw/issues/30790) + +## References + +- [Agentic Workflows CLI Extension][cli-setup] +- [Agentic Authoring][configure-repo] +- [Authentication][authentication] +- [Agentic Workflow Imports][imports] +- [Custom Steps][steps] +- [Custom Jobs][jobs] +- [Job Outputs][job-outputs] +- [Engine Configuration][engine] +- [Engine Environment Variables][engine-vars] +- [Update agentic engine token handling to use user-provided secrets (github/gh-aw#18017)][secret-override] +- [Case Function in Workflow Expressions][case-expression] +- [Passing a secret between jobs or workflows][passing-secrets] + +[cli-setup]: https://github.github.com/gh-aw/setup/cli/ +[configure-repo]: https://github.github.com/gh-aw/guides/agentic-authoring/#configuring-your-repository +[authentication]: https://github.github.com/gh-aw/reference/auth/ +[create-pat]: https://github.com/settings/personal-access-tokens/new?name=dotnet%20org%20agentic%20workflows&description=GitHub+Agentic+Workflows+-+Copilot+engine+authentication.++Used+for+dotnet+org+workflows.+MUST+be+configured+with+only+Copilot+Requests+permissions+and+user+account+as+resource+owner.+Weekly+expiration+and+required+renewal.&user_copilot_requests=read&expires_in=8 +[imports]: https://github.github.com/gh-aw/reference/imports/ +[steps]: https://github.github.com/gh-aw/reference/frontmatter/#custom-steps-steps +[jobs]: https://github.github.com/gh-aw/reference/frontmatter/#custom-jobs-jobs +[job-outputs]: https://github.github.com/gh-aw/reference/frontmatter/#job-outputs +[engine]: https://github.github.com/gh-aw/reference/frontmatter/#ai-engine-engine +[engine-vars]: https://github.github.com/gh-aw/reference/engines/#engine-environment-variables +[secret-override]: https://github.com/github/gh-aw/pull/18017 +[case-expression]: https://docs.github.com/actions/reference/workflows-and-actions/expressions#case +[passing-secrets]: https://docs.github.com/actions/reference/workflows-and-actions/workflow-commands#example-masking-and-passing-a-secret-between-jobs-or-workflows diff --git a/.github/workflows/shared/pat_pool.md b/.github/workflows/shared/pat_pool.md new file mode 100644 index 00000000000..19a830cd01f --- /dev/null +++ b/.github/workflows/shared/pat_pool.md @@ -0,0 +1,123 @@ +--- +description: Agentic workflow import to integrate the Copilot PAT Pool + +jobs: + pat_pool: + environment: ${{ github.aw.import-inputs.environment }} + needs: [pre_activation] + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - id: select-pat-number + name: Select Copilot token from pool + env: + COPILOT_PAT_0: ${{ github.aw.import-inputs.COPILOT_PAT_0 }} + COPILOT_PAT_1: ${{ github.aw.import-inputs.COPILOT_PAT_1 }} + COPILOT_PAT_2: ${{ github.aw.import-inputs.COPILOT_PAT_2 }} + COPILOT_PAT_3: ${{ github.aw.import-inputs.COPILOT_PAT_3 }} + COPILOT_PAT_4: ${{ github.aw.import-inputs.COPILOT_PAT_4 }} + COPILOT_PAT_5: ${{ github.aw.import-inputs.COPILOT_PAT_5 }} + COPILOT_PAT_6: ${{ github.aw.import-inputs.COPILOT_PAT_6 }} + COPILOT_PAT_7: ${{ github.aw.import-inputs.COPILOT_PAT_7 }} + COPILOT_PAT_8: ${{ github.aw.import-inputs.COPILOT_PAT_8 }} + COPILOT_PAT_9: ${{ github.aw.import-inputs.COPILOT_PAT_9 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_9. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 9); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_9)" + echo "$warning_message" + exit 0 + fi + + # Select a random index. Seed only when a non-negative integer is provided, + # normalized to Bash's 0-32767 $RANDOM seed range -- assigning a non-integer to + # $RANDOM would break the arithmetic expansion below. + if printf '%s' "$RANDOM_SEED" | grep -Eq '^[0-9]+$'; then + RANDOM=$(( RANDOM_SEED % 32768 )) + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|8|9|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + +import-schema: + environment: + type: string + required: true + COPILOT_PAT_0: + type: string + required: false + default: ${{ secrets.COPILOT_PAT_0 }} + COPILOT_PAT_1: + type: string + required: false + default: ${{ secrets.COPILOT_PAT_1 }} + COPILOT_PAT_2: + type: string + required: false + default: ${{ secrets.COPILOT_PAT_2 }} + COPILOT_PAT_3: + type: string + required: false + default: ${{ secrets.COPILOT_PAT_3 }} + COPILOT_PAT_4: + type: string + required: false + default: ${{ secrets.COPILOT_PAT_4 }} + COPILOT_PAT_5: + type: string + required: false + default: ${{ secrets.COPILOT_PAT_5 }} + COPILOT_PAT_6: + type: string + required: false + default: ${{ secrets.COPILOT_PAT_6 }} + COPILOT_PAT_7: + type: string + required: false + default: ${{ secrets.COPILOT_PAT_7 }} + COPILOT_PAT_8: + type: string + required: false + default: ${{ secrets.COPILOT_PAT_8 }} + COPILOT_PAT_9: + type: string + required: false + default: ${{ secrets.COPILOT_PAT_9 }} + random_seed: + type: number + required: false + description: >- + Optional non-negative integer seed for deterministic PAT selection. Non-integer + values are ignored and the selection falls back to Bash's default randomness. +--- diff --git a/.github/workflows/validate-pat-pool.yml b/.github/workflows/validate-pat-pool.yml new file mode 100644 index 00000000000..e3f490074f7 --- /dev/null +++ b/.github/workflows/validate-pat-pool.yml @@ -0,0 +1,228 @@ +name: Validate PAT Pool + +on: + schedule: + - cron: '41 3 * * *' # Daily at ~3:41 AM UTC (off-round to reduce contention) + workflow_dispatch: + +# No GitHub API permissions needed +permissions: {} + +jobs: + validate: + environment: copilot-pat-pool + name: Validate Copilot PAT Pool + if: ${{ github.event_name == 'workflow_dispatch' || !github.event.repository.fork }} + runs-on: ubuntu-latest + env: + VALIDATE_PAT: | + if [ -z "$COPILOT_GITHUB_TOKEN" ]; then echo "status=empty" >> "$GITHUB_OUTPUT"; exit 0; fi + set +e; timeout 30 copilot --prompt "Say OK" --available-tools="" --silent --effort=low; rc=$?; set -e + if [ $rc -eq 0 ]; then echo "status=valid" >> "$GITHUB_OUTPUT" + elif [ $rc -eq 124 ]; then echo "status=unknown" >> "$GITHUB_OUTPUT" + else echo "status=invalid" >> "$GITHUB_OUTPUT"; fi + steps: + - name: Setup gh-aw scripts + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + + - name: Install Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 + + # ----------------------------------------------------------- + # Make a Copilot CLI request with each PAT. + # Each step sets COPILOT_GITHUB_TOKEN directly from the secret + # via env: so the value never passes through shell variables. + # ----------------------------------------------------------- + + - name: Validate COPILOT_PAT_0 + id: pat0 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_0 }} + shell: bash + run: | + # copilot --prompt "Say OK" + bash -c "$VALIDATE_PAT" + + - name: Validate COPILOT_PAT_1 + id: pat1 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_1 }} + shell: bash + run: | + # copilot --prompt "Say OK" + bash -c "$VALIDATE_PAT" + + - name: Validate COPILOT_PAT_2 + id: pat2 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_2 }} + shell: bash + run: | + # copilot --prompt "Say OK" + bash -c "$VALIDATE_PAT" + + - name: Validate COPILOT_PAT_3 + id: pat3 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_3 }} + shell: bash + run: | + # copilot --prompt "Say OK" + bash -c "$VALIDATE_PAT" + + - name: Validate COPILOT_PAT_4 + id: pat4 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_4 }} + shell: bash + run: | + # copilot --prompt "Say OK" + bash -c "$VALIDATE_PAT" + + - name: Validate COPILOT_PAT_5 + id: pat5 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_5 }} + shell: bash + run: | + # copilot --prompt "Say OK" + bash -c "$VALIDATE_PAT" + + - name: Validate COPILOT_PAT_6 + id: pat6 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_6 }} + shell: bash + run: | + # copilot --prompt "Say OK" + bash -c "$VALIDATE_PAT" + + - name: Validate COPILOT_PAT_7 + id: pat7 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_7 }} + shell: bash + run: | + # copilot --prompt "Say OK" + bash -c "$VALIDATE_PAT" + + - name: Validate COPILOT_PAT_8 + id: pat8 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_8 }} + shell: bash + run: | + # copilot --prompt "Say OK" + bash -c "$VALIDATE_PAT" + + - name: Validate COPILOT_PAT_9 + id: pat9 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_9 }} + shell: bash + run: | + # copilot --prompt "Say OK" + bash -c "$VALIDATE_PAT" + + # ----------------------------------------------------------- + # Collect results and build the step summary + # ----------------------------------------------------------- + + - name: Build summary + if: always() + env: + S0: ${{ steps.pat0.outputs.status }} + S1: ${{ steps.pat1.outputs.status }} + S2: ${{ steps.pat2.outputs.status }} + S3: ${{ steps.pat3.outputs.status }} + S4: ${{ steps.pat4.outputs.status }} + S5: ${{ steps.pat5.outputs.status }} + S6: ${{ steps.pat6.outputs.status }} + S7: ${{ steps.pat7.outputs.status }} + S8: ${{ steps.pat8.outputs.status }} + S9: ${{ steps.pat9.outputs.status }} + shell: bash + run: | + # Build summary + statuses=("$S0" "$S1" "$S2" "$S3" "$S4" "$S5" "$S6" "$S7" "$S8" "$S9") + + valid=0; empty=0; invalid=0; unknown=0 + for s in "${statuses[@]}"; do + case "$s" in + valid) valid=$((valid + 1)) ;; + empty) empty=$((empty + 1)) ;; + invalid) invalid=$((invalid + 1)) ;; + *) unknown=$((unknown + 1)) ;; + esac + done + + { + if [ $invalid -eq 0 ] && [ $unknown -eq 0 ] && [ $valid -gt 0 ]; then + echo "> [!NOTE]" + echo "> **PAT pool is valid** — no action needed" + echo "" + fi + + if [ $invalid -eq 0 ] && [ $unknown -eq 0 ] && [ $valid -eq 0 ]; then + echo "> [!WARNING]" + echo "> **Empty PAT pool** — agentic workflows will fall back to the default \`COPILOT_GITHUB_TOKEN\`." + echo "" + fi + + if [ $invalid -gt 0 ]; then + echo "> [!CAUTION]" + echo "> **Invalid PAT pool** — agentic workflows selecting an invalid PAT will fail." + echo "" + fi + + if [ $unknown -gt 0 ]; then + echo "> [!WARNING]" + echo "> **PAT pool not verified** due to transient errors — re-run the workflow to retry." + echo "" + fi + + echo "☑️ Valid: ${valid} • ⏹️ Empty: ${empty} • ❌ Invalid: ${invalid} • ❓ Unknown: ${unknown}" + echo "" + + echo "| PAT Secret | Status |" + echo "|:-----------|:-------|" + + for i in $(seq 0 9); do + case "${statuses[$i]}" in + valid) symbol="☑️ Valid" ;; + empty) symbol="⏹️ Empty" ;; + invalid) symbol="❌ Invalid" ;; + *) symbol="❓ Unknown" ;; + esac + echo "| \`COPILOT_PAT_${i}\` | ${symbol} |" + done + } >> "$GITHUB_STEP_SUMMARY" + + if [ $invalid -gt 0 ]; then + echo "::error::${invalid} PAT(s) in the pool are invalid and need to be removed or replaced" + exit 1 + fi + + if [ $unknown -gt 0 ]; then + echo "::error::${unknown} PAT(s) could not be verified due to transient errors — re-run to retry" + exit 1 + fi + + if [ $valid -eq 0 ]; then + echo "::error::The PAT pool is empty — no PATs are available" + exit 1 + fi + + echo "PAT pool validation passed: ${valid} valid PAT(s)" diff --git a/src/Analyzers/Microsoft.Analyzers.Extra/Utilities/SymbolExtensions.cs b/src/Analyzers/Microsoft.Analyzers.Extra/Utilities/SymbolExtensions.cs index bc8966ba8cf..078ab556cf6 100644 --- a/src/Analyzers/Microsoft.Analyzers.Extra/Utilities/SymbolExtensions.cs +++ b/src/Analyzers/Microsoft.Analyzers.Extra/Utilities/SymbolExtensions.cs @@ -32,7 +32,7 @@ public static bool IsAncestorOf(this ITypeSymbol potentialAncestor, ITypeSymbol } /// - /// True if the symbol is externally visible outside this assembly. + /// Determines whether the symbol is externally visible outside this assembly. /// public static bool IsExternallyVisible(this ISymbol symbol) { diff --git a/src/Analyzers/Microsoft.Analyzers.Local/Utilities/SymbolExtensions.cs b/src/Analyzers/Microsoft.Analyzers.Local/Utilities/SymbolExtensions.cs index f3665b80295..2cefcdfa7af 100644 --- a/src/Analyzers/Microsoft.Analyzers.Local/Utilities/SymbolExtensions.cs +++ b/src/Analyzers/Microsoft.Analyzers.Local/Utilities/SymbolExtensions.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.LocalAnalyzers.Utilities; internal static class SymbolExtensions { /// - /// True if the symbol is externally visible outside this assembly. + /// Determines whether the symbol is externally visible outside this assembly. /// public static bool IsExternallyVisible(this ISymbol symbol) { diff --git a/src/LegacySupport/CompilerFeatureRequiredAttribute/CompilerFeatureRequiredAttribute.cs b/src/LegacySupport/CompilerFeatureRequiredAttribute/CompilerFeatureRequiredAttribute.cs index b979931673c..49a55ac603d 100644 --- a/src/LegacySupport/CompilerFeatureRequiredAttribute/CompilerFeatureRequiredAttribute.cs +++ b/src/LegacySupport/CompilerFeatureRequiredAttribute/CompilerFeatureRequiredAttribute.cs @@ -17,12 +17,12 @@ public CompilerFeatureRequiredAttribute(string featureName) } /// - /// The name of the compiler feature. + /// Gets the name of the compiler feature. /// public string FeatureName { get; } /// - /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . + /// Gets a value indicating whether the compiler can choose to allow access to the location where this attribute is applied if it does not understand . /// public bool IsOptional { get; init; } diff --git a/src/LegacySupport/NullabilityInfoContext/NullabilityInfo.cs b/src/LegacySupport/NullabilityInfoContext/NullabilityInfo.cs index bd9b132cd0f..7d989ce128c 100644 --- a/src/LegacySupport/NullabilityInfoContext/NullabilityInfo.cs +++ b/src/LegacySupport/NullabilityInfoContext/NullabilityInfo.cs @@ -25,28 +25,28 @@ internal NullabilityInfo(Type type, NullabilityState readState, NullabilityState } /// - /// The of the member or generic parameter + /// Gets the of the member or generic parameter /// to which this NullabilityInfo belongs. /// public Type Type { get; } /// - /// The nullability read state of the member. + /// Gets the nullability read state of the member. /// public NullabilityState ReadState { get; internal set; } /// - /// The nullability write state of the member. + /// Gets the nullability write state of the member. /// public NullabilityState WriteState { get; internal set; } /// - /// If the member type is an array, gives the of the elements of the array, null otherwise. + /// Gets the of the elements of the array if the member type is an array; otherwise, . /// public NullabilityInfo? ElementType { get; } /// - /// If the member type is a generic type, gives the array of for each type parameter. + /// Gets the array of values for each type parameter if the member type is a generic type. /// public NullabilityInfo[] GenericTypeArguments { get; } } diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion/Chunkers/IngestionChunkerOptions.cs b/src/Libraries/Microsoft.Extensions.DataIngestion/Chunkers/IngestionChunkerOptions.cs index 294f4c92d27..7010bb9cc29 100644 --- a/src/Libraries/Microsoft.Extensions.DataIngestion/Chunkers/IngestionChunkerOptions.cs +++ b/src/Libraries/Microsoft.Extensions.DataIngestion/Chunkers/IngestionChunkerOptions.cs @@ -32,8 +32,11 @@ public IngestionChunkerOptions(Tokenizer tokenizer) public Tokenizer Tokenizer { get; } /// - /// Gets or sets the maximum number of tokens allowed in each chunk. Default is 2000. + /// Gets or sets the maximum number of tokens allowed in each chunk. /// + /// + /// The default is 2000. + /// public int MaxTokensPerChunk { get => field == default ? DefaultTokensPerChunk : field; @@ -51,8 +54,11 @@ public int MaxTokensPerChunk } /// - /// Gets or sets the number of overlapping tokens between consecutive chunks. Default is 500. + /// Gets or sets the number of overlapping tokens between consecutive chunks. /// + /// + /// The default is 500. + /// public int OverlapTokens { get diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion/Chunkers/ValueStringBuilder.cs b/src/Libraries/Microsoft.Extensions.DataIngestion/Chunkers/ValueStringBuilder.cs index 199d55262b3..2fd6c655c35 100644 --- a/src/Libraries/Microsoft.Extensions.DataIngestion/Chunkers/ValueStringBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.DataIngestion/Chunkers/ValueStringBuilder.cs @@ -69,7 +69,7 @@ public void NullTerminate() } /// - /// Get a pinnable reference to the builder. + /// Gets a pinnable reference to the builder. /// Does not ensure there is a null char after /// This overload is pattern matched in the C# 7.3+ compiler so you can omit /// the explicit method call, and write eg "fixed (char* c = builder)" diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion/Processors/EnricherOptions.cs b/src/Libraries/Microsoft.Extensions.DataIngestion/Processors/EnricherOptions.cs index 182e07d9c1f..204a4794d20 100644 --- a/src/Libraries/Microsoft.Extensions.DataIngestion/Processors/EnricherOptions.cs +++ b/src/Libraries/Microsoft.Extensions.DataIngestion/Processors/EnricherOptions.cs @@ -41,8 +41,11 @@ public EnricherOptions(IChatClient chatClient) public ILoggerFactory? LoggerFactory { get; set; } /// - /// Gets or sets the batch size for processing chunks. Default is 20. + /// Gets or sets the batch size for processing chunks. /// + /// + /// The default is 20. + /// public int BatchSize { get; set => field = Throw.IfLessThanOrEqual(value, 0); } = 20; internal EnricherOptions Clone() => new(ChatClient) diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion/Writers/VectorStoreWriterOptions.cs b/src/Libraries/Microsoft.Extensions.DataIngestion/Writers/VectorStoreWriterOptions.cs index cbc2036061a..3995ecb2815 100644 --- a/src/Libraries/Microsoft.Extensions.DataIngestion/Writers/VectorStoreWriterOptions.cs +++ b/src/Libraries/Microsoft.Extensions.DataIngestion/Writers/VectorStoreWriterOptions.cs @@ -11,8 +11,11 @@ namespace Microsoft.Extensions.DataIngestion; public sealed class VectorStoreWriterOptions { /// - /// Gets or sets the name of the collection. When not provided, "chunks" will be used. + /// Gets or sets the name of the collection. /// + /// + /// When not provided, "chunks" is used. + /// public string CollectionName { get => field ?? "chunks"; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Metrics/MetricCollector.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Metrics/MetricCollector.cs index 7102de8987c..30ff8461963 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Metrics/MetricCollector.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Metrics/MetricCollector.cs @@ -259,7 +259,7 @@ public async Task WaitForMeasurementsAsync(int minCount, TimeSpan timeout) } /// - /// Scan all registered observable instruments. + /// Records all registered observable instruments. /// public void RecordObservableInstruments() { diff --git a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/CollectionModel.cs b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/CollectionModel.cs index 10bc3d59f76..19093c30c7c 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/CollectionModel.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/CollectionModel.cs @@ -84,8 +84,10 @@ internal CollectionModel( /// /// Gets the single vector property in the model, and throws if there are multiple vector properties. - /// Suitable for providers where validation is in place for single vectors only (). /// + /// + /// This is suitable for providers where validation is in place for single vectors only (). + /// public VectorPropertyModel VectorProperty => _singleVectorProperty ??= VectorProperties.Single(); // TODO: the pattern of first instantiating via parameterless constructor and then populating the properties isn't compatible diff --git a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/CollectionModelBuildingOptions.cs b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/CollectionModelBuildingOptions.cs index 2e59613d7c6..e02ff429fd6 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/CollectionModelBuildingOptions.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/CollectionModelBuildingOptions.cs @@ -29,8 +29,10 @@ public sealed class CollectionModelBuildingOptions public bool UsesExternalSerializer { get; init; } /// - /// Gets the special, reserved name for the key property of the database. - /// When set, the model builder manages the key storage name, and users cannot customize it. + /// Gets or initializes the special, reserved name for the key property of the database. /// + /// + /// When set, the model builder manages the key storage name, and users cannot customize it. + /// public string? ReservedKeyStorageName { get; init; } } diff --git a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs index 6c4ef7357f4..d3e93bdc9f0 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs @@ -21,10 +21,12 @@ public class KeyPropertyModel(string modelName, Type type) : PropertyModel(model /// /// Gets or sets the name that the JSON serializer will produce for this key property. - /// This is needed for providers that use an external JSON serializer combined with a reserved key storage name - /// (e.g. CosmosDB NoSQL uses "id"): the serializer produces a JSON object with the policy-transformed name, and - /// the provider needs to find and replace it with the reserved storage name. /// + /// + /// This is needed for providers that use an external JSON serializer combined with a reserved key storage name + /// (for example, CosmosDB NoSQL uses "id"): the serializer produces a JSON object with the + /// policy-transformed name, and the provider needs to find and replace it with the reserved storage name. + /// public string? SerializedKeyName { get; set; } /// diff --git a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/PropertyModel.cs b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/PropertyModel.cs index f9f25432b36..ca256affbf6 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/PropertyModel.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/ProviderServices/PropertyModel.cs @@ -21,13 +21,19 @@ public abstract class PropertyModel(string modelName, Type type) private Action? _setter; /// - /// Gets or sets the model name of the property. If the property corresponds to a .NET property, this name is the name of that property. + /// Gets or sets the model name of the property. /// + /// + /// If the property corresponds to a .NET property, this name is the name of that property. + /// public string ModelName { get; set; } = modelName; /// - /// Gets or sets the storage name of the property. This is the name to which the property is mapped in the vector store. + /// Gets or sets the storage name of the property. /// + /// + /// This is the name to which the property is mapped in the vector store. + /// public string StorageName { get => field ?? ModelName; @@ -57,11 +63,14 @@ public string StorageName public Dictionary? ProviderAnnotations { get; set; } /// - /// Gets a value indicating whether the property type is nullable. For value types, this is when the type is - /// . For reference types on .NET 6+, this uses NRT annotations via - /// NullabilityInfoContext when a is available - /// (i.e., POCO mapping); otherwise, reference types are assumed nullable. + /// Gets a value indicating whether the property type is nullable. /// + /// + /// For value types, this is when the type is . + /// For reference types on .NET 6+, this uses NRT annotations via NullabilityInfoContext + /// when a is available (that is, during POCO mapping); + /// otherwise, reference types are assumed nullable. + /// public bool IsNullable { get diff --git a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/VectorStoreCollectionMetadata.cs b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/VectorStoreCollectionMetadata.cs index 60678df1c7b..4c65f8d4d5b 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/VectorStoreCollectionMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/VectorStoreCollectionMetadata.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.VectorData; /// Provides metadata about an . public class VectorStoreCollectionMetadata { - /// Gets the name of the vector store system. + /// Gets or initializes the name of the vector store system. /// /// Where possible, this value maps to the "db.system.name" attribute defined in the /// OpenTelemetry Semantic Conventions for database calls and systems; see . @@ -15,12 +15,12 @@ public class VectorStoreCollectionMetadata public string? VectorStoreSystemName { get; init; } /// - /// Gets the name of the vector store (database). + /// Gets or initializes the name of the vector store (database). /// public string? VectorStoreName { get; init; } /// - /// Gets the name of a collection (table, container) within the vector store (database). + /// Gets or initializes the name of a collection (table, container) within the vector store (database). /// public string? CollectionName { get; init; } } diff --git a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/VectorStoreException.cs b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/VectorStoreException.cs index 9961f106569..32958c277ca 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/VectorStoreException.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/VectorStoreException.cs @@ -36,7 +36,7 @@ public VectorStoreException(string? message, Exception? innerException) { } - /// Gets the name of the vector store system. + /// Gets or initializes the name of the vector store system. /// /// Where possible, this value maps to the "db.system.name" attribute defined in the /// OpenTelemetry Semantic Conventions for database calls and systems; see . @@ -45,17 +45,17 @@ public VectorStoreException(string? message, Exception? innerException) public string? VectorStoreSystemName { get; init; } /// - /// Gets the name of the vector store (database). + /// Gets or initializes the name of the vector store (database). /// public string? VectorStoreName { get; init; } /// - /// Gets the name of the vector store collection that the failing operation was performed on. + /// Gets or initializes the name of the vector store collection that the failing operation was performed on. /// public string? CollectionName { get; init; } /// - /// Gets the name of the vector store operation that failed. + /// Gets or initializes the name of the vector store operation that failed. /// public string? OperationName { get; init; } } diff --git a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/VectorStoreMetadata.cs b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/VectorStoreMetadata.cs index 73091776eb9..6cdf0c57deb 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/VectorStoreMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.Abstractions/VectorStoreMetadata.cs @@ -15,7 +15,7 @@ public class VectorStoreMetadata public string? VectorStoreSystemName { get; init; } /// - /// Gets the name of the vector store (database). + /// Gets or initializes the name of the vector store (database). /// public string? VectorStoreName { get; init; } } diff --git a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/CollectionManagementTests.cs b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/CollectionManagementTests.cs index ebaddb7ec35..f6ff4ed9de7 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/CollectionManagementTests.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/CollectionManagementTests.cs @@ -14,7 +14,7 @@ public Task InitializeAsync() => fixture.VectorStore.EnsureCollectionDeletedAsync(CollectionName); [Fact] - public async Task Collection_Ensure_Exists_Delete() + public virtual async Task Collection_Ensure_Exists_Delete() { var collection = GetCollection(); @@ -29,7 +29,7 @@ public async Task Collection_Ensure_Exists_Delete() } [Fact] - public async Task EnsureCollectionExists_twice_does_not_throw() + public virtual async Task EnsureCollectionExists_twice_does_not_throw() { var collection = GetCollection(); @@ -39,7 +39,7 @@ public async Task EnsureCollectionExists_twice_does_not_throw() } [Fact] - public async Task Store_CollectionExists() + public virtual async Task Store_CollectionExists() { var store = fixture.VectorStore; var collection = GetCollection(); @@ -50,7 +50,7 @@ public async Task Store_CollectionExists() } [Fact] - public async Task Store_DeleteCollection() + public virtual async Task Store_DeleteCollection() { var store = fixture.VectorStore; var collection = GetCollection(); @@ -61,7 +61,7 @@ public async Task Store_DeleteCollection() } [Fact] - public async Task Store_ListCollections() + public virtual async Task Store_ListCollections() { var store = fixture.VectorStore; var collection = GetCollection(); @@ -75,7 +75,7 @@ public async Task Store_ListCollections() } [Fact] - public void Collection_metadata() + public virtual void Collection_metadata() { var collection = GetCollection(); diff --git a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/DependencyInjectionTests.cs b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/DependencyInjectionTests.cs index ae0aa1ab210..d7568d9192c 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/DependencyInjectionTests.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/DependencyInjectionTests.cs @@ -31,7 +31,7 @@ public abstract class DependencyInjectionTests> CollectionDelegates { get; } [Fact] - public void ServiceCollectionCantBeNull() + public virtual void ServiceCollectionCantBeNull() { foreach (var registrationDelegate in StoreDelegates) { @@ -47,7 +47,7 @@ public void ServiceCollectionCantBeNull() } [Fact] - public void CollectionNameCantBeNullOrEmpty() + public virtual void CollectionNameCantBeNullOrEmpty() { const string EmptyCollectionName = ""; @@ -84,7 +84,7 @@ public virtual void CanRegisterVectorStore(ServiceLifetime lifetime, object? ser [Theory] [MemberData(nameof(LifetimesAndServiceKeys))] - public void CanRegisterCollections(ServiceLifetime lifetime, object? serviceKey) + public virtual void CanRegisterCollections(ServiceLifetime lifetime, object? serviceKey) { foreach (var registrationDelegate in CollectionDelegates) { @@ -136,7 +136,7 @@ public virtual void CanRegisterConcreteTypeVectorStoreAfterSomeAbstractionHasBee [Theory] [MemberData(nameof(LifetimesAndServiceKeys))] - public void CanRegisterConcreteTypeCollectionsAfterSomeAbstractionHasBeenRegistered(ServiceLifetime lifetime, object? serviceKey) + public virtual void CanRegisterConcreteTypeCollectionsAfterSomeAbstractionHasBeenRegistered(ServiceLifetime lifetime, object? serviceKey) { foreach (var registrationDelegate in CollectionDelegates) { @@ -156,7 +156,7 @@ public void CanRegisterConcreteTypeCollectionsAfterSomeAbstractionHasBeenRegiste [Theory] [MemberData(nameof(LifetimesAndServiceKeys))] - public void EmbeddingGeneratorIsResolved(ServiceLifetime lifetime, object? serviceKey) + public virtual void EmbeddingGeneratorIsResolved(ServiceLifetime lifetime, object? serviceKey) { foreach (var registrationDelegate in CollectionDelegates) { diff --git a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/HybridSearchTests.cs b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/HybridSearchTests.cs index 44f64188ca8..6fd4ce6587c 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/HybridSearchTests.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/HybridSearchTests.cs @@ -17,7 +17,7 @@ public abstract class HybridSearchTests( where TKey : notnull { [Fact] - public async Task HybridSearchAsync() + public virtual async Task HybridSearchAsync() { // Arrange var vector = new ReadOnlyMemory([1, 0, 0, 0]); @@ -33,7 +33,7 @@ public async Task HybridSearchAsync() } [Fact] - public async Task HybridSearchAsync_with_filter() + public virtual async Task HybridSearchAsync_with_filter() { // Arrange var vector = new ReadOnlyMemory([1, 0, 0, 0]); @@ -54,7 +54,7 @@ public async Task HybridSearchAsync_with_filter() } [Fact] - public async Task HybridSearchAsync_with_top() + public virtual async Task HybridSearchAsync_with_top() { // Arrange var vector = new ReadOnlyMemory([1, 0, 0, 0]); @@ -71,7 +71,7 @@ public async Task HybridSearchAsync_with_top() } [Fact] - public async Task HybridSearchAsync_with_Skip() + public virtual async Task HybridSearchAsync_with_Skip() { // Arrange var vector = new ReadOnlyMemory([1, 0, 0, 0]); @@ -88,7 +88,7 @@ public async Task HybridSearchAsync_with_Skip() } [Fact] - public async Task HybridSearchAsync_with_multiple_keywords_ranks_matched_keywords_higher() + public virtual async Task HybridSearchAsync_with_multiple_keywords_ranks_matched_keywords_higher() { // Arrange var vector = new ReadOnlyMemory([1, 0, 0, 0]); @@ -105,7 +105,7 @@ public async Task HybridSearchAsync_with_multiple_keywords_ranks_matched_keyword } [Fact] - public async Task HybridSearchAsync_with_multiple_text_properties() + public virtual async Task HybridSearchAsync_with_multiple_text_properties() { // Arrange var vector = new ReadOnlyMemory([1, 0, 0, 0]); @@ -131,7 +131,7 @@ public async Task HybridSearchAsync_with_multiple_text_properties() } [Fact] - public Task HybridSearchAsync_without_explicitly_specified_property_fails() + public virtual Task HybridSearchAsync_without_explicitly_specified_property_fails() => Assert.ThrowsAsync(async () => await multiTextFixture.HybridSearchable .HybridSearchAsync(new ReadOnlyMemory([1, 0, 0, 0]), ["Apples"], top: 3) diff --git a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/ModelTests/DynamicModelTests.cs b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/ModelTests/DynamicModelTests.cs index 48ba8be4b90..8d13ec8fceb 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/ModelTests/DynamicModelTests.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/ModelTests/DynamicModelTests.cs @@ -284,7 +284,7 @@ public virtual async Task Insert_multiple_records() #region Delete [Fact] - public async Task Delete_single_record() + public virtual async Task Delete_single_record() { var recordToRemove = fixture.TestData[2]; diff --git a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/Support/TestStore.cs b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/Support/TestStore.cs index e8ec5215f66..98b3c28b1f4 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/Support/TestStore.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/Support/TestStore.cs @@ -16,13 +16,13 @@ public abstract class TestStore private VectorStore? _defaultVectorStore; /// - /// Some databases modify vectors on upsert, e.g. normalizing them, so vectors - /// returned cannot be compared with the original ones. + /// Gets a value indicating whether vectors returned from the database can be + /// compared with the original vectors. /// public virtual bool VectorsComparable => true; /// - /// Whether the database supports filtering by score threshold in vector search. + /// Gets a value indicating whether the database supports filtering by score threshold in vector search. /// public virtual bool SupportsScoreThreshold => true; @@ -95,8 +95,10 @@ public virtual string AdjustCollectionName(string baseName) /// /// Creates a collection for the given name and definition. - /// Override this to provide provider-specific collection options (e.g., partition key configuration). /// + /// + /// Override this to provide provider-specific collection options, such as partition key configuration. + /// public virtual VectorStoreCollection CreateCollection( string name, VectorStoreCollectionDefinition definition) @@ -106,8 +108,10 @@ public virtual VectorStoreCollection CreateCollection /// Creates a dynamic collection for the given name and definition. - /// Override this to provide provider-specific collection options (e.g., partition key configuration). /// + /// + /// Override this to provide provider-specific collection options, such as partition key configuration. + /// public virtual VectorStoreCollection> CreateDynamicCollection( string name, VectorStoreCollectionDefinition definition) diff --git a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/Support/VectorStoreCollectionFixtureBase.cs b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/Support/VectorStoreCollectionFixtureBase.cs index 1caf31ef99a..ec2ce898629 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/Support/VectorStoreCollectionFixtureBase.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/Support/VectorStoreCollectionFixtureBase.cs @@ -25,7 +25,7 @@ public abstract class VectorStoreCollectionFixtureBase : VectorSt protected abstract string CollectionNameBase { get; } /// - /// The actual name of the test collection, after any provider-specific collection naming rules have been applied. + /// Gets the actual name of the test collection after any provider-specific collection naming rules have been applied. /// public virtual string CollectionName => TestStore.AdjustCollectionName(CollectionNameBase); diff --git a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/Support/VectorStoreFixture.cs b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/Support/VectorStoreFixture.cs index 5702abce498..9c7a1002abb 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/Support/VectorStoreFixture.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/Support/VectorStoreFixture.cs @@ -27,8 +27,10 @@ public virtual TKey GenerateNextKey() /// /// Creates a collection for the given name and definition. - /// Delegates to which can be overridden for provider-specific options. /// + /// + /// This delegates to , which can be overridden for provider-specific options. + /// public virtual VectorStoreCollection CreateCollection(string name, VectorStoreCollectionDefinition definition) where TKey : notnull where TRecord : class @@ -36,8 +38,10 @@ public virtual VectorStoreCollection CreateCollection /// Creates a dynamic collection for the given name and definition. - /// Delegates to which can be overridden for provider-specific options. /// + /// + /// This delegates to , which can be overridden for provider-specific options. + /// public virtual VectorStoreCollection> CreateDynamicCollection(string name, VectorStoreCollectionDefinition definition) => TestStore.CreateDynamicCollection(name, definition); } diff --git a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/TypeTests/DataTypeTests.cs b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/TypeTests/DataTypeTests.cs index e6f0c992ced..bdc99a62d52 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/TypeTests/DataTypeTests.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/TypeTests/DataTypeTests.cs @@ -440,9 +440,11 @@ public abstract class Fixture : VectorStoreCollectionFixture private readonly IList _defaultDataProperties; /// - /// Whether the recreate the collection while testing, as opposed to deleting the records. - /// Necessary for InMemory, where the .NET mapped on the collection cannot be changed. + /// Gets a value indicating whether the collection is recreated while testing instead of deleting the records. /// + /// + /// This is necessary for InMemory, where the .NET type mapped on the collection cannot be changed. + /// public virtual bool RecreateCollection => false; #pragma warning disable CA2214 // Do not call overridable methods in constructors diff --git a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/TypeTests/EmbeddingTypeTests.cs b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/TypeTests/EmbeddingTypeTests.cs index 941151734dc..ee9bc56fb90 100644 --- a/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/TypeTests/EmbeddingTypeTests.cs +++ b/src/Libraries/Microsoft.Extensions.VectorData.ConformanceTests/TypeTests/EmbeddingTypeTests.cs @@ -242,16 +242,20 @@ public virtual VectorStoreCollectionDefinition CreateRecordDefinition - /// Whether the recreate the collection while testing, as opposed to deleting the records. - /// Necessary for InMemory, where the .NET mapped on the collection cannot be changed. + /// Gets a value indicating whether the collection is recreated while testing instead of deleting the records. /// + /// + /// This is necessary for InMemory, where the .NET type mapped on the collection cannot be changed. + /// public virtual bool RecreateCollection => false; /// - /// Whether to assert that no vectors were loaded when embedding generation is used. - /// Necessary for InMemory which returns the same object which was inserted, and therefore contains - /// the original input value. + /// Gets a value indicating whether to assert that no vectors were loaded when embedding generation is used. /// + /// + /// Asserting that no vectors were loaded is necessary for InMemory, which returns the same object that was inserted and therefore contains + /// the original input value. + /// public virtual bool AssertNoVectorsLoadedWithEmbeddingGeneration => true; } }