diff --git a/.github/scripts/agent-framework-discover.cs b/.github/scripts/agent-framework-discover.cs new file mode 100644 index 00000000000..bb5ed27bfe0 --- /dev/null +++ b/.github/scripts/agent-framework-discover.cs @@ -0,0 +1,177 @@ +#!/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); + +// Newest by SemVer 2.0 order, or null when the sequence is empty -- e.g. a transient feed/API +// error left a package (or the anchor) with no versions. Explicit LastOrDefault-after-ordering so +// the empty case is obvious and callers emit a controlled Fail()/null, rather than relying on the +// reference-type Enumerable.Max() returning null on empty. +static NuGetVersion? Newest(IEnumerable vs) => vs.OrderBy(v => v).LastOrDefault(); + +// ---- release_version = newest STABLE of the anchor ------------------------------------------ +var anchorVersions = versionsById.TryGetValue(Anchor, out var av) ? av : []; +var release = Newest(anchorVersions.Where(v => !v.IsPrerelease)); +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 = Newest(vs.Where(AtRelease)); + var newest = Newest(vs); + var newestStable = Newest(vs.Where(v => !v.IsPrerelease)); + + 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/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/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..c8ecd4533f8 --- /dev/null +++ b/.github/workflows/agent-framework-worker.lock.yml @@ -0,0 +1,2053 @@ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"df35af94545d37b1d73d5587517f9873e155d0fbc013a7f2b681eddf4027845b","body_hash":"099ed149198914e9ffade584f1ff5219fb7ccfbabcbc6a653cb180f5443b40ca","compiler_version":"v0.82.6","strict":true,"agent_id":"copilot","detection_agent_id":"copilot","engine_versions":{"copilot":"1.0.68"},"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":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"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":"9a946fdbd5fb07b82b2f5a4466058b876ab72bb2"},{"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":"cec6394202d7db187b02310d928812194988eb20","version":"v0.82.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.27","digest":"sha256:bb5a0150dcff1cddf9b8045bb411b7759806bace0abcb132fb22158073e155d9","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.27@sha256:bb5a0150dcff1cddf9b8045bb411b7759806bace0abcb132fb22158073e155d9"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.27","digest":"sha256:01e58c4383fa9952abe76e0a134a27c970f81f744d6b7861fc9e08b7964d94c3","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.27@sha256:01e58c4383fa9952abe76e0a134a27c970f81f744d6b7861fc9e08b7964d94c3"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.27","digest":"sha256:70df326caf73bf5911340dca4620b529a483dd8f42142b0a41d7b9761ab4ab7a","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.27@sha256:70df326caf73bf5911340dca4620b529a483dd8f42142b0a41d7b9761ab4ab7a"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.27","digest":"sha256:92d820df47b2eff75d93a5bec4dc183a3ec55ed7ddb4f25cb0fdda5c3e995409","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.27@sha256:92d820df47b2eff75d93a5bec4dc183a3ec55ed7ddb4f25cb0fdda5c3e995409"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.0","digest":"sha256:9dbdf42842c224a95016df1d2a85a2901e04204c242079343b302a307d2b8031","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.0@sha256:9dbdf42842c224a95016df1d2a85a2901e04204c242079343b302a307d2b8031"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# This file was automatically generated by gh-aw (v0.82.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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 +# - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # 9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@cec6394202d7db187b02310d928812194988eb20 # v0.82.6 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.27.27@sha256:bb5a0150dcff1cddf9b8045bb411b7759806bace0abcb132fb22158073e155d9 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.27@sha256:01e58c4383fa9952abe76e0a134a27c970f81f744d6b7861fc9e08b7964d94c3 +# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.27@sha256:70df326caf73bf5911340dca4620b529a483dd8f42142b0a41d7b9761ab4ab7a +# - ghcr.io/github/gh-aw-firewall/squid:0.27.27@sha256:92d820df47b2eff75d93a5bec4dc183a3ec55ed7ddb4f25cb0fdda5c3e995409 +# - ghcr.io/github/gh-aw-mcpg:v0.4.0@sha256:9dbdf42842c224a95016df1d2a85a2901e04204c242079343b302a307d2b8031 +# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 + +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@cec6394202d7db187b02310d928812194988eb20 # v0.82.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.68" + GH_AW_INFO_AWF_VERSION: "v0.27.27" + 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.68" + GH_AW_INFO_AGENT_VERSION: "1.0.68" + GH_AW_INFO_CLI_VERSION: "v0.82.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.27" + 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 }} + GH_AW_INFO_FEATURES: '{"integrity-reactions":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/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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + 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: Check for OAuth tokens + id: check-oauth-tokens + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" + 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') }} + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + - 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.82.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 }} + http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} + 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@cec6394202d7db187b02310d928812194988eb20 # v0.82.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.68" + GH_AW_INFO_AWF_VERSION: "v0.27.27" + 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 }} + GITHUB_API_URL: ${{ github.api_url }} + GH_HOST: ${{ env.GH_HOST }} + GITHUB_HOST: ${{ env.GITHUB_HOST }} + GITHUB_ENTERPRISE_HOST: ${{ env.GITHUB_ENTERPRISE_HOST }} + GITHUB_GRAPHQL_URL: ${{ env.GITHUB_GRAPHQL_URL }} + GITHUB_COPILOT_BASE_URL: ${{ env.GITHUB_COPILOT_BASE_URL }} + GH_AW_NETWORK_ISOLATION: 'true' + 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.4.0' + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/start_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: Setup .NET + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # 9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 + 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.68 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.27 --rootless + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + GH_AW_GITHUB_MIN_INTEGRITY: 'approved' + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - 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: 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.27@sha256:bb5a0150dcff1cddf9b8045bb411b7759806bace0abcb132fb22158073e155d9 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.27@sha256:01e58c4383fa9952abe76e0a134a27c970f81f744d6b7861fc9e08b7964d94c3 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.27@sha256:70df326caf73bf5911340dca4620b529a483dd8f42142b0a41d7b9761ab4ab7a ghcr.io/github/gh-aw-firewall/squid:0.27.27@sha256:92d820df47b2eff75d93a5bec4dc183a3ec55ed7ddb4f25cb0fdda5c3e995409 ghcr.io/github/gh-aw-mcpg:v0.4.0@sha256:9dbdf42842c224a95016df1d2a85a2901e04204c242079343b302a307d2b8031 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 + - 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_23bea722adf8f888_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,"required_labels":["automation","area-ai-templates"],"target":"*","update_branch":false}} + GH_AW_SAFE_OUTPUTS_CONFIG_23bea722adf8f888_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: *. Only PRs with labels [automation area-ai-templates] can be updated." + }, + "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="awmg-mcpg" + 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" + export GH_AW_MCP_CLI_SERVERS='["safeoutputs"]' + 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 bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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.4.0' + + 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_872cbac985b9e552_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_SHA": "\${GITHUB_SHA}", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", + "RUNNER_TEMP": "\${RUNNER_TEMP}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ], + "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_872cbac985b9e552_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 }} + GITHUB_API_URL: ${{ github.api_url }} + GH_HOST: ${{ env.GH_HOST }} + GITHUB_HOST: ${{ env.GITHUB_HOST }} + GITHUB_ENTERPRISE_HOST: ${{ env.GITHUB_ENTERPRISE_HOST }} + GITHUB_GRAPHQL_URL: ${{ env.GITHUB_GRAPHQL_URL }} + GITHUB_COPILOT_BASE_URL: ${{ env.GITHUB_COPILOT_BASE_URL }} + GH_AW_NETWORK_ISOLATION: 'true' + 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.4.0' + 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.27/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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\",\"awmg-cli-proxy\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"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*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"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\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.27,squid=sha256:92d820df47b2eff75d93a5bec4dc183a3ec55ed7ddb4f25cb0fdda5c3e995409,agent=sha256:bb5a0150dcff1cddf9b8045bb411b7759806bace0abcb132fb22158073e155d9,api-proxy=sha256:01e58c4383fa9952abe76e0a134a27c970f81f744d6b7861fc9e08b7964d94c3,cli-proxy=sha256:70df326caf73bf5911340dca4620b529a483dd8f42142b0a41d7b9761ab4ab7a\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${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 + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/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,SC2016,SC2086 + 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"} --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 --skip-pull --difc-proxy-host awmg-cli-proxy: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.82.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: | + # Best-effort permission fix for artifact upload (AWF cleanup may not have run) + sudo -n chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || 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\n# Validate exactly as agent-framework-worker-setup.sh reads the block next run: the begin/end\n# markers must be whole trimmed lines, and the recorded values are read from inside that block.\n# A looser substring match could pass here yet leave the body unparseable next run, wedging the\n# state machine -- so mirror that parser (whole-line markers + block extraction) and require the\n# recorded agent-framework-version and feedback-processed-through to be present within the block.\nmarker=\"agent-framework-template\"\ntracking_block() {\n awk -v b=\"# ${marker}:state:begin\" -v e=\"# ${marker}:state:end\" '\n { t=$0; sub(/\\r$/,\"\",t); gsub(/^[[:space:]]+|[[:space:]]+$/,\"\",t) }\n t == b { inb=1; buf=$0 ORS; next }\n inb { buf=buf $0 ORS; if (t == e) { inb=0; last=buf } }\n END { if (inb) last=buf; printf \"%s\", last }'\n}\ntracking_value() {\n sed -n \"s/^[[:space:]]*[-*+>]*[[:space:]]*$1:[[:space:]]*//p\" |\n head -1 | tr -d '\"'\\''\\r' | sed 's/[[:space:]]*#.*$//; s/[[:space:]]*$//'\n}\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 block=$(printf '%s' \"$body\" | tracking_block)\n miss=\"\"\n if [ -z \"$block\" ]; then\n miss=\"$miss state-block(whole-line begin/end markers)\"\n else\n [ -n \"$(printf '%s' \"$block\" | tracking_value agent-framework-version)\" ] || miss=\"$miss agent-framework-version\"\n [ -n \"$(printf '%s' \"$block\" | tracking_value feedback-processed-through)\" ] || miss=\"$miss feedback-processed-through\"\n fi\n if [ -n \"$miss\" ]; then\n echo \"::error::$typ item #$i is missing required tracking identity:$miss\"\n rc=1\n fi\ndone <<< \"$idx\"\nif [ \"$rc\" -ne 0 ]; then\n # The generated safe-output job intentionally runs after failed agents to report\n # valid partial outputs. Quarantine this invalid mutation so it cannot be published.\n cp \"$out\" /tmp/gh-aw/agent/rejected_agent_output.json\n printf '%s\\n' '{\"items\":[]}' > \"$out\"\n echo \"::error::Rejected invalid agent output; downstream safe outputs will receive no items.\"\n exit \"$rc\"\nfi\necho \"Agent output identity validated.\"" + + - 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@cec6394202d7db187b02310d928812194988eb20 # v0.82.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.68" + GH_AW_INFO_AWF_VERSION: "v0.27.27" + 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 safe outputs items manifest + id: download-safe-outputs-manifest + if: always() + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + path: /tmp/gh-aw/ + - 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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + 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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + 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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_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@cec6394202d7db187b02310d928812194988eb20 # v0.82.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.68" + GH_AW_INFO_AWF_VERSION: "v0.27.27" + 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.27@sha256:bb5a0150dcff1cddf9b8045bb411b7759806bace0abcb132fb22158073e155d9 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.27@sha256:01e58c4383fa9952abe76e0a134a27c970f81f744d6b7861fc9e08b7964d94c3 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.27@sha256:70df326caf73bf5911340dca4620b529a483dd8f42142b0a41d7b9761ab4ab7a ghcr.io/github/gh-aw-firewall/squid:0.27.27@sha256:92d820df47b2eff75d93a5bec4dc183a3ec55ed7ddb4f25cb0fdda5c3e995409 + - 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.68 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.27 + - 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.27/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.27,squid=sha256:92d820df47b2eff75d93a5bec4dc183a3ec55ed7ddb4f25cb0fdda5c3e995409,agent=sha256:bb5a0150dcff1cddf9b8045bb411b7759806bace0abcb132fb22158073e155d9,api-proxy=sha256:01e58c4383fa9952abe76e0a134a27c970f81f744d6b7861fc9e08b7964d94c3,cli-proxy=sha256:70df326caf73bf5911340dca4620b529a483dd8f42142b0a41d7b9761ab4ab7a\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${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 + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/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" > "${RUNNER_TEMP}/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,SC2016,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"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --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.82.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@cec6394202d7db187b02310d928812194988eb20 # v0.82.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.68" + GH_AW_INFO_AWF_VERSION: "v0.27.27" + 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.68" + 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@cec6394202d7db187b02310d928812194988eb20 # v0.82.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.68" + GH_AW_INFO_AWF_VERSION: "v0.27.27" + 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,\"required_labels\":[\"automation\",\"area-ai-templates\"],\"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..7c6bdab83c5 --- /dev/null +++ b/.github/workflows/agent-framework-worker.md @@ -0,0 +1,462 @@ +--- +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: + # Route Safe Outputs through the CLI proxy instead of the native HTTP MCP endpoint on the + # awmg-mcpg gateway, which the firewall denies (TCP_DENIED/403) before recommending the + # non-compilable `awmgmcpg` network.allowed entry. Workaround for github/gh-aw#45915; + # github.mode: gh-proxy (below) removes the native GitHub MCP endpoint for the same reason. + cli-proxy: true + 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 + # Validate exactly as agent-framework-worker-setup.sh reads the block next run: the begin/end + # markers must be whole trimmed lines, and the recorded values are read from inside that block. + # A looser substring match could pass here yet leave the body unparseable next run, wedging the + # state machine -- so mirror that parser (whole-line markers + block extraction) and require the + # recorded agent-framework-version and feedback-processed-through to be present within the block. + marker="agent-framework-template" + tracking_block() { + awk -v b="# ${marker}:state:begin" -v e="# ${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 }' + } + tracking_value() { + sed -n "s/^[[:space:]]*[-*+>]*[[:space:]]*$1:[[:space:]]*//p" | + head -1 | tr -d '"'\''\r' | sed 's/[[:space:]]*#.*$//; s/[[:space:]]*$//' + } + 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") + block=$(printf '%s' "$body" | tracking_block) + miss="" + if [ -z "$block" ]; then + miss="$miss state-block(whole-line begin/end markers)" + else + [ -n "$(printf '%s' "$block" | tracking_value agent-framework-version)" ] || miss="$miss agent-framework-version" + [ -n "$(printf '%s' "$block" | tracking_value feedback-processed-through)" ] || miss="$miss feedback-processed-through" + fi + if [ -n "$miss" ]; then + echo "::error::$typ item #$i is missing required tracking identity:$miss" + rc=1 + fi + done <<< "$idx" + if [ "$rc" -ne 0 ]; then + # The generated safe-output job intentionally runs after failed agents to report + # valid partial outputs. Quarantine this invalid mutation so it cannot be published. + cp "$out" /tmp/gh-aw/agent/rejected_agent_output.json + printf '%s\n' '{"items":[]}' > "$out" + echo "::error::Rejected invalid agent output; downstream safe outputs will receive no items." + exit "$rc" + fi + echo "Agent output identity validated." + +# ############################################################### +# 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. + +### Mandatory safe-output body preflight + +Before emitting **any** `create-pull-request` or `update-pull-request` safe output, first assemble +its complete replacement `body` value, including the tracking block above. Do not submit a summary, +an abbreviated body, or a reference to a local file: the exact final body string in the safe-output +item must contain all of the following: + +- one complete `# agent-framework-template:state:begin` ... + `# agent-framework-template:state:end` block; +- a non-empty `agent-framework-version` set to `target.json.release_version`; and +- a non-empty `feedback-processed-through` set to `target.json.run_started_at`. + +Validate that final string before submitting the safe output (inspect the exact value you will put +in `body`, not an earlier draft). If any invariant is missing, rebuild the complete body and validate +it again; do **not** emit the `create-pull-request` or `update-pull-request` item. This workflow never +uses append or prepend body updates: every `update-pull-request` body is a full replacement and must +pass this preflight. + +## 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/meai-otel-genai-worker.lock.yml b/.github/workflows/meai-otel-genai-worker.lock.yml index 0e0699142b2..2f6b10e56eb 100644 --- a/.github/workflows/meai-otel-genai-worker.lock.yml +++ b/.github/workflows/meai-otel-genai-worker.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"43430c60dbdc1479931eb6ae643984e37ccc85cbaeb9e0b6189467214219d34a","body_hash":"7f70a76abe9fb85ccbd499380b19356c27d046cddc5d240d32fe3cb0bbe03ece","compiler_version":"v0.82.6","strict":true,"agent_id":"copilot","detection_agent_id":"copilot","engine_versions":{"copilot":"1.0.68"},"agent_image_runner":"ubuntu-latest"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4fab0863b8e0886fe9c1dec21a1e06d1a57c0d699aca229699e86b2aa1a7eaa1","body_hash":"9ebcf1df644d419da3ff332bcf4a074f9f8b7a371d7bf376cdc8dd953df60f05","compiler_version":"v0.82.6","strict":true,"agent_id":"copilot","detection_agent_id":"copilot","engine_versions":{"copilot":"1.0.68"},"agent_image_runner":"ubuntu-latest"} # gh-aw-manifest: {"version":1,"secrets":["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":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"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":"cec6394202d7db187b02310d928812194988eb20","version":"v0.82.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.27","digest":"sha256:bb5a0150dcff1cddf9b8045bb411b7759806bace0abcb132fb22158073e155d9","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.27@sha256:bb5a0150dcff1cddf9b8045bb411b7759806bace0abcb132fb22158073e155d9"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.27","digest":"sha256:01e58c4383fa9952abe76e0a134a27c970f81f744d6b7861fc9e08b7964d94c3","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.27@sha256:01e58c4383fa9952abe76e0a134a27c970f81f744d6b7861fc9e08b7964d94c3"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.27","digest":"sha256:70df326caf73bf5911340dca4620b529a483dd8f42142b0a41d7b9761ab4ab7a","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.27@sha256:70df326caf73bf5911340dca4620b529a483dd8f42142b0a41d7b9761ab4ab7a"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.27","digest":"sha256:92d820df47b2eff75d93a5bec4dc183a3ec55ed7ddb4f25cb0fdda5c3e995409","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.27@sha256:92d820df47b2eff75d93a5bec4dc183a3ec55ed7ddb4f25cb0fdda5c3e995409"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.0","digest":"sha256:9dbdf42842c224a95016df1d2a85a2901e04204c242079343b302a307d2b8031","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.0@sha256:9dbdf42842c224a95016df1d2a85a2901e04204c242079343b302a307d2b8031"},{"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.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} # This file was automatically generated by gh-aw (v0.82.6). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -1252,7 +1252,7 @@ jobs: 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" + 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\"\nif [ \"$rc\" -ne 0 ]; then\n # The generated safe-output job intentionally runs after failed agents to report\n # valid partial outputs. Quarantine this invalid mutation so it cannot be published.\n cp \"$out\" /tmp/gh-aw/agent/rejected_agent_output.json\n printf '%s\\n' '{\"items\":[]}' > \"$out\"\n echo \"::error::Rejected invalid agent output; downstream safe outputs will receive no items.\"\n exit \"$rc\"\nfi\necho \"Agent output identity validated.\"" - name: Upload agent artifacts if: always() diff --git a/.github/workflows/meai-otel-genai-worker.md b/.github/workflows/meai-otel-genai-worker.md index 0e39c5b6dd4..31aa5c6b186 100644 --- a/.github/workflows/meai-otel-genai-worker.md +++ b/.github/workflows/meai-otel-genai-worker.md @@ -181,8 +181,15 @@ post-steps: rc=1 fi done <<< "$idx" - [ "$rc" -eq 0 ] && echo "Agent output identity validated." - exit $rc + if [ "$rc" -ne 0 ]; then + # The generated safe-output job intentionally runs after failed agents to report + # valid partial outputs. Quarantine this invalid mutation so it cannot be published. + cp "$out" /tmp/gh-aw/agent/rejected_agent_output.json + printf '%s\n' '{"items":[]}' > "$out" + echo "::error::Rejected invalid agent output; downstream safe outputs will receive no items." + exit "$rc" + fi + echo "Agent output identity validated." # ############################################################### # Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. @@ -682,6 +689,24 @@ began) whenever a maintained draft PR exists -- see Step 4's "Honoring reviewer 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. +### Mandatory safe-output body preflight + +Before emitting **any** `create-pull-request` or `update-pull-request` safe output, first +assemble its complete replacement `body` value, including the tracking block above. Do not +submit a summary, an abbreviated body, or a reference to a local file: the exact final body +string in the safe-output item must contain all of the following: + +- one complete `# meai-otel-genai-worker:state:begin` ... + `# meai-otel-genai-worker:state:end` block; +- a non-empty `upstream-scan-ref` set to `target.json.upstream_sha`; and +- a non-empty `feedback-processed-through` set to `target.json.run_started_at`. + +Validate that final string before submitting the safe output (inspect the exact value you +will put in `body`, not an earlier draft). If any invariant is missing, rebuild the complete +body and validate it again; do **not** emit the `create-pull-request` or +`update-pull-request` item. This workflow never uses append or prepend body updates: +every `update-pull-request` body is a full replacement and must pass this preflight. + ## Step 6 -- When the upstream release is published If `target.json.release_ready` is `true` and a matching open PR has a pending release