From 77a04e084e4732cbca86622059c80dd73930f3a5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 8 Feb 2026 03:55:41 +0000 Subject: [PATCH 001/147] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 1710a7107..c8d3ee672 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ io.github.copilot-community-sdk copilot-sdk - 1.0.8 + 1.0.9-SNAPSHOT jar GitHub Copilot Community SDK :: Java @@ -33,7 +33,7 @@ scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git https://github.com/copilot-community-sdk/copilot-sdk-java - v1.0.8 + HEAD From 392c6ef4f636407a9bbc949931efe77b99fb1ee4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 04:11:37 +0000 Subject: [PATCH 002/147] Update CHANGELOG.md with 1.0.8 release information Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b195a7484..79ffe4ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.8] - 2026-02-08 + ### Added #### ResumeSessionConfig Parity with SessionConfig @@ -302,6 +304,7 @@ New types: `GetForegroundSessionResponse`, `SetForegroundSessionResponse` - Pre-commit hook for Spotless code formatting - Comprehensive API documentation +[1.0.8]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.7...v1.0.8 [1.0.7]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.6...v1.0.7 [1.0.6]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.5...v1.0.6 [1.0.5]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v1.0.4...v1.0.5 From 5e58e9d87ed26cdf835bc971554b86a00097aa26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 04:14:11 +0000 Subject: [PATCH 003/147] feat: add CHANGELOG update script and integrate into release workflow Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .github/scripts/update-changelog.sh | 92 +++++++++++++++++++++++++++++ .github/workflows/publish-maven.yml | 5 +- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100755 .github/scripts/update-changelog.sh diff --git a/.github/scripts/update-changelog.sh b/.github/scripts/update-changelog.sh new file mode 100755 index 000000000..a6c2ae982 --- /dev/null +++ b/.github/scripts/update-changelog.sh @@ -0,0 +1,92 @@ +#!/bin/bash +set -e + +# Script to update CHANGELOG.md during release process +# Usage: ./update-changelog.sh +# Example: ./update-changelog.sh 1.0.8 + +if [ -z "$1" ]; then + echo "Error: Version argument required" + echo "Usage: $0 " + exit 1 +fi + +VERSION="$1" +CHANGELOG_FILE="CHANGELOG.md" +RELEASE_DATE=$(date +%Y-%m-%d) + +echo "Updating CHANGELOG.md for version ${VERSION} (${RELEASE_DATE})" + +# Check if CHANGELOG.md exists +if [ ! -f "$CHANGELOG_FILE" ]; then + echo "Error: CHANGELOG.md not found" + exit 1 +fi + +# Check if there's an [Unreleased] section +if ! grep -q "## \[Unreleased\]" "$CHANGELOG_FILE"; then + echo "Error: No [Unreleased] section found in CHANGELOG.md" + exit 1 +fi + +# Create a temporary file +TEMP_FILE=$(mktemp) + +# Process the CHANGELOG +awk -v version="$VERSION" -v date="$RELEASE_DATE" ' +BEGIN { + unreleased_found = 0 + content_found = 0 + links_section = 0 + first_version_link = "" +} + +# Track if we are in the links section at the bottom +/^\[/ { + links_section = 1 +} + +# Replace [Unreleased] with the version and date +/^## \[Unreleased\]/ { + if (!unreleased_found) { + print "## [Unreleased]" + print "" + print "## [" version "] - " date + unreleased_found = 1 + next + } +} + +# Capture the first version link to get the previous version +links_section && first_version_link == "" && /^\[[0-9]+\.[0-9]+\.[0-9]+\]:/ { + match($0, /\[([0-9]+\.[0-9]+\.[0-9]+)\]:/, arr) + if (arr[1] != "") { + first_version_link = arr[1] + # Insert Unreleased and new version links before first version link + print "[Unreleased]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v" version "...HEAD" + print "[" version "]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v" arr[1] "...v" version + } +} + +# Update existing [Unreleased] link if present +links_section && /^\[Unreleased\]:/ { + # Get the previous version from the existing link + match($0, /v([0-9]+\.[0-9]+\.[0-9]+)\.\.\.HEAD/, arr) + if (arr[1] != "") { + print "[Unreleased]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v" version "...HEAD" + print "[" version "]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v" arr[1] "...v" version + next + } +} + +# Print all other lines unchanged +{ print } +' "$CHANGELOG_FILE" > "$TEMP_FILE" + +# Replace the original file +mv "$TEMP_FILE" "$CHANGELOG_FILE" + +echo "✓ CHANGELOG.md updated successfully" +echo " - Added version ${VERSION} with date ${RELEASE_DATE}" +echo " - Created new [Unreleased] section" +echo " - Updated version comparison links" diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml index bc31ecedc..e86f0ddb3 100644 --- a/.github/workflows/publish-maven.yml +++ b/.github/workflows/publish-maven.yml @@ -97,6 +97,9 @@ jobs: run: | VERSION="${{ steps.versions.outputs.release_version }}" + # Update CHANGELOG.md with release version + ./.github/scripts/update-changelog.sh "${VERSION}" + # Update version in README.md sed -i "s|[0-9]*\.[0-9]*\.[0-9]*|${VERSION}|g" README.md sed -i "s|copilot-sdk:[0-9]*\.[0-9]*\.[0-9]*|copilot-sdk:${VERSION}|g" README.md @@ -105,7 +108,7 @@ jobs: sed -i "s|copilot-sdk:[0-9]*\.[0-9]*\.[0-9]*|copilot-sdk:${VERSION}|g" jbang-example.java # Commit the documentation changes before release:prepare (requires clean working directory) - git add README.md jbang-example.java + git add CHANGELOG.md README.md jbang-example.java git commit -m "docs: update version references to ${VERSION}" # Save the commit SHA for potential rollback From 41a65f82904a19b94f8a8088caa4a1186dc87b7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 04:16:42 +0000 Subject: [PATCH 004/147] fix: extract repository URL from existing links in CHANGELOG Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .github/scripts/test-update-changelog.sh | 203 +++++++++++++++++++++++ .github/scripts/update-changelog.sh | 27 ++- 2 files changed, 221 insertions(+), 9 deletions(-) create mode 100755 .github/scripts/test-update-changelog.sh diff --git a/.github/scripts/test-update-changelog.sh b/.github/scripts/test-update-changelog.sh new file mode 100755 index 000000000..ade87d6a9 --- /dev/null +++ b/.github/scripts/test-update-changelog.sh @@ -0,0 +1,203 @@ +#!/bin/bash +# Test script for update-changelog.sh + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +UPDATE_SCRIPT="${SCRIPT_DIR}/update-changelog.sh" +TEST_DIR="/tmp/changelog-test-$$" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +passed=0 +failed=0 + +# Setup test directory +mkdir -p "$TEST_DIR" + +# Cleanup on exit +cleanup() { + rm -rf "$TEST_DIR" +} +trap cleanup EXIT + +# Helper function to run a test +run_test() { + local test_name="$1" + local test_func="$2" + + echo -n "Testing: $test_name ... " + + if $test_func; then + echo -e "${GREEN}PASSED${NC}" + ((passed++)) + else + echo -e "${RED}FAILED${NC}" + ((failed++)) + fi +} + +# Test 1: Basic functionality - Replace Unreleased with version +test_basic_replace() { + local test_file="${TEST_DIR}/test1.md" + cat > "$test_file" << 'EOF' +# Changelog + +## [Unreleased] + +### Added +- New feature + +## [1.0.0] - 2026-01-01 + +### Added +- Initial release + +[1.0.0]: https://github.com/test/repo/releases/tag/1.0.0 +EOF + + # Run the script + CHANGELOG_FILE="$test_file" bash "$UPDATE_SCRIPT" 1.0.1 > /dev/null 2>&1 + + # Verify the changes + if grep -q "## \[Unreleased\]" "$test_file" && \ + grep -q "## \[1.0.1\] - $(date +%Y-%m-%d)" "$test_file" && \ + grep -q "\[Unreleased\]: https://github.com/test/repo/compare/v1.0.1...HEAD" "$test_file" && \ + grep -q "\[1.0.1\]: https://github.com/test/repo/compare/v1.0.0...v1.0.1" "$test_file"; then + return 0 + else + return 1 + fi +} + +# Test 2: Handle CHANGELOG without Unreleased link +test_no_unreleased_link() { + local test_file="${TEST_DIR}/test2.md" + cat > "$test_file" << 'EOF' +# Changelog + +## [Unreleased] + +### Added +- New feature + +## [1.0.0] - 2026-01-01 + +[1.0.0]: https://github.com/test/repo/releases/tag/1.0.0 +EOF + + CHANGELOG_FILE="$test_file" bash "$UPDATE_SCRIPT" 1.0.1 > /dev/null 2>&1 + + # Should add both Unreleased and version links + if grep -q "\[Unreleased\]: https://github.com/test/repo/compare/v1.0.1...HEAD" "$test_file" && \ + grep -q "\[1.0.1\]: https://github.com/test/repo/compare/v1.0.0...v1.0.1" "$test_file"; then + return 0 + else + return 1 + fi +} + +# Test 3: Preserve content structure +test_preserve_content() { + local test_file="${TEST_DIR}/test3.md" + cat > "$test_file" << 'EOF' +# Changelog + +## [Unreleased] + +### Added +- Feature A +- Feature B + +### Fixed +- Bug fix + +## [1.0.0] - 2026-01-01 + +[1.0.0]: https://github.com/test/repo/releases/tag/1.0.0 +EOF + + CHANGELOG_FILE="$test_file" bash "$UPDATE_SCRIPT" 1.0.1 > /dev/null 2>&1 + + # Verify content is preserved under the new version + if grep -A 6 "## \[1.0.1\]" "$test_file" | grep -q "Feature A" && \ + grep -A 6 "## \[1.0.1\]" "$test_file" | grep -q "Bug fix"; then + return 0 + else + return 1 + fi +} + +# Test 4: Error handling - no Unreleased section +test_no_unreleased_section() { + local test_file="${TEST_DIR}/test4.md" + cat > "$test_file" << 'EOF' +# Changelog + +## [1.0.0] - 2026-01-01 + +[1.0.0]: https://github.com/test/repo/releases/tag/1.0.0 +EOF + + # Should fail because there's no Unreleased section + if ! CHANGELOG_FILE="$test_file" bash "$UPDATE_SCRIPT" 1.0.1 > /dev/null 2>&1; then + return 0 + else + return 1 + fi +} + +# Test 5: Multiple version handling +test_multiple_versions() { + local test_file="${TEST_DIR}/test5.md" + cat > "$test_file" << 'EOF' +# Changelog + +## [Unreleased] + +### Added +- New feature + +## [1.0.1] - 2026-02-01 + +## [1.0.0] - 2026-01-01 + +[1.0.1]: https://github.com/test/repo/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.com/test/repo/releases/tag/1.0.0 +EOF + + CHANGELOG_FILE="$test_file" bash "$UPDATE_SCRIPT" 1.0.2 > /dev/null 2>&1 + + # Verify the new version is added and links are updated + if grep -q "## \[1.0.2\] - $(date +%Y-%m-%d)" "$test_file" && \ + grep -q "\[1.0.2\]: https://github.com/test/repo/compare/v1.0.1...v1.0.2" "$test_file"; then + return 0 + else + return 1 + fi +} + +# Run all tests +echo "Running CHANGELOG update script tests..." +echo "" + +run_test "Basic functionality - Replace Unreleased with version" test_basic_replace +run_test "Handle CHANGELOG without Unreleased link" test_no_unreleased_link +run_test "Preserve content structure" test_preserve_content +run_test "Error handling - no Unreleased section" test_no_unreleased_section +run_test "Multiple version handling" test_multiple_versions + +echo "" +echo "==========================================" +echo -e "Tests passed: ${GREEN}${passed}${NC}" +echo -e "Tests failed: ${RED}${failed}${NC}" +echo "==========================================" + +if [ $failed -eq 0 ]; then + exit 0 +else + exit 1 +fi diff --git a/.github/scripts/update-changelog.sh b/.github/scripts/update-changelog.sh index a6c2ae982..beeafb54b 100755 --- a/.github/scripts/update-changelog.sh +++ b/.github/scripts/update-changelog.sh @@ -12,7 +12,7 @@ if [ -z "$1" ]; then fi VERSION="$1" -CHANGELOG_FILE="CHANGELOG.md" +CHANGELOG_FILE="${CHANGELOG_FILE:-CHANGELOG.md}" RELEASE_DATE=$(date +%Y-%m-%d) echo "Updating CHANGELOG.md for version ${VERSION} (${RELEASE_DATE})" @@ -39,6 +39,7 @@ BEGIN { content_found = 0 links_section = 0 first_version_link = "" + repo_url = "" } # Track if we are in the links section at the bottom @@ -46,6 +47,14 @@ BEGIN { links_section = 1 } +# Capture the repository URL from the first version link +links_section && repo_url == "" && /^\[[0-9]+\.[0-9]+\.[0-9]+\]:/ { + match($0, /(https:\/\/github\.com\/[^\/]+\/[^\/]+)\//, arr) + if (arr[1] != "") { + repo_url = arr[1] + } +} + # Replace [Unreleased] with the version and date /^## \[Unreleased\]/ { if (!unreleased_found) { @@ -60,21 +69,21 @@ BEGIN { # Capture the first version link to get the previous version links_section && first_version_link == "" && /^\[[0-9]+\.[0-9]+\.[0-9]+\]:/ { match($0, /\[([0-9]+\.[0-9]+\.[0-9]+)\]:/, arr) - if (arr[1] != "") { + if (arr[1] != "" && repo_url != "") { first_version_link = arr[1] # Insert Unreleased and new version links before first version link - print "[Unreleased]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v" version "...HEAD" - print "[" version "]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v" arr[1] "...v" version + print "[Unreleased]: " repo_url "/compare/v" version "...HEAD" + print "[" version "]: " repo_url "/compare/v" arr[1] "...v" version } } # Update existing [Unreleased] link if present links_section && /^\[Unreleased\]:/ { - # Get the previous version from the existing link - match($0, /v([0-9]+\.[0-9]+\.[0-9]+)\.\.\.HEAD/, arr) - if (arr[1] != "") { - print "[Unreleased]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v" version "...HEAD" - print "[" version "]: https://github.com/copilot-community-sdk/copilot-sdk-java/compare/v" arr[1] "...v" version + # Get the previous version and repo URL from the existing link + match($0, /(https:\/\/github\.com\/[^\/]+\/[^\/]+)\/compare\/v([0-9]+\.[0-9]+\.[0-9]+)\.\.\.HEAD/, arr) + if (arr[1] != "" && arr[2] != "") { + print "[Unreleased]: " arr[1] "/compare/v" version "...HEAD" + print "[" version "]: " arr[1] "/compare/v" arr[2] "...v" version next } } From 12812c679c4d2e6350d902a1b786929a7982c937 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 04:18:01 +0000 Subject: [PATCH 005/147] docs: document release process in copilot instructions Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .github/copilot-instructions.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e3530baa8..4ec355c84 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -249,3 +249,23 @@ This SDK is designed to be **lightweight with minimal dependencies**: 6. **Commit**: Make focused commits with clear messages 7. **Push**: Push your branch and create a PR 8. **Review**: Address review feedback and iterate + +## Release Process + +The release process is automated via the `publish-maven.yml` GitHub Actions workflow. Key steps: + +1. **CHANGELOG Update**: The script `.github/scripts/update-changelog.sh` automatically: + - Converts the `## [Unreleased]` section to `## [version] - date` + - Creates a new empty `## [Unreleased]` section at the top + - Updates version comparison links at the bottom of CHANGELOG.md + +2. **Documentation Updates**: README.md and jbang-example.java are updated with the new version + +3. **Maven Release**: Uses `maven-release-plugin` to: + - Update pom.xml version + - Create a git tag + - Deploy to Maven Central + +4. **Rollback**: If the release fails, the documentation commit is automatically reverted + +The workflow is triggered manually via workflow_dispatch with optional version parameters. From 09a1e5a444c47d0c65c0dfec7823fbbaaff7afba Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 09:07:05 -0500 Subject: [PATCH 006/147] vscode settings --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 061d444e0..85dfb5748 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "-Dcopilot.sdk.dir=${workspaceFolder}/target/copilot-sdk", "-Dcopilot.tests.dir=${workspaceFolder}/target/copilot-sdk/test" ] - } + }, + "java.compile.nullAnalysis.mode": "automatic" } From c9a1eea46b9e6b7521b0d46e32b9ed02761490c1 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 09:43:02 -0500 Subject: [PATCH 007/147] fix: disable Husky Git hooks in certain CI workflows to prevent local development hooks from running --- .github/workflows/deploy-site.yml | 6 ++++++ .github/workflows/publish-maven.yml | 3 +++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 3bf29870b..a4eb544f2 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -1,6 +1,12 @@ # Workflow for deploying versioned documentation to GitHub Pages name: Deploy Documentation +env: + # Disable Husky Git hooks in CI to prevent local development hooks + # (e.g., pre-commit formatting checks) from running during automated + # workflows that perform git commits and pushes. + HUSKY: 0 + on: # Runs after Build & Test succeeds on main (publishes to /snapshot/) workflow_run: diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml index e86f0ddb3..7170f1e66 100644 --- a/.github/workflows/publish-maven.yml +++ b/.github/workflows/publish-maven.yml @@ -1,6 +1,9 @@ name: Publish to Maven Central env: + # Disable Husky Git hooks in CI to prevent local development hooks + # (e.g., pre-commit formatting checks) from running during automated + # workflows that perform git commits and pushes. HUSKY: 0 on: From 91bee47e1cbaf4595826b71b24ceafe7578e8e71 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 10:23:46 -0500 Subject: [PATCH 008/147] feat: enhance release process to track upstream SDK sync in CHANGELOG and update scripts --- .github/copilot-instructions.md | 12 +++++++++--- .github/scripts/update-changelog.sh | 26 +++++++++++++++++++++++--- .github/workflows/publish-maven.yml | 10 ++++++++-- CHANGELOG.md | 9 +++++++-- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4ec355c84..375278cff 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -258,14 +258,20 @@ The release process is automated via the `publish-maven.yml` GitHub Actions work - Converts the `## [Unreleased]` section to `## [version] - date` - Creates a new empty `## [Unreleased]` section at the top - Updates version comparison links at the bottom of CHANGELOG.md + - Injects the upstream SDK commit hash (from `.lastmerge`) as a `> **Upstream sync:**` blockquote in both the new `[Unreleased]` section and the released version section -2. **Documentation Updates**: README.md and jbang-example.java are updated with the new version +2. **Upstream Sync Tracking**: Each release records which commit from the official `github/copilot-sdk` it is synced to: + - The `.lastmerge` file is read during the release workflow + - The commit hash is injected into `CHANGELOG.md` under the release heading + - Format: `> **Upstream sync:** [\`github/copilot-sdk@SHORT_HASH\`](link-to-commit)` -3. **Maven Release**: Uses `maven-release-plugin` to: +3. **Documentation Updates**: README.md and jbang-example.java are updated with the new version. + +4. **Maven Release**: Uses `maven-release-plugin` to: - Update pom.xml version - Create a git tag - Deploy to Maven Central -4. **Rollback**: If the release fails, the documentation commit is automatically reverted +5. **Rollback**: If the release fails, the documentation commit is automatically reverted The workflow is triggered manually via workflow_dispatch with optional version parameters. diff --git a/.github/scripts/update-changelog.sh b/.github/scripts/update-changelog.sh index beeafb54b..6dfedbf1c 100755 --- a/.github/scripts/update-changelog.sh +++ b/.github/scripts/update-changelog.sh @@ -2,20 +2,25 @@ set -e # Script to update CHANGELOG.md during release process -# Usage: ./update-changelog.sh +# Usage: ./update-changelog.sh [upstream-hash] # Example: ./update-changelog.sh 1.0.8 +# Example: ./update-changelog.sh 1.0.8 05e3c46c8c23130c9c064dc43d00ec78f7a75eab if [ -z "$1" ]; then echo "Error: Version argument required" - echo "Usage: $0 " + echo "Usage: $0 [upstream-hash]" exit 1 fi VERSION="$1" +UPSTREAM_HASH="${2:-}" CHANGELOG_FILE="${CHANGELOG_FILE:-CHANGELOG.md}" RELEASE_DATE=$(date +%Y-%m-%d) echo "Updating CHANGELOG.md for version ${VERSION} (${RELEASE_DATE})" +if [ -n "$UPSTREAM_HASH" ]; then + echo " Upstream SDK sync: ${UPSTREAM_HASH:0:7}" +fi # Check if CHANGELOG.md exists if [ ! -f "$CHANGELOG_FILE" ]; then @@ -33,7 +38,7 @@ fi TEMP_FILE=$(mktemp) # Process the CHANGELOG -awk -v version="$VERSION" -v date="$RELEASE_DATE" ' +awk -v version="$VERSION" -v date="$RELEASE_DATE" -v upstream_hash="$UPSTREAM_HASH" ' BEGIN { unreleased_found = 0 content_found = 0 @@ -60,12 +65,27 @@ links_section && repo_url == "" && /^\[[0-9]+\.[0-9]+\.[0-9]+\]:/ { if (!unreleased_found) { print "## [Unreleased]" print "" + if (upstream_hash != "") { + short_hash = substr(upstream_hash, 1, 7) + print "> **Upstream sync:** [`github/copilot-sdk@" short_hash "`](https://github.com/github/copilot-sdk/commit/" upstream_hash ")" + print "" + } print "## [" version "] - " date + if (upstream_hash != "") { + print "" + print "> **Upstream sync:** [`github/copilot-sdk@" short_hash "`](https://github.com/github/copilot-sdk/commit/" upstream_hash ")" + } unreleased_found = 1 + skip_old_upstream = 1 next } } +# Skip the old upstream sync line and surrounding blank lines from the previous [Unreleased] section +skip_old_upstream && /^[[:space:]]*$/ { next } +skip_old_upstream && /^> \*\*Upstream sync:\*\*/ { next } +skip_old_upstream && !/^[[:space:]]*$/ && !/^> \*\*Upstream sync:\*\*/ { skip_old_upstream = 0 } + # Capture the first version link to get the previous version links_section && first_version_link == "" && /^\[[0-9]+\.[0-9]+\.[0-9]+\]:/ { match($0, /\[([0-9]+\.[0-9]+\.[0-9]+)\]:/, arr) diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml index 7170f1e66..5af997965 100644 --- a/.github/workflows/publish-maven.yml +++ b/.github/workflows/publish-maven.yml @@ -100,8 +100,14 @@ jobs: run: | VERSION="${{ steps.versions.outputs.release_version }}" - # Update CHANGELOG.md with release version - ./.github/scripts/update-changelog.sh "${VERSION}" + # Read the upstream SDK commit hash that this release is synced to + UPSTREAM_HASH=$(cat .lastmerge) + UPSTREAM_SHORT="${UPSTREAM_HASH:0:7}" + UPSTREAM_URL="https://github.com/github/copilot-sdk/commit/${UPSTREAM_HASH}" + echo "Upstream SDK sync: ${UPSTREAM_SHORT} (${UPSTREAM_URL})" + + # Update CHANGELOG.md with release version and upstream sync hash + ./.github/scripts/update-changelog.sh "${VERSION}" "${UPSTREAM_HASH}" # Update version in README.md sed -i "s|[0-9]*\.[0-9]*\.[0-9]*|${VERSION}|g" README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 79ffe4ddd..5992e0d97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,18 @@ All notable changes to the Copilot SDK for Java will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +> Note: This file is automatically modified by scripts and coding agents. Do not change it manually. ## [Unreleased] +> **Upstream sync:** [`github/copilot-sdk@05e3c46`](https://github.com/github/copilot-sdk/commit/05e3c46c8c23130c9c064dc43d00ec78f7a75eab) + ## [1.0.8] - 2026-02-08 +> **Upstream sync:** [`github/copilot-sdk@05e3c46`](https://github.com/github/copilot-sdk/commit/05e3c46c8c23130c9c064dc43d00ec78f7a75eab) + ### Added #### ResumeSessionConfig Parity with SessionConfig From 5d7265285bf5d169d188676ac54de656d86726fb Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 10:25:10 -0500 Subject: [PATCH 009/147] Update documentation badge to version 1.0.8 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b79b39c3e..c5c3afc6f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ #### Latest release [![GitHub Release](https://img.shields.io/github/v/release/copilot-community-sdk/copilot-sdk-java)](https://github.com/copilot-community-sdk/copilot-sdk-java/releases) [![Maven Central](https://img.shields.io/maven-central/v/io.github.copilot-community-sdk/copilot-sdk)](https://central.sonatype.com/artifact/io.github.copilot-community-sdk/copilot-sdk) -[![Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://copilot-community-sdk.github.io/copilot-sdk-java/1.0.7/) +[![Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://copilot-community-sdk.github.io/copilot-sdk-java/1.0.8/) [![Javadoc](https://javadoc.io/badge2/io.github.copilot-community-sdk/copilot-sdk/javadoc.svg)](https://javadoc.io/doc/io.github.copilot-community-sdk/copilot-sdk/latest/index.html) ## Overview @@ -138,4 +138,4 @@ MIT — see [LICENSE](LICENSE) for details. [![Star History Chart](https://api.star-history.com/svg?repos=copilot-community-sdk/copilot-sdk-java&type=Date)](https://www.star-history.com/#copilot-community-sdk/copilot-sdk-java&Date) -⭐ Drop a star if you find this useful! \ No newline at end of file +⭐ Drop a star if you find this useful! From fd038dec5fc28acf1110e110f66b0ebf644c3662 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 10:30:00 -0500 Subject: [PATCH 010/147] docs: update documentation links to point to the latest version --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c5c3afc6f..5e6223b2c 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ #### Latest release [![GitHub Release](https://img.shields.io/github/v/release/copilot-community-sdk/copilot-sdk-java)](https://github.com/copilot-community-sdk/copilot-sdk-java/releases) [![Maven Central](https://img.shields.io/maven-central/v/io.github.copilot-community-sdk/copilot-sdk)](https://central.sonatype.com/artifact/io.github.copilot-community-sdk/copilot-sdk) -[![Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://copilot-community-sdk.github.io/copilot-sdk-java/1.0.8/) -[![Javadoc](https://javadoc.io/badge2/io.github.copilot-community-sdk/copilot-sdk/javadoc.svg)](https://javadoc.io/doc/io.github.copilot-community-sdk/copilot-sdk/latest/index.html) +[![Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://copilot-community-sdk.github.io/copilot-sdk-java/latest/) +[![Javadoc](https://javadoc.io/badge2/io.github.copilot-community-sdk/copilot-sdk/javadoc.svg?q=1)](https://javadoc.io/doc/io.github.copilot-community-sdk/copilot-sdk/latest/index.html) ## Overview From ef1a3f8974a52dcd71788581c61e88d6f482a47e Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 10:31:31 -0500 Subject: [PATCH 011/147] docs: add GitHub release date badge to README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e6223b2c..230b1989f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) #### Latest release +[![GitHub Release Date](https://img.shields.io/github/release-date/copilot-community-sdk/copilot-sdk-java)](https://github.com/copilot-community-sdk/copilot-sdk-java/releases) [![GitHub Release](https://img.shields.io/github/v/release/copilot-community-sdk/copilot-sdk-java)](https://github.com/copilot-community-sdk/copilot-sdk-java/releases) [![Maven Central](https://img.shields.io/maven-central/v/io.github.copilot-community-sdk/copilot-sdk)](https://central.sonatype.com/artifact/io.github.copilot-community-sdk/copilot-sdk) [![Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://copilot-community-sdk.github.io/copilot-sdk-java/latest/) @@ -138,4 +139,4 @@ MIT — see [LICENSE](LICENSE) for details. [![Star History Chart](https://api.star-history.com/svg?repos=copilot-community-sdk/copilot-sdk-java&type=Date)](https://www.star-history.com/#copilot-community-sdk/copilot-sdk-java&Date) -⭐ Drop a star if you find this useful! +⭐ Drop a star if you find this useful! \ No newline at end of file From bfebbd63871fc7ac39cced67b721ffbc28086503 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 10:33:13 -0500 Subject: [PATCH 012/147] refactor: remove pre-site image copy execution and update resources --- pom.xml | 12 ------------ src/site/resources/.gitignore | 2 -- image.png => src/site/resources/image.png | Bin 3 files changed, 14 deletions(-) delete mode 100644 src/site/resources/.gitignore rename image.png => src/site/resources/image.png (100%) diff --git a/pom.xml b/pom.xml index c8d3ee672..5fb5da257 100644 --- a/pom.xml +++ b/pom.xml @@ -179,18 +179,6 @@ - - copy-image-to-site - pre-site - - run - - - - - - - diff --git a/src/site/resources/.gitignore b/src/site/resources/.gitignore deleted file mode 100644 index 4ccc5817f..000000000 --- a/src/site/resources/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Copied from root during pre-site phase - do not edit here -image.png diff --git a/image.png b/src/site/resources/image.png similarity index 100% rename from image.png rename to src/site/resources/image.png From 15ad10156b5fb4b90c2fafb53a8f91d6b2a21c4d Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 10:36:15 -0500 Subject: [PATCH 013/147] feat: move Checkstyle and SpotBugs configuration files to the config folder --- checkstyle.xml => config/checkstyle/checkstyle.xml | 0 .../spotbugs/spotbugs-exclude.xml | 0 pom.xml | 6 +++--- 3 files changed, 3 insertions(+), 3 deletions(-) rename checkstyle.xml => config/checkstyle/checkstyle.xml (100%) rename spotbugs-exclude.xml => config/spotbugs/spotbugs-exclude.xml (100%) diff --git a/checkstyle.xml b/config/checkstyle/checkstyle.xml similarity index 100% rename from checkstyle.xml rename to config/checkstyle/checkstyle.xml diff --git a/spotbugs-exclude.xml b/config/spotbugs/spotbugs-exclude.xml similarity index 100% rename from spotbugs-exclude.xml rename to config/spotbugs/spotbugs-exclude.xml diff --git a/pom.xml b/pom.xml index 5fb5da257..38d337ab3 100644 --- a/pom.xml +++ b/pom.xml @@ -97,7 +97,7 @@ spotbugs-maven-plugin 4.9.8.2 - spotbugs-exclude.xml + config/spotbugs/spotbugs-exclude.xml @@ -289,7 +289,7 @@ maven-checkstyle-plugin 3.6.0 - checkstyle.xml + config/checkstyle/checkstyle.xml true true false @@ -442,7 +442,7 @@ spotbugs-maven-plugin 4.9.8.2 - spotbugs-exclude.xml + config/spotbugs/spotbugs-exclude.xml From 099155b5f93fc807d5f9f8a07d23cf1c20a1fcc2 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 11:09:14 -0500 Subject: [PATCH 014/147] feat: add 'latest' tag update step to GitHub Actions and document JBang usage in README --- .github/workflows/publish-maven.yml | 8 ++++++++ README.md | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml index 5af997965..a73e523bf 100644 --- a/.github/workflows/publish-maven.yml +++ b/.github/workflows/publish-maven.yml @@ -196,6 +196,14 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Move 'latest' tag to new release + run: | + VERSION="${{ needs.publish-maven.outputs.version }}" + git tag -f latest "v${VERSION}" + git push origin latest --force + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + deploy-site: name: Deploy Documentation needs: [publish-maven, github-release] diff --git a/README.md b/README.md index 230b1989f..8c96da2ef 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,18 @@ public class CopilotSDK { } ``` +## Try it with JBang + +You can run the SDK without setting up a full Java project, by using [JBang](https://www.jbang.dev/). + +See the full source of [`jbang-example.java`](jbang-example.java) for a complete example with more features like session idle handling and usage info events. + +Or run it directly from the repository: + +```bash +jbang https://github.com/copilot-community-sdk/copilot-sdk-java/blob/latest/jbang-example.java +``` + ## Documentation 📚 **[Full Documentation](https://copilot-community-sdk.github.io/copilot-sdk-java/)** — Complete API reference, advanced usage examples, and guides. From 892071515754b8301d2cc4c02afe8d7aacc85c16 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 11:13:15 -0500 Subject: [PATCH 015/147] fix: move SuppressFBWarnings annotation to the package declaration for event and json packages --- src/main/java/com/github/copilot/sdk/events/package-info.java | 2 +- src/main/java/com/github/copilot/sdk/json/package-info.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/events/package-info.java b/src/main/java/com/github/copilot/sdk/events/package-info.java index 1bcc1ddd0..3bbf130f5 100644 --- a/src/main/java/com/github/copilot/sdk/events/package-info.java +++ b/src/main/java/com/github/copilot/sdk/events/package-info.java @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "DTOs for JSON deserialization - low risk") /** * Event types emitted during Copilot session processing. * @@ -88,4 +87,5 @@ * @see com.github.copilot.sdk.CopilotSession#on(java.util.function.Consumer) * @see com.github.copilot.sdk.events.AbstractSessionEvent */ +@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "DTOs for JSON deserialization - low risk") package com.github.copilot.sdk.events; diff --git a/src/main/java/com/github/copilot/sdk/json/package-info.java b/src/main/java/com/github/copilot/sdk/json/package-info.java index c0fbe53a9..aabf62069 100644 --- a/src/main/java/com/github/copilot/sdk/json/package-info.java +++ b/src/main/java/com/github/copilot/sdk/json/package-info.java @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "DTOs for JSON deserialization - low risk") /** * Configuration classes and data transfer objects for the Copilot SDK. * @@ -92,4 +91,5 @@ * @see com.github.copilot.sdk.CopilotClient * @see com.github.copilot.sdk.CopilotSession */ +@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "DTOs for JSON deserialization - low risk") package com.github.copilot.sdk.json; From 87cfd3baed726d1e9982e6270efd8852f72a3a90 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 13:39:01 -0500 Subject: [PATCH 016/147] fix: update GH_TOKEN to use COPILOT_ISSUE_PR_AGENTIC_WORKFLOW and add step to close stale upstream-sync issues --- .github/workflows/weekly-upstream-sync.yml | 26 ++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/weekly-upstream-sync.yml b/.github/workflows/weekly-upstream-sync.yml index 37af04c23..a358cd6f4 100644 --- a/.github/workflows/weekly-upstream-sync.yml +++ b/.github/workflows/weekly-upstream-sync.yml @@ -56,7 +56,7 @@ jobs: - name: Close previous upstream-sync issues if: steps.check.outputs.has_changes == 'true' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} run: | # Find all open issues with the upstream-sync label OPEN_ISSUES=$(gh issue list \ @@ -76,10 +76,32 @@ jobs: --reason "not planned" done + - name: Close stale upstream-sync issues (no changes) + if: steps.check.outputs.has_changes == 'false' + env: + GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} + run: | + OPEN_ISSUES=$(gh issue list \ + --repo "${{ github.repository }}" \ + --label "upstream-sync" \ + --state open \ + --json number \ + --jq '.[].number') + + for ISSUE_NUM in $OPEN_ISSUES; do + echo "Closing stale issue #${ISSUE_NUM} — upstream is up to date" + gh issue comment "$ISSUE_NUM" \ + --repo "${{ github.repository }}" \ + --body "No new upstream changes detected. The Java SDK is up to date. Closing." + gh issue close "$ISSUE_NUM" \ + --repo "${{ github.repository }}" \ + --reason "completed" + done + - name: Create issue and assign to Copilot if: steps.check.outputs.has_changes == 'true' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} run: | COMMIT_COUNT="${{ steps.check.outputs.commit_count }}" LAST_MERGE="${{ steps.check.outputs.last_merge }}" From af8ca2b90b910a604421498f7190924b3e070e64 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 13:40:16 -0500 Subject: [PATCH 017/147] feat: assign 'copilot-swe-agent' to upstream-sync issues upon creation --- .github/workflows/weekly-upstream-sync.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/weekly-upstream-sync.yml b/.github/workflows/weekly-upstream-sync.yml index a358cd6f4..95db10fa6 100644 --- a/.github/workflows/weekly-upstream-sync.yml +++ b/.github/workflows/weekly-upstream-sync.yml @@ -134,7 +134,8 @@ jobs: '{ title: $title, body: $body, - labels: ["upstream-sync"] + labels: ["upstream-sync"], + assignees: ["copilot-swe-agent"] }') # Create the issue From d8e3757144c4f328fa8538318a50c1fac0d3fc99 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 13:45:07 -0500 Subject: [PATCH 018/147] refactor: streamline issue creation for upstream sync by using gh issue create command --- .github/workflows/weekly-upstream-sync.yml | 32 ++++++---------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/.github/workflows/weekly-upstream-sync.yml b/.github/workflows/weekly-upstream-sync.yml index 95db10fa6..eb220e003 100644 --- a/.github/workflows/weekly-upstream-sync.yml +++ b/.github/workflows/weekly-upstream-sync.yml @@ -127,29 +127,15 @@ jobs: Follow the [agentic-merge-upstream](.github/prompts/agentic-merge-upstream.prompt.md) prompt to port these changes to the Java SDK." - # Build the issue JSON payload - ISSUE_PAYLOAD=$(jq -n \ - --arg title "Upstream sync: ${COMMIT_COUNT} new commits (${DATE})" \ - --arg body "$BODY" \ - '{ - title: $title, - body: $body, - labels: ["upstream-sync"], - assignees: ["copilot-swe-agent"] - }') - - # Create the issue - ISSUE_NUMBER=$(echo "$ISSUE_PAYLOAD" | \ - gh api \ - --method POST \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${REPO}/issues" \ - --input - \ - --jq '.number') - - echo "Created issue #${ISSUE_NUMBER}" - echo "✅ Issue #${ISSUE_NUMBER} created and assigned to Copilot coding agent." + # Create the issue and assign to Copilot coding agent + gh issue create \ + --repo "$REPO" \ + --title "Upstream sync: ${COMMIT_COUNT} new commits (${DATE})" \ + --body "$BODY" \ + --label "upstream-sync" \ + --assignee "copilot-swe-agent" + + echo "✅ Issue created and assigned to Copilot coding agent." - name: Summary if: always() From e975312e89c3a0038d7649466f8dd4e07838c372 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 14:29:05 -0500 Subject: [PATCH 019/147] feat: enhance issue creation step to output issue URL for upstream sync --- .github/workflows/weekly-upstream-sync.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/weekly-upstream-sync.yml b/.github/workflows/weekly-upstream-sync.yml index eb220e003..84ef95fc5 100644 --- a/.github/workflows/weekly-upstream-sync.yml +++ b/.github/workflows/weekly-upstream-sync.yml @@ -99,6 +99,7 @@ jobs: done - name: Create issue and assign to Copilot + id: create-issue if: steps.check.outputs.has_changes == 'true' env: GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} @@ -128,14 +129,15 @@ jobs: Follow the [agentic-merge-upstream](.github/prompts/agentic-merge-upstream.prompt.md) prompt to port these changes to the Java SDK." # Create the issue and assign to Copilot coding agent - gh issue create \ + ISSUE_URL=$(gh issue create \ --repo "$REPO" \ --title "Upstream sync: ${COMMIT_COUNT} new commits (${DATE})" \ --body "$BODY" \ --label "upstream-sync" \ - --assignee "copilot-swe-agent" + --assignee "copilot-swe-agent") - echo "✅ Issue created and assigned to Copilot coding agent." + echo "issue_url=$ISSUE_URL" >> "$GITHUB_OUTPUT" + echo "✅ Issue created and assigned to Copilot coding agent: $ISSUE_URL" - name: Summary if: always() @@ -144,6 +146,7 @@ jobs: COMMIT_COUNT="${{ steps.check.outputs.commit_count }}" LAST_MERGE="${{ steps.check.outputs.last_merge }}" UPSTREAM_HEAD="${{ steps.check.outputs.upstream_head }}" + ISSUE_URL="${{ steps.create-issue.outputs.issue_url }}" { echo "## Weekly Upstream Sync" @@ -157,7 +160,8 @@ jobs: echo "| **Last merged** | \`${LAST_MERGE:0:12}\` |" echo "| **Upstream HEAD** | \`${UPSTREAM_HEAD:0:12}\` |" echo "" - echo "An issue has been created and assigned to the Copilot coding agent." + echo "An issue has been created and assigned to the Copilot coding agent: " + echo " -> ${ISSUE_URL}" echo "" echo "### Recent upstream commits" echo "" From a6522029fa48685b49e2ed26ef222c28b0be9175 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:34:54 +0000 Subject: [PATCH 020/147] Port upstream setup guides as Java-focused deployment documentation - Add new setup.md covering deployment patterns: * Local CLI (personal development) * GitHub OAuth (multi-user apps) * Backend Services (server deployments) * Bundled CLI (distributable apps) * Scaling & Multi-Tenancy patterns - Update site.xml navigation to include Setup & Deployment - Update .lastmerge to b904431d1917e2b86f76fcde79a563921d8ef28c The upstream added comprehensive SDK-agnostic setup guides. Rather than duplicating those with Java examples, this change creates a Java-focused deployment guide that leverages the SDK's existing capabilities (githubToken, cliUrl, cliPath options) which already support all the upstream patterns. Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .lastmerge | 2 +- src/site/markdown/setup.md | 383 +++++++++++++++++++++++++++++++++++++ src/site/site.xml | 1 + 3 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 src/site/markdown/setup.md diff --git a/.lastmerge b/.lastmerge index 64a7ea328..0db96638f 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -05e3c46c8c23130c9c064dc43d00ec78f7a75eab +b904431d1917e2b86f76fcde79a563921d8ef28c diff --git a/src/site/markdown/setup.md b/src/site/markdown/setup.md new file mode 100644 index 000000000..660b5e75c --- /dev/null +++ b/src/site/markdown/setup.md @@ -0,0 +1,383 @@ +# Setup & Deployment Guide + +This guide explains how to configure the Copilot SDK for different deployment scenarios — from local development to production multi-user applications. + +## Quick Reference + +| Scenario | Configuration | Guide Section | +|----------|--------------|---------------| +| Local development | Default (no options) | [Local CLI](#local-cli) | +| Multi-user app | `setGithubToken(userToken)` | [GitHub OAuth](#github-oauth-authentication) | +| Server deployment | `setCliUrl("host:port")` | [Backend Services](#backend-services) | +| Custom CLI location | `setCliPath("/path/to/copilot")` | [Bundled CLI](#bundled-cli) | +| Own model keys | Provider configuration | [BYOK](advanced.html#Bring_Your_Own_Key_BYOK) | + +## Local CLI + +The simplest setup uses the Copilot CLI already signed in on your development machine. + +**Use when:** Building personal projects, prototyping, or learning the SDK. + +```java +try (var client = new CopilotClient()) { + client.start().get(); + var session = client.createSession( + new SessionConfig().setModel("gpt-4.1") + ).get(); + // Use session... +} +``` + +**How it works:** The SDK automatically spawns the CLI process and uses credentials from your system keychain. + +**Requirements:** +- Copilot CLI installed and signed in (`copilot auth login`) +- Active Copilot subscription + +See [Getting Started](getting-started.html) for a complete tutorial. + +## GitHub OAuth Authentication + +For multi-user applications where users authenticate with their GitHub accounts. + +**Use when:** Building apps where users have GitHub accounts and Copilot subscriptions. + +### Basic Setup + +After obtaining a user's GitHub OAuth token, pass it to the SDK: + +```java +var options = new CopilotClientOptions() + .setGithubToken(userAccessToken) + .setUseLoggedInUser(false); + +try (var client = new CopilotClient(options)) { + client.start().get(); + var session = client.createSession( + new SessionConfig().setModel("gpt-4.1") + ).get(); + // Requests are made on behalf of the authenticated user +} +``` + +### OAuth Flow Integration + +Your application handles the OAuth flow: + +1. Create a GitHub OAuth App in your GitHub settings +2. Redirect users to GitHub's authorization URL +3. Exchange the authorization code for an access token +4. Pass the token to `CopilotClientOptions.setGithubToken()` + +### Per-User Client Management + +Each authenticated user should get their own client instance: + +```java +private final Map clients = new ConcurrentHashMap<>(); + +public CopilotClient getClientForUser(String userId, String githubToken) { + return clients.computeIfAbsent(userId, id -> { + var options = new CopilotClientOptions() + .setGithubToken(githubToken) + .setUseLoggedInUser(false); + var client = new CopilotClient(options); + try { + client.start().get(); + } catch (Exception e) { + throw new RuntimeException("Failed to start client for user: " + userId, e); + } + return client; + }); +} +``` + +### Token Types + +| Token Prefix | Description | Supported | +|--------------|-------------|-----------| +| `gho_` | OAuth user access token | ✅ | +| `ghu_` | GitHub App user access token | ✅ | +| `github_pat_` | Fine-grained personal access token | ✅ | + +**Note:** Token lifecycle management (storage, refresh, expiration) is your application's responsibility. + +## Backend Services + +Run the SDK in server-side applications by connecting to an external CLI server. + +**Use when:** Building web backends, APIs, microservices, or any server-side workload. + +### Architecture + +Instead of spawning a CLI process, your application connects to a separately-running CLI server: + +``` +┌─────────────────┐ ┌─────────────────┐ +│ Your Backend │ │ CLI Server │ +│ │ │ (headless) │ +│ ┌───────────┐ │ │ ┌───────────┐ │ +│ │ SDK ├──┼──────►│ │JSON-RPC │ │ +│ └───────────┘ │ TCP │ │:4321 │ │ +└─────────────────┘ │ └───────────┘ │ + └─────────────────┘ +``` + +### Start the CLI Server + +Run the CLI in headless server mode: + +```bash +copilot server --port 4321 +``` + +Or with authentication: + +```bash +export GITHUB_TOKEN=your_token +copilot server --port 4321 +``` + +### Connect from Your Application + +Configure the SDK to connect to the external server: + +```java +var options = new CopilotClientOptions() + .setCliUrl("localhost:4321"); + +try (var client = new CopilotClient(options)) { + client.start().get(); + // Client connects to the external server +} +``` + +### Multiple SDK Clients, One Server + +Multiple application instances can share a single CLI server: + +```java +// In different parts of your application or different containers +var client1 = new CopilotClient(new CopilotClientOptions().setCliUrl("cli-server:4321")); +var client2 = new CopilotClient(new CopilotClientOptions().setCliUrl("cli-server:4321")); +// Both connect to the same CLI server +``` + +### Deployment Patterns + +**Container deployment:** +```yaml +# docker-compose.yml +services: + cli-server: + image: copilot-cli:latest + command: copilot server --port 4321 + environment: + - GITHUB_TOKEN=${GITHUB_TOKEN} + ports: + - "4321:4321" + + backend: + image: your-backend:latest + environment: + - CLI_URL=cli-server:4321 + depends_on: + - cli-server +``` + +**Kubernetes deployment:** +```yaml +apiVersion: v1 +kind: Service +metadata: + name: copilot-cli +spec: + selector: + app: copilot-cli + ports: + - port: 4321 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: copilot-cli +spec: + replicas: 1 + template: + spec: + containers: + - name: cli + image: copilot-cli:latest + args: ["server", "--port", "4321"] + env: + - name: GITHUB_TOKEN + valueFrom: + secretKeyRef: + name: copilot-auth + key: token +``` + +## Bundled CLI + +Package the Copilot CLI with your application so users don't need to install it separately. + +**Use when:** Distributing desktop applications or standalone tools. + +### Configuration + +Point the SDK to your bundled CLI binary: + +```java +var options = new CopilotClientOptions() + .setCliPath("./bundled/copilot"); // Relative to working directory + +try (var client = new CopilotClient(options)) { + client.start().get(); + // SDK uses the bundled CLI +} +``` + +### Packaging + +1. Download the appropriate CLI binary for your target platform +2. Include it in your application bundle: + ``` + my-app/ + ├── bin/ + │ └── copilot # CLI binary + ├── lib/ + │ └── my-app.jar + └── run.sh + ``` +3. Configure the path in your application + +### Platform-Specific Binaries + +For cross-platform applications, detect the platform and use the appropriate binary: + +```java +private String getCliPathForPlatform() { + String os = System.getProperty("os.name").toLowerCase(); + String arch = System.getProperty("os.arch").toLowerCase(); + + if (os.contains("win")) { + return "./bin/copilot-windows-" + arch + ".exe"; + } else if (os.contains("mac")) { + return "./bin/copilot-darwin-" + arch; + } else { + return "./bin/copilot-linux-" + arch; + } +} + +var options = new CopilotClientOptions() + .setCliPath(getCliPathForPlatform()); +``` + +## Scaling & Multi-Tenancy + +For applications serving many concurrent users, consider these patterns: + +### Session Isolation + +Each user's sessions are automatically isolated within their client instance. For strongest isolation, use one CLI server per user: + +```java +// Pattern: Isolated CLI per user (requires CLI server per user) +public CopilotClient createIsolatedClient(String userId, int port) { + // Start CLI server for this user: copilot server --port {port} + var options = new CopilotClientOptions() + .setCliUrl("localhost:" + port); + return new CopilotClient(options); +} +``` + +### Resource Management + +For high-concurrency scenarios: + +```java +// Use a client pool with bounded resources +public class CopilotClientPool { + private final Semaphore permits; + private final CopilotClient sharedClient; + + public CopilotClientPool(int maxConcurrentSessions) { + this.permits = new Semaphore(maxConcurrentSessions); + this.sharedClient = new CopilotClient(/* options */); + } + + public T withSession(SessionConfig config, + Function action) throws Exception { + permits.acquire(); + try { + var session = sharedClient.createSession(config).get(); + try { + return action.apply(session); + } finally { + session.close(); + } + } finally { + permits.release(); + } + } +} +``` + +### Horizontal Scaling + +When scaling beyond a single server: + +1. Run multiple CLI servers (one per app server or shared) +2. Use load balancing at the application tier +3. Each app server connects to its assigned CLI server via `setCliUrl()` + +## Configuration Reference + +Complete list of `CopilotClientOptions` settings: + +| Option | Type | Description | Default | +|--------|------|-------------|---------| +| `cliPath` | String | Path to CLI executable | `"copilot"` from PATH | +| `cliUrl` | String | External CLI server URL | `null` (spawn process) | +| `cliArgs` | String[] | Extra CLI arguments | `null` | +| `githubToken` | String | GitHub OAuth token | `null` | +| `useLoggedInUser` | Boolean | Use system credentials | `true` | +| `useStdio` | boolean | Use stdio transport | `true` | +| `port` | int | TCP port for CLI | `0` (random) | +| `autoStart` | boolean | Auto-start server | `true` | +| `autoRestart` | boolean | Auto-restart on crash | `true` | +| `logLevel` | String | CLI log level | `"info"` | +| `environment` | Map | Environment variables | inherited | +| `cwd` | String | Working directory | current dir | + +## Best Practices + +### Development + +- Use default configuration (local CLI) for fastest iteration +- Enable debug logging: `setLogLevel("debug")` +- Test with multiple models to ensure compatibility + +### Production + +- Use external CLI servers (`setCliUrl`) for better resource management +- Implement health checks on the CLI server endpoint +- Monitor CLI server resource usage (CPU, memory) +- Use connection pooling for high-concurrency scenarios +- Implement proper token refresh for OAuth-based auth +- Set appropriate timeouts for session operations + +### Security + +- Never log or expose GitHub tokens +- Use environment variables for tokens in production +- Regularly rotate tokens +- Implement proper access controls for multi-user apps +- Validate user input before sending to sessions + +## Next Steps + +- **[Getting Started](getting-started.html)** - Complete tutorial +- **[Documentation](documentation.html)** - Core concepts +- **[Advanced Usage](advanced.html)** - Tools, BYOK, MCP servers +- **[API Javadoc](apidocs/index.html)** - Complete API reference diff --git a/src/site/site.xml b/src/site/site.xml index dc6c67be8..7ca83b462 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -36,6 +36,7 @@ + From 7951af7212152c1c6cecf12fbc1dee91930414c5 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 15:02:13 -0500 Subject: [PATCH 021/147] Fix cliUrl to auto-correct useStdio for backend service scenarios (#97) When cliUrl is set (connecting to an external CLI server), useStdio is now automatically set to false since TCP is inherently required. Previously, users had to explicitly call setUseStdio(false) alongside setCliUrl(), which contradicted the setup.md documentation. Also simplified CliServerManager.connectToServer to be parameter-driven rather than flag-driven, and updated tests accordingly. --- .../github/copilot/sdk/CliServerManager.java | 16 +++++------ .../com/github/copilot/sdk/CopilotClient.java | 11 +++++--- .../github/copilot/sdk/CopilotClientTest.java | 27 +++++++++++++------ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CliServerManager.java b/src/main/java/com/github/copilot/sdk/CliServerManager.java index c95d049d5..1ac43c719 100644 --- a/src/main/java/com/github/copilot/sdk/CliServerManager.java +++ b/src/main/java/com/github/copilot/sdk/CliServerManager.java @@ -127,17 +127,15 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { * if connection fails */ JsonRpcClient connectToServer(Process process, String tcpHost, Integer tcpPort) throws IOException { - if (options.isUseStdio()) { - if (process == null) { - throw new IllegalStateException("CLI process not started"); - } - return JsonRpcClient.fromProcess(process); - } else { - if (tcpHost == null || tcpPort == null) { - throw new IllegalStateException("Cannot connect because TCP host or port are not available"); - } + if (tcpHost != null && tcpPort != null) { + // TCP mode: external server or child process with explicit port Socket socket = new Socket(tcpHost, tcpPort); return JsonRpcClient.fromSocket(socket); + } else if (process != null) { + // Stdio mode: child process + return JsonRpcClient.fromProcess(process); + } else { + throw new IllegalStateException("Cannot connect: no process for stdio and no host:port for TCP"); } } diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 5f7013815..377f6b64a 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -90,10 +90,15 @@ public CopilotClient() { public CopilotClient(CopilotClientOptions options) { this.options = options != null ? options : new CopilotClientOptions(); - // Validate mutually exclusive options + // When cliUrl is set, auto-correct useStdio since we're connecting via TCP + if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty()) { + this.options.setUseStdio(false); + } + + // Validate mutually exclusive options: cliUrl and cliPath cannot both be set if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty() - && (this.options.isUseStdio() || this.options.getCliPath() != null)) { - throw new IllegalArgumentException("CliUrl is mutually exclusive with UseStdio and CliPath"); + && this.options.getCliPath() != null) { + throw new IllegalArgumentException("CliUrl is mutually exclusive with CliPath"); } // Validate auth options with external server diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index d3ece944f..23e0d85ce 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -96,16 +96,29 @@ void testClientConstructionWithOptions() { } @Test - void testCliUrlMutualExclusion() { + void testCliUrlAutoCorrectsUseStdio() { var options = new CopilotClientOptions().setCliUrl("localhost:3000").setUseStdio(true); - assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); + // Should NOT throw - useStdio is auto-corrected to false when cliUrl is set + var client = new CopilotClient(options); + assertFalse(options.isUseStdio(), "useStdio should be auto-corrected to false when cliUrl is set"); + client.close(); + } + + @Test + void testCliUrlOnlyConstruction() { + var options = new CopilotClientOptions().setCliUrl("localhost:4321"); + + // Should work without explicitly setting useStdio to false + var client = new CopilotClient(options); + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + assertFalse(options.isUseStdio(), "useStdio should be auto-corrected to false when cliUrl is set"); + client.close(); } @Test void testCliUrlMutualExclusionWithCliPath() { - var options = new CopilotClientOptions().setCliUrl("localhost:3000").setCliPath("/path/to/cli") - .setUseStdio(false); + var options = new CopilotClientOptions().setCliUrl("localhost:3000").setCliPath("/path/to/cli"); assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); } @@ -194,16 +207,14 @@ void testExplicitUseLoggedInUserTrueWithGithubToken() { @Test void testGithubTokenWithCliUrlThrows() { - var options = new CopilotClientOptions().setCliUrl("localhost:8080").setGithubToken("gho_test_token") - .setUseStdio(false); + var options = new CopilotClientOptions().setCliUrl("localhost:8080").setGithubToken("gho_test_token"); assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); } @Test void testUseLoggedInUserWithCliUrlThrows() { - var options = new CopilotClientOptions().setCliUrl("localhost:8080").setUseLoggedInUser(false) - .setUseStdio(false); + var options = new CopilotClientOptions().setCliUrl("localhost:8080").setUseLoggedInUser(false); assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); } From bf3a457d1819a7a8da56b4ddaf092952079dbe1f Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 15:24:27 -0500 Subject: [PATCH 022/147] Add commit-as-pull-request skill for Copilot and Claude Code (#98) Adds a reusable skill that automates the workflow of committing changes to a new branch, pushing, creating a PR, squash-merging, and syncing local main. Registered for both GitHub Copilot (.github/skills/) and Claude Code (.claude/skills/), sharing a single prompt file. --- .../skills/commit-as-pull-request/SKILL.md | 9 ++ .../prompts/commit-as-pull-request.prompt.md | 113 ++++++++++++++++++ .../skills/commit-as-pull-request/SKILL.md | 9 ++ 3 files changed, 131 insertions(+) create mode 100644 .claude/skills/commit-as-pull-request/SKILL.md create mode 100644 .github/prompts/commit-as-pull-request.prompt.md create mode 100644 .github/skills/commit-as-pull-request/SKILL.md diff --git a/.claude/skills/commit-as-pull-request/SKILL.md b/.claude/skills/commit-as-pull-request/SKILL.md new file mode 100644 index 000000000..ba65e73cb --- /dev/null +++ b/.claude/skills/commit-as-pull-request/SKILL.md @@ -0,0 +1,9 @@ +```skill +--- +name: commit-as-pull-request +description: Commit current changes as a pull request — creates a branch, pushes, opens a PR, squash-merges, and syncs local main. +license: MIT +--- + +Follow instructions in the [commit-as-pull-request prompt](../../../.github/prompts/commit-as-pull-request.prompt.md) to take the current uncommitted changes, create a feature branch, push it, open a pull request, squash-merge it into main, and sync the local repository. +``` diff --git a/.github/prompts/commit-as-pull-request.prompt.md b/.github/prompts/commit-as-pull-request.prompt.md new file mode 100644 index 000000000..684e3ee9a --- /dev/null +++ b/.github/prompts/commit-as-pull-request.prompt.md @@ -0,0 +1,113 @@ +# Commit as Pull Request + +You are an automated assistant that takes the current uncommitted changes in the workspace, creates a branch, commits, pushes, opens a pull request, merges it, and syncs the local `main` branch. + +## Prerequisites + +- The workspace must be a git repository with a configured remote named `origin`. +- There must be uncommitted changes (staged or unstaged) in the working tree. +- The GitHub MCP tools must be available for creating and merging pull requests. + +## Workflow + +Execute the following steps **in order**. Stop immediately if any step fails. + +### Step 1: Verify there are changes to commit + +Run `git status --porcelain` to confirm there are uncommitted changes. If the output is empty, inform the user there is nothing to commit and stop. + +### Step 2: Determine the repository owner and name + +Read the repository remote URL to extract the GitHub `owner` and `repo`: + +```bash +git remote get-url origin +``` + +Parse the owner and repo from the URL (handles both HTTPS and SSH formats). + +### Step 3: Auto-detect branch name and commit message + +Analyze the changed files using `git diff` (and `git diff --cached` for staged changes) to understand what was modified. Generate: + +- **Branch name**: A short, kebab-case branch name prefixed with an appropriate category (`fix/`, `feat/`, `docs/`, `refactor/`, `chore/`). Example: `fix/cliurl-auto-correct-usestdio`. +- **Commit message**: A clear, descriptive commit message following the project conventions: + - First line: imperative verb, under 72 characters (e.g., "Fix cliUrl to auto-correct useStdio") + - Body (if needed): explain *why* the change was made + +If the user has provided an explicit branch name or commit message, use those instead. + +### Step 4: Run code formatter + +If the project has a formatter configured, run it before committing: + +```bash +mvn spotless:apply +``` + +Only run this if a `pom.xml` exists with Spotless configured. Skip for non-Maven projects. + +### Step 5: Create branch, stage, and commit + +```bash +git checkout -b +git add -A +git commit -m "" +``` + +### Step 6: Push the branch + +```bash +git push -u origin +``` + +If the push reports "Everything up-to-date", verify with `git log --oneline -1` that the commit exists, then retry with `git push -u origin 2>&1`. + +### Step 7: Create a pull request + +Use the GitHub MCP `create_pull_request` tool with: + +- **owner** and **repo**: from Step 2 +- **title**: the first line of the commit message +- **head**: the branch name +- **base**: `main` (or the repository's default branch) +- **body**: A well-structured PR description including: + - **Summary**: What the change does and why + - **Changes**: Bullet list of files/areas modified + - **Testing**: How the changes were verified + +### Step 8: Merge the pull request + +Use the GitHub MCP `merge_pull_request` tool with: + +- **merge_method**: `squash` +- **commit_title**: ` (#)` + +### Step 9: Sync local main + +```bash +git checkout main +git pull +``` + +### Step 10: Clean up the local branch (optional) + +```bash +git branch -d +``` + +## Error Handling + +- If the branch name already exists, append a numeric suffix (e.g., `fix/my-change-2`). +- If the push fails due to authentication, inform the user and stop. +- If the PR creation fails, provide the error and stop. +- If the merge fails (e.g., merge conflicts, required checks), inform the user and leave the PR open. + +## Output + +After completion, provide a brief summary: + +1. Branch name +2. PR URL and number +3. Merge commit SHA +4. Confirmation that local `main` is up to date diff --git a/.github/skills/commit-as-pull-request/SKILL.md b/.github/skills/commit-as-pull-request/SKILL.md new file mode 100644 index 000000000..f29f09a41 --- /dev/null +++ b/.github/skills/commit-as-pull-request/SKILL.md @@ -0,0 +1,9 @@ +```skill +--- +name: commit-as-pull-request +description: Commit current changes as a pull request — creates a branch, pushes, opens a PR, squash-merges, and syncs local main. +license: MIT +--- + +Follow instructions in the [commit-as-pull-request prompt](../../prompts/commit-as-pull-request.prompt.md) to take the current uncommitted changes, create a feature branch, push it, open a pull request, squash-merge it into main, and sync the local repository. +``` From 6499fd32d99890e7845fdf43529c0bdb5d1c0642 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:40:02 +0000 Subject: [PATCH 023/147] Initial plan From 199bf370c92dd0dabff7ce23a8ef847d370ca22c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:47:33 +0000 Subject: [PATCH 024/147] Add GitHub Agentic Workflow for test coverage improvement Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .gitattributes | 1 + .../workflows/increase-test-coverage.lock.yml | 1097 +++++++++++++++++ .github/workflows/increase-test-coverage.md | 174 +++ 3 files changed, 1272 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/increase-test-coverage.lock.yml create mode 100644 .github/workflows/increase-test-coverage.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..c1965c216 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/workflows/increase-test-coverage.lock.yml b/.github/workflows/increase-test-coverage.lock.yml new file mode 100644 index 000000000..553e38673 --- /dev/null +++ b/.github/workflows/increase-test-coverage.lock.yml @@ -0,0 +1,1097 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.42.17). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md +# +# Automated workflow to identify untested code and add comprehensive tests to increase coverage. +# Groups related tests sensibly and creates PRs for human review. +# +# frontmatter-hash: 356158feac1509bba005f2374c86a35bd82440bae39e70a1aefb4cd2d5c7ff11 + +name: "Increase Test Coverage" +"on": + workflow_dispatch: + inputs: + min_coverage_threshold: + default: 80 + description: "Minimum coverage percentage to aim for (default: 80)" + required: false + type: number + target_package: + description: Specific package to focus on (e.g., com.github.copilot.sdk.json, com.github.copilot.sdk.events, or leave empty for auto-select) + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Increase Test Coverage" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_WORKFLOW_FILE: "increase-test-coverage.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + 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_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + model: ${{ steps.generate_aw_info.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Checkout .github and .agents folders + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + with: + sparse-checkout: | + .github + .agents + depth: 1 + persist-credentials: false + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.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('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.13.12 + - name: Determine automatic lockdown mode for GitHub MCP server + id: determine-automatic-lockdown + env: + TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + if: env.TOKEN_CHECK != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.13.12 ghcr.io/github/gh-aw-firewall/squid:0.13.12 ghcr.io/github/gh-aw-mcpg:v0.0.113 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p /opt/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' + {"add_comment":{"max":1},"create_pull_request":{},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' + [ + { + "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 1 comment(s) can be added.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "The comment text in Markdown format. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation.", + "type": "string" + }, + "item_number": { + "description": "The issue, pull request, or discussion number to comment on. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123). If omitted, the tool will attempt to resolve the target from the current workflow context (triggering issue, PR, or discussion).", + "type": "number" + } + }, + "required": [ + "body" + ], + "type": "object" + }, + "name": "add_comment" + }, + { + "description": "Create a new GitHub pull request to propose code changes. Use this after making file edits to submit them for review and merging. The PR will be created from the current branch with your committed changes. For code review comments on an existing PR, use create_pull_request_review_comment instead. CONSTRAINTS: Maximum 1 pull request(s) can be created.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "Detailed PR description in Markdown. Include what changes were made, why, testing notes, and any breaking changes. Do NOT repeat the title as a heading.", + "type": "string" + }, + "branch": { + "description": "Source branch name containing the changes. If omitted, uses the current working branch.", + "type": "string" + }, + "labels": { + "description": "Labels to categorize the PR (e.g., 'enhancement', 'bugfix'). Labels must exist in the repository.", + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "description": "Concise PR title describing the changes. Follow repository conventions (e.g., conventional commits). The title appears as the main heading.", + "type": "string" + } + }, + "required": [ + "title", + "body" + ], + "type": "object" + }, + "name": "create_pull_request" + }, + { + "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "reason": { + "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", + "type": "string" + }, + "tool": { + "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", + "type": "string" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "name": "missing_tool" + }, + { + "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "name": "noop" + }, + { + "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "context": { + "description": "Additional context about the missing data or where it should come from (max 256 characters).", + "type": "string" + }, + "data_type": { + "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", + "type": "string" + }, + "reason": { + "description": "Explanation of why this data is needed to complete the task (max 256 characters).", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "name": "missing_data" + } + ] + EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + } + } + }, + "create_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "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 + } + } + } + } + EOF + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + API_KEY="" + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + PORT=3001 + + # Register API key as secret to mask it from logs + echo "::add-mask::${API_KEY}" + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash /opt/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY="" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export DEBUG="*" + + # Register API key as secret to mask it from logs + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -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_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_LOCKDOWN -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 GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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 ghcr.io/github/gh-aw-mcpg:v0.0.113' + + mkdir -p /home/runner/.copilot + cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.30.3", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "repos,pull_requests,issues" + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + MCPCONFIG_EOF + - name: Generate agentic run info + id: generate_aw_info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "copilot", + engine_name: "GitHub Copilot CLI", + model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", + version: "", + agent_version: "0.0.405", + cli_version: "v0.42.17", + workflow_name: "Increase Test Coverage", + experimental: false, + supports_tools_allowlist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults"], + firewall_enabled: true, + awf_version: "v0.13.12", + awmg_version: "v0.0.113", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in PR + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/jobs + core.setOutput('model', awInfo.model); + - name: Generate workflow overview + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); + await generateWorkflowOverview(core); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" + + PROMPT_EOF + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + + GitHub API Access Instructions + + The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. + + + To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. + + Discover available tools from the safeoutputs MCP server. + + **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. + + **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. + + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + PROMPT_EOF + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + + PROMPT_EOF + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/workflows/increase-test-coverage.md}} + PROMPT_EOF + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + 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 + } + }); + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 30 + run: | + set -o pipefail + sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.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 --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.13.12 --skip-pull \ + -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' \ + 2>&1 | tee /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: | + # Copy Copilot session state files to logs folder for artifact collection + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; then + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true + echo "Session state files copied successfully" + else + echo "No session-state directory found at $SESSION_STATE_DIR" + fi + - 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 /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + 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: Upload Safe Outputs + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: safe-output + path: ${{ env.GH_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output + id: collect_output + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.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" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Upload sanitized agent output + if: always() && env.GH_AW_AGENT_OUTPUT + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-output + path: ${{ env.GH_AW_AGENT_OUTPUT }} + if-no-files-found: warn + - name: Upload engine output files + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent_outputs + path: | + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + if-no-files-found: ignore + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/aw.patch + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + outputs: + 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 + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: 1 + GH_AW_WORKFLOW_NAME: "Increase Test Coverage" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Increase Test Coverage" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Increase Test Coverage" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Handle No-Op Message + id: handle_noop_message + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Increase Test Coverage" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Handle Create Pull Request Error + id: handle_create_pr_error + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Increase Test Coverage" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_create_pr_error.cjs'); + await main(); + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Increase Test Coverage" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/notify_comment_error.cjs'); + await main(); + + detection: + needs: agent + if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' + runs-on: ubuntu-latest + permissions: {} + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + timeout-minutes: 10 + outputs: + success: ${{ steps.parse_results.outputs.success }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Download agent artifacts + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-artifacts + path: /tmp/gh-aw/threat-detection/ + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/threat-detection/ + - name: Echo agent output types + env: + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + run: | + echo "Agent output-types: $AGENT_OUTPUT_TYPES" + - name: Setup threat detection + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + WORKFLOW_NAME: "Increase Test Coverage" + WORKFLOW_DESCRIPTION: "Automated workflow to identify untested code and add comprehensive tests to increase coverage.\nGroups related tests sensibly and creates PRs for human review." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool shell(cat) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(tail) + # --allow-tool shell(wc) + timeout-minutes: 20 + run: | + set -o pipefail + COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" + mkdir -p /tmp/ + mkdir -p /tmp/gh-aw/ + mkdir -p /tmp/gh-aw/agent/ + mkdir -p /tmp/gh-aw/sandbox/agent/logs/ + copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection results + id: parse_results + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection log + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: threat-detection.log + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + + safe_outputs: + needs: + - activation + - agent + - detection + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + discussions: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_ENGINE_ID: "copilot" + GH_AW_WORKFLOW_ID: "increase-test-coverage" + GH_AW_WORKFLOW_NAME: "Increase Test Coverage" + outputs: + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + 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 }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-artifacts + path: /tmp/gh-aw/ + - name: Checkout repository + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + with: + token: ${{ github.token }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":false,\"max\":1,\"max_patch_size\":1024},\"missing_data\":{},\"missing_tool\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + diff --git a/.github/workflows/increase-test-coverage.md b/.github/workflows/increase-test-coverage.md new file mode 100644 index 000000000..d3b021591 --- /dev/null +++ b/.github/workflows/increase-test-coverage.md @@ -0,0 +1,174 @@ +--- +name: Increase Test Coverage +description: | + Automated workflow to identify untested code and add comprehensive tests to increase coverage. + Groups related tests sensibly and creates PRs for human review. + +on: + workflow_dispatch: + inputs: + target_package: + description: 'Specific package to focus on (e.g., com.github.copilot.sdk.json, com.github.copilot.sdk.events, or leave empty for auto-select)' + required: false + type: string + min_coverage_threshold: + description: 'Minimum coverage percentage to aim for (default: 80)' + required: false + type: number + default: 80 + +permissions: + contents: read + pull-requests: read + issues: read + +tools: + github: + toolsets: + - repos + - pull_requests + - issues + edit: + +safe-outputs: + create-pull-request: + draft: false + add-comment: {} + +timeout-minutes: 30 + +--- + +# Test Coverage Enhancement Agent + +You are a test coverage expert for the `copilot-sdk-java` repository. Your mission is to identify code that lacks adequate test coverage and create comprehensive, high-quality tests. + +## Your Task + +1. **Analyze Current Coverage** + - Run `mvn clean test jacoco:report` to generate coverage reports + - Examine the JaCoCo report at `target/site/jacoco-coverage/index.html` + - Identify files and packages with low coverage (below {{ inputs.min_coverage_threshold }}%) + - Look at the detailed coverage reports to find specific untested methods and branches + +2. **Prioritize Testing Work** + {% if inputs.target_package %} + - Focus specifically on the `{{ inputs.target_package }}` package as requested + {% else %} + - Prioritize core SDK classes in `com.github.copilot.sdk` package first + - Then focus on `com.github.copilot.sdk.json` and `com.github.copilot.sdk.events` packages + {% endif %} + - Focus on: + - Public API methods that aren't tested + - Error handling paths (exception cases) + - Edge cases and boundary conditions + - Branch coverage (if/else, switch statements) + - Complex methods with low cyclomatic complexity coverage + +3. **Group Tests Logically** + - Group related functionality together (e.g., all tests for error handling in a single class) + - Create test classes that mirror the structure of source classes + - Follow existing test patterns in the repository (see `src/test/java` for examples) + - Tests should be in the same package as the code they test + +4. **Write High-Quality Tests** + - Follow the existing test patterns in the repository (examine existing test files) + - Use JUnit 5 (already configured in the project) + - Follow the E2E test pattern using `E2ETestContext` when testing Copilot client/session functionality + - For unit tests, use standard JUnit assertions + - Test both success and failure paths + - Include descriptive test names that explain what is being tested + - Add comments only when necessary to explain complex test scenarios + - Ensure tests are deterministic and don't depend on external state + +5. **Build Commands** (from repository instructions) + - Build and test: `mvn clean verify` + - Run specific test: `mvn test -Dtest=ClassName#methodName` + - Format code: `mvn spotless:apply` (REQUIRED before committing) + - Check format: `mvn spotless:check` + +6. **Verify Your Changes** + - Run `mvn spotless:apply` to format the code + - Run the new tests: `mvn test -Dtest=YourNewTestClass` + - Run all tests to ensure nothing broke: `mvn clean verify` + - Generate new coverage report: `mvn jacoco:report` + - Verify coverage increased in target areas + +7. **Create a Pull Request** + - Use descriptive PR title: "Add tests for [functionality/package] to increase coverage" + - In the PR body, include: + - Which code/package you added tests for + - Coverage before and after (percentage and specific methods covered) + - Number of new test cases added + - Any important testing patterns or decisions + - Use the `create-pull-request` safe output with: + - `title`: Clear description of test additions + - `body`: Detailed summary as described above + - `head`: Create branch named `test-coverage-[package-or-feature]` + - `base`: Target the default branch + - `draft`: Set to false so it's ready for review + +## Important Guidelines + +- **Minimal Changes**: Only add tests - do NOT modify production code unless absolutely necessary for testability +- **Follow Repository Patterns**: Study existing tests before writing new ones + - E2E tests use `E2ETestContext.create()` and snapshot-based testing + - Unit tests follow standard JUnit patterns + - Test file naming: `*Test.java` (e.g., `CopilotClientTest.java`) +- **Code Style**: This project uses Spotless for formatting - ALWAYS run `mvn spotless:apply` before committing +- **Test Quality**: Tests should be clear, maintainable, and actually test the intended behavior +- **Branch Names**: Use descriptive names like `test-coverage-json-package` or `test-coverage-error-handling` +- **Safety**: Never commit files from `target/`, `node_modules/`, or other build artifacts + +## Example Workflow + +```bash +# 1. Generate coverage report +mvn clean test jacoco:report + +# 2. Examine coverage (use view/grep tools to read the HTML report) +# Look at target/site/jacoco-coverage/index.html and package-specific reports + +# 3. Identify low-coverage files +# Example: SessionConfig.java shows 65% coverage, missing tests for edge cases + +# 4. Create test file (if it doesn't exist) or add to existing test +# Follow pattern: src/test/java/com/github/copilot/sdk/json/SessionConfigTest.java + +# 5. Write tests following existing patterns + +# 6. Format and verify +mvn spotless:apply +mvn test -Dtest=SessionConfigTest +mvn clean verify + +# 7. Check new coverage +mvn jacoco:report + +# 8. Create PR using safe output +``` + +## Repository Context + +- **Language**: Java 17+ +- **Build Tool**: Maven +- **Test Framework**: JUnit 5 +- **Coverage Tool**: JaCoCo +- **Code Formatter**: Spotless (Eclipse style, 4-space indent) +- **Current Coverage**: ~66% (aim for {{ inputs.min_coverage_threshold }}%+) + +## Key Packages to Focus On + +1. **com.github.copilot.sdk** - Core SDK classes (CopilotClient, CopilotSession, JsonRpcClient) +2. **com.github.copilot.sdk.json** - DTOs and request/response types +3. **com.github.copilot.sdk.events** - Event types and handlers + +## Remember + +- Quality over quantity - a few well-written tests are better than many shallow tests +- Test behavior, not implementation details +- Focus on increasing coverage of important code paths +- Make the PR reviewable by grouping related tests together +- Always run spotless:apply before creating the PR + +Good luck! Your work will help ensure the SDK is reliable and maintainable. From b98f953e619487bb487b718041fe2b500418fc9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:48:10 +0000 Subject: [PATCH 025/147] Document the test coverage workflow in README Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 8c96da2ef..2935c8771 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,26 @@ This SDK tracks the official [Copilot SDK](https://github.com/github/copilot-sdk - **VS Code Copilot Chat** — type `/agentic-merge-upstream` - **GitHub Copilot CLI** — use `copilot` CLI with the same skill reference +### Automated Test Coverage Improvement + +The repository includes an [Agentic Workflow](https://github.com/github/gh-aw) that automatically identifies untested code and creates comprehensive tests to increase coverage. The workflow is triggered manually: + +**To run the workflow:** +1. Go to the [Actions tab](../../actions/workflows/increase-test-coverage.lock.yml) +2. Click "Run workflow" +3. Optionally specify: + - `target_package` — Focus on a specific package (e.g., `com.github.copilot.sdk.json`) + - `min_coverage_threshold` — Target coverage percentage (default: 80%) + +**What it does:** +- Analyzes current test coverage using JaCoCo +- Identifies methods, branches, and edge cases lacking tests +- Creates high-quality tests following repository patterns +- Groups related tests logically +- Creates a PR for human review with coverage metrics + +The workflow follows all repository conventions (Spotless formatting, existing test patterns, E2E test infrastructure) and ensures no production code is modified unnecessarily. + ### Development Setup ```bash From ff8b439a5d11c8cdabf0ea03fcbbf29032101718 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 15:48:25 -0500 Subject: [PATCH 026/147] Organize .github/scripts into categorized subfolders (#100) Move scripts into build/, ci/, release/, and upstream-sync/ subfolders based on their usage. Update all references in workflows, prompts, copilot-instructions.md, and script usage comments. --- .github/copilot-instructions.md | 2 +- .../prompts/agentic-merge-upstream.prompt.md | 34 +++++++++---------- .../coding-agent-merge-instructions.md | 2 +- .../scripts/{ => build}/format-and-test.sh | 8 ++--- .../create-issue-assigned-to-copilot.ts | 0 .../{ => release}/test-update-changelog.sh | 0 .../scripts/{ => release}/update-changelog.sh | 0 .../merge-upstream-diff.sh | 4 +-- .../merge-upstream-finish.sh | 8 ++--- .../merge-upstream-start.sh | 6 ++-- .github/workflows/copilot-issue.yml | 6 ++-- .github/workflows/publish-maven.yml | 2 +- 12 files changed, 36 insertions(+), 36 deletions(-) rename .github/scripts/{ => build}/format-and-test.sh (82%) rename .github/scripts/{ => ci}/create-issue-assigned-to-copilot.ts (100%) rename .github/scripts/{ => release}/test-update-changelog.sh (100%) rename .github/scripts/{ => release}/update-changelog.sh (100%) rename .github/scripts/{ => upstream-sync}/merge-upstream-diff.sh (95%) rename .github/scripts/{ => upstream-sync}/merge-upstream-finish.sh (87%) rename .github/scripts/{ => upstream-sync}/merge-upstream-start.sh (93%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 375278cff..65d62e0b0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -254,7 +254,7 @@ This SDK is designed to be **lightweight with minimal dependencies**: The release process is automated via the `publish-maven.yml` GitHub Actions workflow. Key steps: -1. **CHANGELOG Update**: The script `.github/scripts/update-changelog.sh` automatically: +1. **CHANGELOG Update**: The script `.github/scripts/release/update-changelog.sh` automatically: - Converts the `## [Unreleased]` section to `## [version] - date` - Creates a new empty `## [Unreleased]` section at the top - Updates version comparison links at the bottom of CHANGELOG.md diff --git a/.github/prompts/agentic-merge-upstream.prompt.md b/.github/prompts/agentic-merge-upstream.prompt.md index d962373c3..8db8e8446 100644 --- a/.github/prompts/agentic-merge-upstream.prompt.md +++ b/.github/prompts/agentic-merge-upstream.prompt.md @@ -19,22 +19,22 @@ The `.github/scripts/` directory contains helper scripts that automate the repea | Script | Purpose | |---|---| -| `.github/scripts/merge-upstream-start.sh` | Creates branch, updates CLI, clones upstream, reads `.lastmerge`, prints commit summary | -| `.github/scripts/merge-upstream-diff.sh` | Detailed diff analysis grouped by area (`.NET src`, tests, snapshots, docs, etc.) | -| `.github/scripts/merge-upstream-finish.sh` | Runs format + test + build, updates `.lastmerge`, commits, pushes branch | -| `.github/scripts/format-and-test.sh` | Standalone `spotless:apply` + `mvn clean verify` (useful during porting too) | +| `.github/scripts/upstream-sync/merge-upstream-start.sh` | Creates branch, updates CLI, clones upstream, reads `.lastmerge`, prints commit summary | +| `.github/scripts/upstream-sync/merge-upstream-diff.sh` | Detailed diff analysis grouped by area (`.NET src`, tests, snapshots, docs, etc.) | +| `.github/scripts/upstream-sync/merge-upstream-finish.sh` | Runs format + test + build, updates `.lastmerge`, commits, pushes branch | +| `.github/scripts/build/format-and-test.sh` | Standalone `spotless:apply` + `mvn clean verify` (useful during porting too) | All scripts write/read a `.merge-env` file (git-ignored) to share state (branch name, upstream dir, last-merge commit). ## Workflow Overview -1. Run `./.github/scripts/merge-upstream-start.sh` (creates branch, clones upstream, shows summary) -2. Run `./.github/scripts/merge-upstream-diff.sh` (analyze changes) +1. Run `./.github/scripts/upstream-sync/merge-upstream-start.sh` (creates branch, clones upstream, shows summary) +2. Run `./.github/scripts/upstream-sync/merge-upstream-diff.sh` (analyze changes) 3. Update README with minimum CLI version requirement 4. Port changes to Java SDK (commit as you go) -5. Run `./.github/scripts/format-and-test.sh` frequently while porting +5. Run `./.github/scripts/build/format-and-test.sh` frequently while porting 6. Update documentation -7. Run `./.github/scripts/merge-upstream-finish.sh` (final test + push) +7. Run `./.github/scripts/upstream-sync/merge-upstream-finish.sh` (final test + push) 8. Finalize Pull Request (see note below about coding agent vs. manual workflow) --- @@ -44,7 +44,7 @@ All scripts write/read a `.merge-env` file (git-ignored) to share state (branch Run the start script to create a branch, update the CLI, clone the upstream repo, and see a summary of new commits: ```bash -./.github/scripts/merge-upstream-start.sh +./.github/scripts/upstream-sync/merge-upstream-start.sh ``` This writes a `.merge-env` file used by the other scripts. It outputs: @@ -56,8 +56,8 @@ This writes a `.merge-env` file used by the other scripts. It outputs: Then run the diff script for a detailed breakdown by area: ```bash -./.github/scripts/merge-upstream-diff.sh # stat only -./.github/scripts/merge-upstream-diff.sh --full # full diffs +./.github/scripts/upstream-sync/merge-upstream-diff.sh # stat only +./.github/scripts/upstream-sync/merge-upstream-diff.sh --full # full diffs ``` The diff script groups changes into: .NET source, .NET tests, test snapshots, documentation, protocol/config, Go/Node.js/Python SDKs, and other files. @@ -220,15 +220,15 @@ Commit tests separately or together with their corresponding implementation chan After applying changes, use the convenience script: ```bash -./.github/scripts/format-and-test.sh # format + full verify -./.github/scripts/format-and-test.sh --debug # with debug logging +./.github/scripts/build/format-and-test.sh # format + full verify +./.github/scripts/build/format-and-test.sh --debug # with debug logging ``` Or for quicker iteration during porting: ```bash -./.github/scripts/format-and-test.sh --format-only # just spotless -./.github/scripts/format-and-test.sh --test-only # skip formatting +./.github/scripts/build/format-and-test.sh --format-only # just spotless +./.github/scripts/build/format-and-test.sh --test-only # skip formatting ``` ### If Tests Fail @@ -321,8 +321,8 @@ Ensure consistency across all documentation files: Run the finish script which updates `.lastmerge`, runs a final build, and pushes the branch: ```bash -./.github/scripts/merge-upstream-finish.sh # full format + test + push -./.github/scripts/merge-upstream-finish.sh --skip-tests # if tests already passed +./.github/scripts/upstream-sync/merge-upstream-finish.sh # full format + test + push +./.github/scripts/upstream-sync/merge-upstream-finish.sh --skip-tests # if tests already passed ``` ### PR Handling: Coding Agent vs. Manual Workflow diff --git a/.github/prompts/coding-agent-merge-instructions.md b/.github/prompts/coding-agent-merge-instructions.md index 429bb5382..1c18e1f5f 100644 --- a/.github/prompts/coding-agent-merge-instructions.md +++ b/.github/prompts/coding-agent-merge-instructions.md @@ -4,7 +4,7 @@ Follow the agentic-merge-upstream prompt at .github/prompts/agentic-merge-upstream.prompt.md to port upstream changes to the Java SDK. -Use the utility scripts in .github/scripts/ for initialization, diffing, formatting, and testing. +Use the utility scripts in .github/scripts/ subfolders for initialization, diffing, formatting, and testing. Commit changes incrementally. Update .lastmerge when done. IMPORTANT: A pull request has already been created automatically for you — do NOT create a new diff --git a/.github/scripts/format-and-test.sh b/.github/scripts/build/format-and-test.sh similarity index 82% rename from .github/scripts/format-and-test.sh rename to .github/scripts/build/format-and-test.sh index 857e6ce36..6de3972f3 100755 --- a/.github/scripts/format-and-test.sh +++ b/.github/scripts/build/format-and-test.sh @@ -6,10 +6,10 @@ # 1. spotless:apply (auto-format code) # 2. mvn clean verify (compile + test + checkstyle + spotbugs) # -# Usage: ./.github/scripts/format-and-test.sh -# ./.github/scripts/format-and-test.sh --format-only -# ./.github/scripts/format-and-test.sh --test-only -# ./.github/scripts/format-and-test.sh --debug (uses -Pdebug) +# Usage: ./.github/scripts/build/format-and-test.sh +# ./.github/scripts/build/format-and-test.sh --format-only +# ./.github/scripts/build/format-and-test.sh --test-only +# ./.github/scripts/build/format-and-test.sh --debug (uses -Pdebug) # ────────────────────────────────────────────────────────────── set -euo pipefail diff --git a/.github/scripts/create-issue-assigned-to-copilot.ts b/.github/scripts/ci/create-issue-assigned-to-copilot.ts similarity index 100% rename from .github/scripts/create-issue-assigned-to-copilot.ts rename to .github/scripts/ci/create-issue-assigned-to-copilot.ts diff --git a/.github/scripts/test-update-changelog.sh b/.github/scripts/release/test-update-changelog.sh similarity index 100% rename from .github/scripts/test-update-changelog.sh rename to .github/scripts/release/test-update-changelog.sh diff --git a/.github/scripts/update-changelog.sh b/.github/scripts/release/update-changelog.sh similarity index 100% rename from .github/scripts/update-changelog.sh rename to .github/scripts/release/update-changelog.sh diff --git a/.github/scripts/merge-upstream-diff.sh b/.github/scripts/upstream-sync/merge-upstream-diff.sh similarity index 95% rename from .github/scripts/merge-upstream-diff.sh rename to .github/scripts/upstream-sync/merge-upstream-diff.sh index 983cd7d0b..799a227a3 100755 --- a/.github/scripts/merge-upstream-diff.sh +++ b/.github/scripts/upstream-sync/merge-upstream-diff.sh @@ -10,7 +10,7 @@ # • Documentation # • Protocol / config files # -# Usage: ./.github/scripts/merge-upstream-diff.sh [--full] +# Usage: ./.github/scripts/upstream-sync/merge-upstream-diff.sh [--full] # --full Show actual diffs, not just stats # # Requires: .merge-env written by merge-upstream-start.sh @@ -21,7 +21,7 @@ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" ENV_FILE="$ROOT_DIR/.merge-env" if [[ ! -f "$ENV_FILE" ]]; then - echo "❌ $ENV_FILE not found. Run ./.github/scripts/merge-upstream-start.sh first." + echo "❌ $ENV_FILE not found. Run ./.github/scripts/upstream-sync/merge-upstream-start.sh first." exit 1 fi diff --git a/.github/scripts/merge-upstream-finish.sh b/.github/scripts/upstream-sync/merge-upstream-finish.sh similarity index 87% rename from .github/scripts/merge-upstream-finish.sh rename to .github/scripts/upstream-sync/merge-upstream-finish.sh index 69ad51ed3..06ac7c71b 100755 --- a/.github/scripts/merge-upstream-finish.sh +++ b/.github/scripts/upstream-sync/merge-upstream-finish.sh @@ -8,8 +8,8 @@ # 3. Commits the .lastmerge update # 4. Pushes the branch to origin # -# Usage: ./.github/scripts/merge-upstream-finish.sh -# ./.github/scripts/merge-upstream-finish.sh --skip-tests +# Usage: ./.github/scripts/upstream-sync/merge-upstream-finish.sh +# ./.github/scripts/upstream-sync/merge-upstream-finish.sh --skip-tests # # Requires: .merge-env written by merge-upstream-start.sh # ────────────────────────────────────────────────────────────── @@ -19,7 +19,7 @@ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" ENV_FILE="$ROOT_DIR/.merge-env" if [[ ! -f "$ENV_FILE" ]]; then - echo "❌ $ENV_FILE not found. Run ./.github/scripts/merge-upstream-start.sh first." + echo "❌ $ENV_FILE not found. Run ./.github/scripts/upstream-sync/merge-upstream-start.sh first." exit 1 fi @@ -40,7 +40,7 @@ if $SKIP_TESTS; then mvn clean package -DskipTests else echo "▸ Running format + test + build…" - "$ROOT_DIR/.github/scripts/format-and-test.sh" + "$ROOT_DIR/.github/scripts/build/format-and-test.sh" fi # ── 2. Update .lastmerge ───────────────────────────────────── diff --git a/.github/scripts/merge-upstream-start.sh b/.github/scripts/upstream-sync/merge-upstream-start.sh similarity index 93% rename from .github/scripts/merge-upstream-start.sh rename to .github/scripts/upstream-sync/merge-upstream-start.sh index f7b9da16f..fe679a7f5 100755 --- a/.github/scripts/merge-upstream-start.sh +++ b/.github/scripts/upstream-sync/merge-upstream-start.sh @@ -8,7 +8,7 @@ # 3. Clones the upstream copilot-sdk repo into a temp dir # 4. Reads .lastmerge and prints a short summary of new commits # -# Usage: ./.github/scripts/merge-upstream-start.sh +# Usage: ./.github/scripts/upstream-sync/merge-upstream-start.sh # Output: Exports UPSTREAM_DIR and LAST_MERGE_COMMIT to a # .merge-env file so other scripts can source it. # ────────────────────────────────────────────────────────────── @@ -83,6 +83,6 @@ EOF echo "▸ Env file written to $ENV_FILE (sourced by other merge scripts)." echo "" echo "✅ Ready. Next steps:" -echo " 1. Run ./.github/scripts/merge-upstream-diff.sh to see the full diff analysis." +echo " 1. Run ./.github/scripts/upstream-sync/merge-upstream-diff.sh to see the full diff analysis." echo " 2. Port changes to the Java SDK." -echo " 3. Run ./.github/scripts/merge-upstream-finish.sh when done." +echo " 3. Run ./.github/scripts/upstream-sync/merge-upstream-finish.sh when done." diff --git a/.github/workflows/copilot-issue.yml b/.github/workflows/copilot-issue.yml index 51ee70395..3a1d1898f 100644 --- a/.github/workflows/copilot-issue.yml +++ b/.github/workflows/copilot-issue.yml @@ -11,7 +11,7 @@ on: branches: [main] paths: - '.github/workflows/copilot-issue.yml' - - '.github/scripts/create-issue-assigned-to-copilot.ts' + - '.github/scripts/ci/create-issue-assigned-to-copilot.ts' jobs: create-issue: @@ -58,7 +58,7 @@ jobs: - name: Install dependencies run: npm install @octokit/rest tsx - working-directory: .github/scripts + working-directory: .github/scripts/ci - name: Create issue via GraphQL env: @@ -66,4 +66,4 @@ jobs: GITHUB_REPO_OWNER: ${{ github.repository_owner }} GITHUB_REPO_NAME: ${{ github.event.repository.name }} run: npx tsx create-issue-assigned-to-copilot.ts "${{ inputs.title || 'Investigation needed from Copilot Coding Agent' }}" - working-directory: .github/scripts + working-directory: .github/scripts/ci diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml index a73e523bf..7484be6f8 100644 --- a/.github/workflows/publish-maven.yml +++ b/.github/workflows/publish-maven.yml @@ -107,7 +107,7 @@ jobs: echo "Upstream SDK sync: ${UPSTREAM_SHORT} (${UPSTREAM_URL})" # Update CHANGELOG.md with release version and upstream sync hash - ./.github/scripts/update-changelog.sh "${VERSION}" "${UPSTREAM_HASH}" + ./.github/scripts/release/update-changelog.sh "${VERSION}" "${UPSTREAM_HASH}" # Update version in README.md sed -i "s|[0-9]*\.[0-9]*\.[0-9]*|${VERSION}|g" README.md From 69005ead5b696cbc9520896e4ca5cc26cbd1d0c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:49:16 +0000 Subject: [PATCH 027/147] Address code review feedback on workflow documentation Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .github/workflows/increase-test-coverage.md | 2 +- README.md | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/increase-test-coverage.md b/.github/workflows/increase-test-coverage.md index d3b021591..0a286030a 100644 --- a/.github/workflows/increase-test-coverage.md +++ b/.github/workflows/increase-test-coverage.md @@ -155,7 +155,7 @@ mvn jacoco:report - **Test Framework**: JUnit 5 - **Coverage Tool**: JaCoCo - **Code Formatter**: Spotless (Eclipse style, 4-space indent) -- **Current Coverage**: ~66% (aim for {{ inputs.min_coverage_threshold }}%+) +- **Current Coverage**: ~66% (aim for {{ inputs.min_coverage_threshold }}%+ overall) ## Key Packages to Focus On diff --git a/README.md b/README.md index 2935c8771..1704e88ad 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,10 @@ This SDK tracks the official [Copilot SDK](https://github.com/github/copilot-sdk The repository includes an [Agentic Workflow](https://github.com/github/gh-aw) that automatically identifies untested code and creates comprehensive tests to increase coverage. The workflow is triggered manually: **To run the workflow:** -1. Go to the [Actions tab](../../actions/workflows/increase-test-coverage.lock.yml) -2. Click "Run workflow" -3. Optionally specify: +1. Navigate to the repository on GitHub +2. Go to **Actions** → **Increase Test Coverage** workflow +3. Click "Run workflow" +4. Optionally specify: - `target_package` — Focus on a specific package (e.g., `com.github.copilot.sdk.json`) - `min_coverage_threshold` — Target coverage percentage (default: 80%) From 4f876d560d70b3bc184c0db35f690f45e91b4c41 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 15:49:30 -0500 Subject: [PATCH 028/147] Add agentic workflow daily-repo-status --- .gitattributes | 1 + .github/workflows/daily-repo-status.lock.yml | 1031 ++++++++++++++++++ .github/workflows/daily-repo-status.md | 50 + 3 files changed, 1082 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/daily-repo-status.lock.yml create mode 100644 .github/workflows/daily-repo-status.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..c1965c216 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/workflows/daily-repo-status.lock.yml b/.github/workflows/daily-repo-status.lock.yml new file mode 100644 index 000000000..aac922eaa --- /dev/null +++ b/.github/workflows/daily-repo-status.lock.yml @@ -0,0 +1,1031 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.42.17). DO NOT EDIT. +# +# To update this file, edit githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323 and run: +# gh aw compile +# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md +# +# This workflow creates daily repo status reports. It gathers recent repository +# activity (issues, PRs, discussions, releases, code changes) and generates +# engaging GitHub issues with productivity insights, community highlights, +# and project recommendations. +# +# Source: githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323 +# +# frontmatter-hash: 7936e337cf8a6040e3d48eb456ec675afa36d9ef413fef734ff36966c0d72be1 + +name: "Daily Repo Status" +"on": + schedule: + - cron: "13 19 * * *" + # Friendly format: daily (scattered) + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Daily Repo Status" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_WORKFLOW_FILE: "daily-repo-status.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + 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_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + model: ${{ steps.generate_aw_info.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Checkout .github and .agents folders + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + with: + sparse-checkout: | + .github + .agents + depth: 1 + persist-credentials: false + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.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('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.13.12 + - name: Determine automatic lockdown mode for GitHub MCP server + id: determine-automatic-lockdown + env: + TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + if: env.TOKEN_CHECK != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.13.12 ghcr.io/github/gh-aw-firewall/squid:0.13.12 ghcr.io/github/gh-aw-mcpg:v0.0.113 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p /opt/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' + {"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' + [ + { + "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[repo-status] \". Labels [report daily-status] will be automatically added.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "Detailed issue description in Markdown. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate.", + "type": "string" + }, + "labels": { + "description": "Labels to categorize the issue (e.g., 'bug', 'enhancement'). Labels must exist in the repository.", + "items": { + "type": "string" + }, + "type": "array" + }, + "parent": { + "description": "Parent issue number for creating sub-issues. This is the numeric ID from the GitHub URL (e.g., 42 in github.com/owner/repo/issues/42). Can also be a temporary_id (e.g., 'aw_abc123def456') from a previously created issue in the same workflow run.", + "type": [ + "number", + "string" + ] + }, + "temporary_id": { + "description": "Unique temporary identifier for referencing this issue before it's created. Format: 'aw_' followed by 12 hex characters (e.g., 'aw_abc123def456'). Use '#aw_ID' in body text to reference other issues by their temporary_id; these are replaced with actual issue numbers after creation.", + "type": "string" + }, + "title": { + "description": "Concise issue title summarizing the bug, feature, or task. The title appears as the main heading, so keep it brief and descriptive.", + "type": "string" + } + }, + "required": [ + "title", + "body" + ], + "type": "object" + }, + "name": "create_issue" + }, + { + "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "reason": { + "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", + "type": "string" + }, + "tool": { + "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", + "type": "string" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "name": "missing_tool" + }, + { + "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "name": "noop" + }, + { + "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "context": { + "description": "Additional context about the missing data or where it should come from (max 256 characters).", + "type": "string" + }, + "data_type": { + "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", + "type": "string" + }, + "reason": { + "description": "Explanation of why this data is needed to complete the task (max 256 characters).", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "name": "missing_data" + } + ] + EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' + { + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "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 + } + } + } + } + EOF + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + API_KEY="" + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + PORT=3001 + + # Register API key as secret to mask it from logs + echo "::add-mask::${API_KEY}" + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash /opt/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY="" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export DEBUG="*" + + # Register API key as secret to mask it from logs + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -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_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_LOCKDOWN -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 GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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 ghcr.io/github/gh-aw-mcpg:v0.0.113' + + mkdir -p /home/runner/.copilot + cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.30.3", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + MCPCONFIG_EOF + - name: Generate agentic run info + id: generate_aw_info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "copilot", + engine_name: "GitHub Copilot CLI", + model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", + version: "", + agent_version: "0.0.405", + cli_version: "v0.42.17", + workflow_name: "Daily Repo Status", + experimental: false, + supports_tools_allowlist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults"], + firewall_enabled: true, + awf_version: "v0.13.12", + awmg_version: "v0.0.113", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in PR + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/jobs + core.setOutput('model', awInfo.model); + - name: Generate workflow overview + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); + await generateWorkflowOverview(core); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" + + PROMPT_EOF + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + + GitHub API Access Instructions + + The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. + + + To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. + + Discover available tools from the safeoutputs MCP server. + + **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. + + **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. + + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + PROMPT_EOF + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + + PROMPT_EOF + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/workflows/daily-repo-status.md}} + PROMPT_EOF + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + 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 + } + }); + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.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 --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.13.12 --skip-pull \ + -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' \ + 2>&1 | tee /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: | + # Copy Copilot session state files to logs folder for artifact collection + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; then + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true + echo "Session state files copied successfully" + else + echo "No session-state directory found at $SESSION_STATE_DIR" + fi + - 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 /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + 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: Upload Safe Outputs + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: safe-output + path: ${{ env.GH_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output + id: collect_output + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.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" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Upload sanitized agent output + if: always() && env.GH_AW_AGENT_OUTPUT + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-output + path: ${{ env.GH_AW_AGENT_OUTPUT }} + if-no-files-found: warn + - name: Upload engine output files + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent_outputs + path: | + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + if-no-files-found: ignore + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + outputs: + 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 + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: 1 + GH_AW_WORKFLOW_NAME: "Daily Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/36514b53398b42745c0de75b3e7edeb839890323/workflows/daily-repo-status.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Daily Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/36514b53398b42745c0de75b3e7edeb839890323/workflows/daily-repo-status.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Daily Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/36514b53398b42745c0de75b3e7edeb839890323/workflows/daily-repo-status.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Handle No-Op Message + id: handle_noop_message + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Daily Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/36514b53398b42745c0de75b3e7edeb839890323/workflows/daily-repo-status.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_MESSAGE: ${{ steps.noop.outputs.noop_message }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Daily Repo Status" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/notify_comment_error.cjs'); + await main(); + + detection: + needs: agent + if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' + runs-on: ubuntu-latest + permissions: {} + timeout-minutes: 10 + outputs: + success: ${{ steps.parse_results.outputs.success }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Download agent artifacts + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-artifacts + path: /tmp/gh-aw/threat-detection/ + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/threat-detection/ + - name: Echo agent output types + env: + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + run: | + echo "Agent output-types: $AGENT_OUTPUT_TYPES" + - name: Setup threat detection + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + WORKFLOW_NAME: "Daily Repo Status" + WORKFLOW_DESCRIPTION: "This workflow creates daily repo status reports. It gathers recent repository\nactivity (issues, PRs, discussions, releases, code changes) and generates\nengaging GitHub issues with productivity insights, community highlights,\nand project recommendations." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool shell(cat) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(tail) + # --allow-tool shell(wc) + timeout-minutes: 20 + run: | + set -o pipefail + COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" + mkdir -p /tmp/ + mkdir -p /tmp/gh-aw/ + mkdir -p /tmp/gh-aw/agent/ + mkdir -p /tmp/gh-aw/sandbox/agent/logs/ + copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection results + id: parse_results + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection log + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: threat-detection.log + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + + safe_outputs: + needs: + - agent + - detection + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + timeout-minutes: 15 + env: + GH_AW_WORKFLOW_ID: "daily-repo-status" + GH_AW_WORKFLOW_NAME: "Daily Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/36514b53398b42745c0de75b3e7edeb839890323/workflows/daily-repo-status.md" + outputs: + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + 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 }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"labels\":[\"report\",\"daily-status\"],\"max\":1,\"title_prefix\":\"[repo-status] \"},\"missing_data\":{},\"missing_tool\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + diff --git a/.github/workflows/daily-repo-status.md b/.github/workflows/daily-repo-status.md new file mode 100644 index 000000000..d6d6b2931 --- /dev/null +++ b/.github/workflows/daily-repo-status.md @@ -0,0 +1,50 @@ +--- +description: | + This workflow creates daily repo status reports. It gathers recent repository + activity (issues, PRs, discussions, releases, code changes) and generates + engaging GitHub issues with productivity insights, community highlights, + and project recommendations. + +on: + schedule: daily + workflow_dispatch: + +permissions: + contents: read + issues: read + pull-requests: read + +network: defaults + +tools: + github: + +safe-outputs: + create-issue: + title-prefix: "[repo-status] " + labels: [report, daily-status] +source: githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323 +--- + +# Daily Repo Status + +Create an upbeat daily status report for the repo as a GitHub issue. + +## What to include + +- Recent repository activity (issues, PRs, discussions, releases, code changes) +- Progress tracking, goal reminders and highlights +- Project status and recommendations +- Actionable next steps for maintainers + +## Style + +- Be positive, encouraging, and helpful 🌟 +- Use emojis moderately for engagement +- Keep it concise - adjust length based on actual activity + +## Process + +1. Gather recent activity from the repository +2. Study the repository, its issues and its pull requests +3. Create a new GitHub issue with your findings and insights From 9cbe4373ed3806e14d542002000d65532aee616c Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 15:51:34 -0500 Subject: [PATCH 029/147] Extract shell scripts from commit-as-pull-request prompt (#102) Move git operations into reusable scripts under .github/scripts/ci/: - parse-repo-info.sh: extract owner/repo from remote URL - commit-and-push.sh: verify, format, branch, commit, push - sync-after-merge.sh: sync main and delete branch Simplify the prompt from 10 steps to 6 by delegating to scripts. --- .../prompts/commit-as-pull-request.prompt.md | 71 +++++++---------- .github/scripts/ci/commit-and-push.sh | 77 +++++++++++++++++++ .github/scripts/ci/parse-repo-info.sh | 29 +++++++ .github/scripts/ci/sync-after-merge.sh | 29 +++++++ 4 files changed, 161 insertions(+), 45 deletions(-) create mode 100755 .github/scripts/ci/commit-and-push.sh create mode 100755 .github/scripts/ci/parse-repo-info.sh create mode 100755 .github/scripts/ci/sync-after-merge.sh diff --git a/.github/prompts/commit-as-pull-request.prompt.md b/.github/prompts/commit-as-pull-request.prompt.md index 684e3ee9a..75368faed 100644 --- a/.github/prompts/commit-as-pull-request.prompt.md +++ b/.github/prompts/commit-as-pull-request.prompt.md @@ -8,25 +8,28 @@ You are an automated assistant that takes the current uncommitted changes in the - There must be uncommitted changes (staged or unstaged) in the working tree. - The GitHub MCP tools must be available for creating and merging pull requests. -## Workflow +## Helper Scripts -Execute the following steps **in order**. Stop immediately if any step fails. +The following scripts in `.github/scripts/ci/` automate the git operations for this workflow: -### Step 1: Verify there are changes to commit +| Script | Purpose | +|--------|---------| +| `parse-repo-info.sh` | Extracts `REPO_OWNER` and `REPO_NAME` from the git remote URL | +| `commit-and-push.sh` | Verifies changes, runs formatter, creates branch, commits, and pushes | +| `sync-after-merge.sh` | Syncs local `main` and deletes the feature branch | -Run `git status --porcelain` to confirm there are uncommitted changes. If the output is empty, inform the user there is nothing to commit and stop. +## Workflow -### Step 2: Determine the repository owner and name +Execute the following steps **in order**. Stop immediately if any step fails. -Read the repository remote URL to extract the GitHub `owner` and `repo`: +### Step 1: Determine the repository owner and name ```bash -git remote get-url origin +eval "$(.github/scripts/ci/parse-repo-info.sh)" +# Sets: REPO_OWNER, REPO_NAME ``` -Parse the owner and repo from the URL (handles both HTTPS and SSH formats). - -### Step 3: Auto-detect branch name and commit message +### Step 2: Auto-detect branch name and commit message Analyze the changed files using `git diff` (and `git diff --cached` for staged changes) to understand what was modified. Generate: @@ -37,69 +40,47 @@ Analyze the changed files using `git diff` (and `git diff --cached` for staged c If the user has provided an explicit branch name or commit message, use those instead. -### Step 4: Run code formatter - -If the project has a formatter configured, run it before committing: - -```bash -mvn spotless:apply -``` - -Only run this if a `pom.xml` exists with Spotless configured. Skip for non-Maven projects. - -### Step 5: Create branch, stage, and commit - -```bash -git checkout -b -git add -A -git commit -m "" -``` +### Step 3: Commit and push -### Step 6: Push the branch +Runs the formatter (if applicable), creates the branch, stages all changes, commits, and pushes: ```bash -git push -u origin +.github/scripts/ci/commit-and-push.sh "" "" +# Outputs: BRANCH_NAME (may differ if suffix was appended) ``` -If the push reports "Everything up-to-date", verify with `git log --oneline -1` that the commit exists, then retry with `git push -u origin 2>&1`. +Pass `--skip-format` as a third argument to skip `mvn spotless:apply` (e.g., when only non-Java files changed). -### Step 7: Create a pull request +### Step 4: Create a pull request Use the GitHub MCP `create_pull_request` tool with: -- **owner** and **repo**: from Step 2 +- **owner** and **repo**: from Step 1 - **title**: the first line of the commit message -- **head**: the branch name +- **head**: the branch name from Step 3 - **base**: `main` (or the repository's default branch) - **body**: A well-structured PR description including: - **Summary**: What the change does and why - **Changes**: Bullet list of files/areas modified - **Testing**: How the changes were verified -### Step 8: Merge the pull request +### Step 5: Merge the pull request Use the GitHub MCP `merge_pull_request` tool with: - **merge_method**: `squash` - **commit_title**: ` (#)` -### Step 9: Sync local main - -```bash -git checkout main -git pull -``` - -### Step 10: Clean up the local branch (optional) +### Step 6: Sync and clean up ```bash -git branch -d +.github/scripts/ci/sync-after-merge.sh "" ``` ## Error Handling -- If the branch name already exists, append a numeric suffix (e.g., `fix/my-change-2`). -- If the push fails due to authentication, inform the user and stop. +- Branch name collisions are handled automatically by `commit-and-push.sh` (appends a numeric suffix). +- If the push fails due to authentication, the script exits with code 2 — inform the user and stop. - If the PR creation fails, provide the error and stop. - If the merge fails (e.g., merge conflicts, required checks), inform the user and leave the PR open. diff --git a/.github/scripts/ci/commit-and-push.sh b/.github/scripts/ci/commit-and-push.sh new file mode 100755 index 000000000..65fcac51b --- /dev/null +++ b/.github/scripts/ci/commit-and-push.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Verify changes, optionally run the code formatter, create a branch, +# stage all changes, commit, and push to origin. +# +# Usage: +# ./commit-and-push.sh [--skip-format] +# +# Arguments: +# branch-name The branch to create (e.g., fix/my-change) +# commit-message The commit message (may include newlines via $'...' syntax) +# --skip-format Skip running mvn spotless:apply +# +# Exit codes: +# 0 Success +# 1 No changes to commit / general error +# 2 Push failed + +set -euo pipefail + +if [[ $# -lt 2 ]]; then + echo "Usage: $0 [--skip-format]" >&2 + exit 1 +fi + +branch_name="$1" +commit_message="$2" +skip_format="${3:-}" + +# -- Step 1: Verify there are changes to commit --------------------------------- +if [[ -z "$(git status --porcelain)" ]]; then + echo "ERROR: No uncommitted changes found. Nothing to commit." >&2 + exit 1 +fi + +echo "✓ Uncommitted changes detected." + +# -- Step 4: Run code formatter (if applicable) ---------------------------------- +if [[ "$skip_format" != "--skip-format" ]] && [[ -f pom.xml ]]; then + if grep -q 'spotless-maven-plugin' pom.xml 2>/dev/null; then + echo "Running Spotless formatter..." + mvn -q spotless:apply + echo "✓ Spotless formatting applied." + fi +fi + +# -- Step 5: Create branch, stage, and commit ------------------------------------ +# If the branch already exists locally, append a numeric suffix +actual_branch="$branch_name" +suffix=2 +while git show-ref --verify --quiet "refs/heads/$actual_branch" 2>/dev/null; do + actual_branch="${branch_name}-${suffix}" + suffix=$((suffix + 1)) +done + +git checkout -b "$actual_branch" +git add -A +git commit -m "$commit_message" + +echo "✓ Committed on branch '$actual_branch'." + +# -- Step 6: Push the branch ----------------------------------------------------- +if ! git push -u origin "$actual_branch" 2>&1; then + echo "ERROR: Push failed." >&2 + exit 2 +fi + +# Verify the push actually transferred the commit +remote_sha=$(git ls-remote origin "refs/heads/$actual_branch" | awk '{print $1}') +local_sha=$(git rev-parse HEAD) + +if [[ "$remote_sha" != "$local_sha" ]]; then + echo "WARNING: Remote SHA does not match local. Retrying push..." >&2 + git push -u origin "$actual_branch" 2>&1 || { echo "ERROR: Retry push failed." >&2; exit 2; } +fi + +echo "✓ Pushed to origin/$actual_branch." +echo "BRANCH_NAME=$actual_branch" diff --git a/.github/scripts/ci/parse-repo-info.sh b/.github/scripts/ci/parse-repo-info.sh new file mode 100755 index 000000000..3d89115c7 --- /dev/null +++ b/.github/scripts/ci/parse-repo-info.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Parse the GitHub owner and repository name from the git remote URL. +# Outputs two lines: owner on the first, repo on the second. +# Handles both HTTPS and SSH remote URL formats. +# +# Usage: +# eval "$(./parse-repo-info.sh)" +# echo "Owner: $REPO_OWNER Repo: $REPO_NAME" + +set -euo pipefail + +remote_url=$(git remote get-url origin 2>/dev/null) || { + echo "ERROR: No git remote named 'origin' found." >&2 + exit 1 +} + +# Strip trailing .git if present +remote_url="${remote_url%.git}" + +if [[ "$remote_url" =~ github\.com[:/]([^/]+)/([^/]+)$ ]]; then + owner="${BASH_REMATCH[1]}" + repo="${BASH_REMATCH[2]}" +else + echo "ERROR: Could not parse owner/repo from remote URL: $remote_url" >&2 + exit 1 +fi + +echo "REPO_OWNER=$owner" +echo "REPO_NAME=$repo" diff --git a/.github/scripts/ci/sync-after-merge.sh b/.github/scripts/ci/sync-after-merge.sh new file mode 100755 index 000000000..94ec9cf8e --- /dev/null +++ b/.github/scripts/ci/sync-after-merge.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# After a PR has been merged, sync the local main branch and +# optionally delete the local feature branch. +# +# Usage: +# ./sync-after-merge.sh [branch-name] +# +# Arguments: +# branch-name The local branch to delete (optional). Skipped if omitted. + +set -euo pipefail + +branch_name="${1:-}" + +# -- Step 9: Sync local main ---------------------------------------------------- +git checkout main +git pull + +echo "✓ Local main is up to date." + +# -- Step 10: Clean up the local branch ------------------------------------------ +if [[ -n "$branch_name" ]]; then + if git show-ref --verify --quiet "refs/heads/$branch_name" 2>/dev/null; then + git branch -d "$branch_name" 2>/dev/null || git branch -D "$branch_name" + echo "✓ Deleted local branch '$branch_name'." + else + echo "Branch '$branch_name' does not exist locally (already cleaned up)." + fi +fi From 987d6052e1d426f9111f6360a383cec68341ac53 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 16:12:34 -0500 Subject: [PATCH 030/147] Add agentic workflow for upstream sync (#104) * backup * Add agentic workflow for upstream sync Create a gh-aw agentic workflow that replaces the existing Copilot coding agent issue-based approach with a native GitHub Agentic Workflow for weekly upstream SDK syncing. The workflow checks for new upstream commits, analyzes diffs, ports changes to the Java SDK, runs tests, and creates a PR. --- .github/aw/actions-lock.json | 19 + .github/workflows/upstream-sync.lock.yml | 1066 +++++++++++++++++ .github/workflows/upstream-sync.md | 234 ++++ .github/workflows/weekly-upstream-sync.backup | 178 +++ 4 files changed, 1497 insertions(+) create mode 100644 .github/aw/actions-lock.json create mode 100644 .github/workflows/upstream-sync.lock.yml create mode 100644 .github/workflows/upstream-sync.md create mode 100644 .github/workflows/weekly-upstream-sync.backup diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json new file mode 100644 index 000000000..36881a0f5 --- /dev/null +++ b/.github/aw/actions-lock.json @@ -0,0 +1,19 @@ +{ + "entries": { + "actions/github-script@v8": { + "repo": "actions/github-script", + "version": "v8", + "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" + }, + "actions/setup-java@v4": { + "repo": "actions/setup-java", + "version": "v4", + "sha": "c1e323688fd81a25caa38c78aa6df2d33d3e20d9" + }, + "actions/setup-node@v4": { + "repo": "actions/setup-node", + "version": "v4", + "sha": "49933ea5288caeca8642d1e84afbd3f7d6820020" + } + } +} diff --git a/.github/workflows/upstream-sync.lock.yml b/.github/workflows/upstream-sync.lock.yml new file mode 100644 index 000000000..b8005dbd6 --- /dev/null +++ b/.github/workflows/upstream-sync.lock.yml @@ -0,0 +1,1066 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.42.17). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md +# +# Weekly upstream sync workflow. Checks for new commits in the official +# Copilot SDK (github/copilot-sdk), analyzes changes, ports them to the +# Java SDK, runs tests, and creates a pull request with the ported changes. +# +# frontmatter-hash: e9e6426a74c083476677690bbc5ce163344277d23cb3d49207075ea1c49c6757 + +name: "Upstream Sync Agent" +"on": + schedule: + - cron: "47 13 * * 4" + # Friendly format: weekly (scattered) + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Upstream Sync Agent" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_WORKFLOW_FILE: "upstream-sync.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + issues: read + pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + 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_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + model: ${{ steps.generate_aw_info.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Checkout .github and .agents folders + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + with: + sparse-checkout: | + .github + .agents + depth: 1 + persist-credentials: false + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Set up Java 17 + uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4 + with: + distribution: temurin + java-version: "17" + - name: Set up Node.js 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22" + + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.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('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.13.12 + - name: Determine automatic lockdown mode for GitHub MCP server + id: determine-automatic-lockdown + env: + TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + if: env.TOKEN_CHECK != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.13.12 ghcr.io/github/gh-aw-firewall/squid:0.13.12 ghcr.io/github/gh-aw-mcpg:v0.0.113 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p /opt/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' + {"create_pull_request":{},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' + [ + { + "description": "Create a new GitHub pull request to propose code changes. Use this after making file edits to submit them for review and merging. The PR will be created from the current branch with your committed changes. For code review comments on an existing PR, use create_pull_request_review_comment instead. CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[upstream-sync] \". Labels [upstream-sync] will be automatically added. PRs will be created as drafts.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "Detailed PR description in Markdown. Include what changes were made, why, testing notes, and any breaking changes. Do NOT repeat the title as a heading.", + "type": "string" + }, + "branch": { + "description": "Source branch name containing the changes. If omitted, uses the current working branch.", + "type": "string" + }, + "labels": { + "description": "Labels to categorize the PR (e.g., 'enhancement', 'bugfix'). Labels must exist in the repository.", + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "description": "Concise PR title describing the changes. Follow repository conventions (e.g., conventional commits). The title appears as the main heading.", + "type": "string" + } + }, + "required": [ + "title", + "body" + ], + "type": "object" + }, + "name": "create_pull_request" + }, + { + "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "reason": { + "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", + "type": "string" + }, + "tool": { + "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", + "type": "string" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "name": "missing_tool" + }, + { + "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "name": "noop" + }, + { + "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "context": { + "description": "Additional context about the missing data or where it should come from (max 256 characters).", + "type": "string" + }, + "data_type": { + "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", + "type": "string" + }, + "reason": { + "description": "Explanation of why this data is needed to complete the task (max 256 characters).", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "name": "missing_data" + } + ] + EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' + { + "create_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "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 + } + } + } + } + EOF + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + API_KEY="" + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + PORT=3001 + + # Register API key as secret to mask it from logs + echo "::add-mask::${API_KEY}" + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash /opt/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY="" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export DEBUG="*" + + # Register API key as secret to mask it from logs + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -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_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_LOCKDOWN -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 GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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 ghcr.io/github/gh-aw-mcpg:v0.0.113' + + mkdir -p /home/runner/.copilot + cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.30.3", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + MCPCONFIG_EOF + - name: Generate agentic run info + id: generate_aw_info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "copilot", + engine_name: "GitHub Copilot CLI", + model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", + version: "", + agent_version: "0.0.405", + cli_version: "v0.42.17", + workflow_name: "Upstream Sync Agent", + experimental: false, + supports_tools_allowlist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults","java","node"], + firewall_enabled: true, + awf_version: "v0.13.12", + awmg_version: "v0.0.113", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in PR + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/jobs + core.setOutput('model', awInfo.model); + - name: Generate workflow overview + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); + await generateWorkflowOverview(core); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" + + PROMPT_EOF + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + + GitHub API Access Instructions + + The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. + + + To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. + + Discover available tools from the safeoutputs MCP server. + + **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. + + **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. + + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + PROMPT_EOF + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + + PROMPT_EOF + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/workflows/upstream-sync.md}} + PROMPT_EOF + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + 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 + } + }); + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.jsr.io,adoptium.net,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.azul.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,dlcdn.apache.org,download.eclipse.org,download.java.net,download.oracle.com,get.pnpm.io,github.com,gradle.org,host.docker.internal,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven.apache.org,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,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,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,s.symcb.com,s.symcd.com,security.ubuntu.com,services.gradle.org,skimdb.npmjs.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.java.com,www.npmjs.com,www.npmjs.org,yarnpkg.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.13.12 --skip-pull \ + -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' \ + 2>&1 | tee /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: | + # Copy Copilot session state files to logs folder for artifact collection + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; then + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true + echo "Session state files copied successfully" + else + echo "No session-state directory found at $SESSION_STATE_DIR" + fi + - 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 /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + 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: Upload Safe Outputs + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: safe-output + path: ${{ env.GH_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output + id: collect_output + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "*.jsr.io,adoptium.net,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.azul.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,dlcdn.apache.org,download.eclipse.org,download.java.net,download.oracle.com,get.pnpm.io,github.com,gradle.org,host.docker.internal,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven.apache.org,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,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,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,s.symcb.com,s.symcd.com,security.ubuntu.com,services.gradle.org,skimdb.npmjs.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.java.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Upload sanitized agent output + if: always() && env.GH_AW_AGENT_OUTPUT + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-output + path: ${{ env.GH_AW_AGENT_OUTPUT }} + if-no-files-found: warn + - name: Upload engine output files + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent_outputs + path: | + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + if-no-files-found: ignore + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/aw.patch + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + outputs: + 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 + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: 1 + GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Handle No-Op Message + id: handle_noop_message + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Handle Create Pull Request Error + id: handle_create_pr_error + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_create_pr_error.cjs'); + await main(); + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/notify_comment_error.cjs'); + await main(); + + detection: + needs: agent + if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' + runs-on: ubuntu-latest + permissions: {} + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + timeout-minutes: 10 + outputs: + success: ${{ steps.parse_results.outputs.success }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Download agent artifacts + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-artifacts + path: /tmp/gh-aw/threat-detection/ + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/threat-detection/ + - name: Echo agent output types + env: + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + run: | + echo "Agent output-types: $AGENT_OUTPUT_TYPES" + - name: Setup threat detection + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + WORKFLOW_NAME: "Upstream Sync Agent" + WORKFLOW_DESCRIPTION: "Weekly upstream sync workflow. Checks for new commits in the official\nCopilot SDK (github/copilot-sdk), analyzes changes, ports them to the\nJava SDK, runs tests, and creates a pull request with the ported changes." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool shell(cat) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(tail) + # --allow-tool shell(wc) + timeout-minutes: 20 + run: | + set -o pipefail + COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" + mkdir -p /tmp/ + mkdir -p /tmp/gh-aw/ + mkdir -p /tmp/gh-aw/agent/ + mkdir -p /tmp/gh-aw/sandbox/agent/logs/ + copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection results + id: parse_results + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection log + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: threat-detection.log + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + + safe_outputs: + needs: + - activation + - agent + - detection + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_ENGINE_ID: "copilot" + GH_AW_WORKFLOW_ID: "upstream-sync" + GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" + outputs: + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + 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 }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-artifacts + path: /tmp/gh-aw/ + - name: Checkout repository + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + with: + token: ${{ github.token }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":true,\"labels\":[\"upstream-sync\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[upstream-sync] \"},\"missing_data\":{},\"missing_tool\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + diff --git a/.github/workflows/upstream-sync.md b/.github/workflows/upstream-sync.md new file mode 100644 index 000000000..1551132f1 --- /dev/null +++ b/.github/workflows/upstream-sync.md @@ -0,0 +1,234 @@ +--- +description: | + Weekly upstream sync workflow. Checks for new commits in the official + Copilot SDK (github/copilot-sdk), analyzes changes, ports them to the + Java SDK, runs tests, and creates a pull request with the ported changes. + +on: + schedule: weekly + workflow_dispatch: + +permissions: + contents: read + actions: read + issues: read + pull-requests: read + +network: + allowed: + - defaults + - java + - node + +steps: + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + - name: Set up Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: '22' + +tools: + github: + toolsets: [default] + +safe-outputs: + create-pull-request: + title-prefix: "[upstream-sync] " + labels: [upstream-sync] + draft: true + noop: +--- + +# Upstream Sync Agent + +You are an expert Java developer. Your job is to check the official Copilot SDK ([github/copilot-sdk](https://github.com/github/copilot-sdk)) for new changes and port them to this Java SDK. + +## ⚠️ CRITICAL: Java SDK Design Takes Priority + +The current design and architecture of the Java SDK is the priority. When porting: + +1. **Adapt, don't copy** — Translate features to fit Java patterns, naming conventions, and architecture +2. **Preserve Java idioms** — The SDK should feel natural to Java developers, not like a C# port +3. **Maintain consistency** — New code must match the existing codebase style +4. **Evaluate before porting** — Not every upstream change applies; some may conflict with Java SDK design + +Before making any changes, **read and understand the existing Java SDK implementation**. + +## Workflow + +### Phase 1: Check for upstream changes + +Run this to detect new commits: + +```bash +LAST_MERGE=$(cat .lastmerge) +echo "Last merged commit: $LAST_MERGE" + +git clone --depth=200 https://github.com/github/copilot-sdk.git /tmp/upstream-sdk +cd /tmp/upstream-sdk + +UPSTREAM_HEAD=$(git rev-parse HEAD) +echo "Upstream HEAD: $UPSTREAM_HEAD" + +if [ "$LAST_MERGE" = "$UPSTREAM_HEAD" ]; then + echo "NO_CHANGES=true" +else + COMMIT_COUNT=$(git rev-list --count "$LAST_MERGE".."$UPSTREAM_HEAD") + echo "Found $COMMIT_COUNT new upstream commits" + git log --oneline "$LAST_MERGE".."$UPSTREAM_HEAD" +fi +``` + +**If there are NO new changes**: Call the `noop` safe output with a message like "No new upstream changes detected. The Java SDK is up to date with commit ``." and stop. + +### Phase 2: Analyze changes + +Examine the upstream diff grouped by area: + +```bash +cd /tmp/upstream-sdk +LAST_MERGE=$(cat $GITHUB_WORKSPACE/.lastmerge) + +echo "=== .NET Source (primary reference) ===" +git diff --stat "$LAST_MERGE"..HEAD -- dotnet/src/ + +echo "=== .NET Tests ===" +git diff --stat "$LAST_MERGE"..HEAD -- dotnet/test/ + +echo "=== Test Snapshots ===" +git diff --stat "$LAST_MERGE"..HEAD -- test/snapshots/ + +echo "=== Documentation ===" +git diff --stat "$LAST_MERGE"..HEAD -- docs/ + +echo "=== Protocol / Config ===" +git diff --stat "$LAST_MERGE"..HEAD -- sdk-protocol-version.json +``` + +For each area with changes, read the full diffs: + +```bash +git diff "$LAST_MERGE"..HEAD -- dotnet/src/ +git diff "$LAST_MERGE"..HEAD -- dotnet/test/ +git diff "$LAST_MERGE"..HEAD -- docs/ +``` + +### Phase 3: Plan the port + +For each upstream change, determine: + +- **New Features**: New methods, classes, or capabilities +- **Bug Fixes**: Corrections to existing functionality +- **API Changes**: Changes to public interfaces +- **Protocol Updates**: JSON-RPC message type changes +- **Test Updates**: New or modified test cases + +Use this mapping to locate the Java equivalents: + +| Upstream (.NET) | Java SDK Equivalent | +|---|---| +| `dotnet/src/Client.cs` | `src/main/java/com/github/copilot/sdk/CopilotClient.java` | +| `dotnet/src/Session.cs` | `src/main/java/com/github/copilot/sdk/CopilotSession.java` | +| `dotnet/src/Types.cs` | `src/main/java/com/github/copilot/sdk/types/*.java` | +| `dotnet/src/Generated/*.cs` | `src/main/java/com/github/copilot/sdk/types/*.java` | +| `dotnet/test/*.cs` | `src/test/java/com/github/copilot/sdk/*Test.java` | +| `docs/getting-started.md` | `README.md` and `src/site/markdown/*.md` | + +### Phase 4: Port changes + +Read the existing Java implementation before modifying anything. Apply these conventions: + +**Type mappings:** +- `string` → `String`, `Task` → `CompletableFuture`, `JsonElement` → `JsonNode` (Jackson) +- C# PascalCase → Java camelCase for methods/variables +- C# `async/await` → `CompletableFuture` +- C# properties → Java getters/setters or fluent setters +- C# nullable `?` → `@Nullable` or `Optional` + +**Code style:** +- 4-space indentation (enforced by Spotless with Eclipse formatter) +- Fluent setter pattern for configuration classes +- Jackson for JSON serialization (`ObjectMapper`, `@JsonProperty`) +- `@JsonInclude(JsonInclude.Include.NON_NULL)` on DTOs +- Public APIs require Javadoc (except `json` and `events` packages) + +**Commit incrementally** as you work: +```bash +git add +git commit -m "Port from upstream" +``` + +### Phase 5: Port tests + +For each new or modified test in `dotnet/test/`: + +1. Create or update the corresponding Java test class in `src/test/java/com/github/copilot/sdk/` +2. Follow existing test patterns (look at `PermissionsTest.java`, `HooksTest.java`) +3. Use `E2ETestContext` for tests requiring the test harness +4. If the test harness doesn't support new RPC methods yet, mark with `@Disabled("Requires test harness update")` + +### Phase 6: Update CLI version in README + +Check the CLI version: +```bash +copilot --version 2>/dev/null || echo "CLI not available in CI" +``` + +If the CLI version changed, update requirements in both `README.md` and `src/site/markdown/index.md`. + +### Phase 7: Update documentation + +For each new feature ported: +- **`README.md`** — Update if there are user-facing changes +- **`src/site/markdown/documentation.md`** — New basic usage patterns +- **`src/site/markdown/advanced.md`** — New advanced features +- **Javadoc** — All new/changed public APIs +- **`src/site/site.xml`** — Update if new pages added + +### Phase 8: Format and test + +```bash +mvn spotless:apply +mvn clean verify +``` + +If tests fail: +1. Read the error output carefully +2. Fix the issue in the Java code +3. Re-run `mvn clean verify` +4. Repeat until all tests pass + +### Phase 9: Finalize + +Update `.lastmerge` with the upstream HEAD commit: + +```bash +cd /tmp/upstream-sdk +UPSTREAM_HEAD=$(git rev-parse HEAD) +cd $GITHUB_WORKSPACE +echo "$UPSTREAM_HEAD" > .lastmerge +git add .lastmerge +git commit -m "Update .lastmerge to $(cat .lastmerge | cut -c1-12)" +``` + +### Phase 10: Create Pull Request + +Create a pull request with your changes using the `create-pull-request` safe output. The PR body should include: + +- **Title**: `Merge upstream SDK changes (YYYY-MM-DD)` +- **Summary**: Number of upstream commits analyzed with commit range +- **Changes ported**: Table of commit hash + description +- **Not ported**: List with reasons (if any) +- **Verification**: Test count, build status + +## Guidelines + +- **SECURITY**: Never commit secrets, tokens, or credentials +- Use `try-with-resources` for streams and readers +- Use `StandardCharsets.UTF_8` when creating InputStreamReader/OutputStreamWriter +- Prefer existing Java SDK abstractions over upstream .NET patterns +- When in doubt, match the style of surrounding Java code diff --git a/.github/workflows/weekly-upstream-sync.backup b/.github/workflows/weekly-upstream-sync.backup new file mode 100644 index 000000000..84ef95fc5 --- /dev/null +++ b/.github/workflows/weekly-upstream-sync.backup @@ -0,0 +1,178 @@ +name: "Weekly Upstream Sync" + +on: + schedule: + # Every Monday at 10:00 UTC (5:00 AM EST / 6:00 AM EDT) + - cron: '0 10 * * 1' + workflow_dispatch: + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + + check-and-sync: + name: "Check upstream & trigger Copilot merge" + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Check for upstream changes + id: check + run: | + LAST_MERGE=$(cat .lastmerge) + echo "Last merged commit: $LAST_MERGE" + + git clone --quiet https://github.com/github/copilot-sdk.git /tmp/upstream + cd /tmp/upstream + + UPSTREAM_HEAD=$(git rev-parse HEAD) + echo "Upstream HEAD: $UPSTREAM_HEAD" + + if [ "$LAST_MERGE" = "$UPSTREAM_HEAD" ]; then + echo "No new upstream changes since last merge." + echo "has_changes=false" >> "$GITHUB_OUTPUT" + else + COMMIT_COUNT=$(git rev-list --count "$LAST_MERGE".."$UPSTREAM_HEAD") + echo "Found $COMMIT_COUNT new upstream commits." + echo "has_changes=true" >> "$GITHUB_OUTPUT" + echo "commit_count=$COMMIT_COUNT" >> "$GITHUB_OUTPUT" + echo "upstream_head=$UPSTREAM_HEAD" >> "$GITHUB_OUTPUT" + echo "last_merge=$LAST_MERGE" >> "$GITHUB_OUTPUT" + + # Generate a short summary of changes + SUMMARY=$(git log --oneline "$LAST_MERGE".."$UPSTREAM_HEAD" | head -20) + { + echo "summary<> "$GITHUB_OUTPUT" + fi + + - name: Close previous upstream-sync issues + if: steps.check.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} + run: | + # Find all open issues with the upstream-sync label + OPEN_ISSUES=$(gh issue list \ + --repo "${{ github.repository }}" \ + --label "upstream-sync" \ + --state open \ + --json number \ + --jq '.[].number') + + for ISSUE_NUM in $OPEN_ISSUES; do + echo "Closing superseded issue #${ISSUE_NUM}" + gh issue comment "$ISSUE_NUM" \ + --repo "${{ github.repository }}" \ + --body "Superseded by a newer upstream sync issue. Closing this one." + gh issue close "$ISSUE_NUM" \ + --repo "${{ github.repository }}" \ + --reason "not planned" + done + + - name: Close stale upstream-sync issues (no changes) + if: steps.check.outputs.has_changes == 'false' + env: + GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} + run: | + OPEN_ISSUES=$(gh issue list \ + --repo "${{ github.repository }}" \ + --label "upstream-sync" \ + --state open \ + --json number \ + --jq '.[].number') + + for ISSUE_NUM in $OPEN_ISSUES; do + echo "Closing stale issue #${ISSUE_NUM} — upstream is up to date" + gh issue comment "$ISSUE_NUM" \ + --repo "${{ github.repository }}" \ + --body "No new upstream changes detected. The Java SDK is up to date. Closing." + gh issue close "$ISSUE_NUM" \ + --repo "${{ github.repository }}" \ + --reason "completed" + done + + - name: Create issue and assign to Copilot + id: create-issue + if: steps.check.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} + run: | + COMMIT_COUNT="${{ steps.check.outputs.commit_count }}" + LAST_MERGE="${{ steps.check.outputs.last_merge }}" + UPSTREAM_HEAD="${{ steps.check.outputs.upstream_head }}" + SUMMARY="${{ steps.check.outputs.summary }}" + DATE=$(date -u +"%Y-%m-%d") + REPO="${{ github.repository }}" + + BODY="## Automated Upstream Sync + + There are **${COMMIT_COUNT}** new commits in the [official Copilot SDK](https://github.com/github/copilot-sdk) since the last merge. + + - **Last merged commit:** [\`${LAST_MERGE}\`](https://github.com/github/copilot-sdk/commit/${LAST_MERGE}) + - **Upstream HEAD:** [\`${UPSTREAM_HEAD}\`](https://github.com/github/copilot-sdk/commit/${UPSTREAM_HEAD}) + + ### Recent upstream commits + + \`\`\` + ${SUMMARY} + \`\`\` + + ### Instructions + + Follow the [agentic-merge-upstream](.github/prompts/agentic-merge-upstream.prompt.md) prompt to port these changes to the Java SDK." + + # Create the issue and assign to Copilot coding agent + ISSUE_URL=$(gh issue create \ + --repo "$REPO" \ + --title "Upstream sync: ${COMMIT_COUNT} new commits (${DATE})" \ + --body "$BODY" \ + --label "upstream-sync" \ + --assignee "copilot-swe-agent") + + echo "issue_url=$ISSUE_URL" >> "$GITHUB_OUTPUT" + echo "✅ Issue created and assigned to Copilot coding agent: $ISSUE_URL" + + - name: Summary + if: always() + run: | + HAS_CHANGES="${{ steps.check.outputs.has_changes }}" + COMMIT_COUNT="${{ steps.check.outputs.commit_count }}" + LAST_MERGE="${{ steps.check.outputs.last_merge }}" + UPSTREAM_HEAD="${{ steps.check.outputs.upstream_head }}" + ISSUE_URL="${{ steps.create-issue.outputs.issue_url }}" + + { + echo "## Weekly Upstream Sync" + echo "" + if [ "$HAS_CHANGES" = "true" ]; then + echo "### ✅ New upstream changes detected" + echo "" + echo "| | |" + echo "|---|---|" + echo "| **New commits** | ${COMMIT_COUNT} |" + echo "| **Last merged** | \`${LAST_MERGE:0:12}\` |" + echo "| **Upstream HEAD** | \`${UPSTREAM_HEAD:0:12}\` |" + echo "" + echo "An issue has been created and assigned to the Copilot coding agent: " + echo " -> ${ISSUE_URL}" + echo "" + echo "### Recent upstream commits" + echo "" + echo '```' + echo "${{ steps.check.outputs.summary }}" + echo '```' + else + echo "### ⏭️ No changes" + echo "" + echo "The Java SDK is already up to date with the upstream Copilot SDK." + echo "" + echo "**Last merged commit:** \`${LAST_MERGE:0:12}\`" + fi + } >> "$GITHUB_STEP_SUMMARY" From a99223053565157f2830ff6a5c6f36b44b375c20 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 16:46:52 -0500 Subject: [PATCH 031/147] Add custom agent for GitHub Agentic Workflows --- .github/agents/ghaw.md | 201 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 .github/agents/ghaw.md diff --git a/.github/agents/ghaw.md b/.github/agents/ghaw.md new file mode 100644 index 000000000..1f7d83066 --- /dev/null +++ b/.github/agents/ghaw.md @@ -0,0 +1,201 @@ +# Creating Agentic Workflows and Other Actions + +This prompt guides you, a coding agent, to create, debug, update or do other actions related to **GitHub Agentic Workflows (gh-aw)** in a repository. + +## Step 1: Install GitHub Agentic Workflows CLI Extension + +Check if `gh aw` is installed by running + +```bash +gh aw version +``` + +If it is installed, run: + +```bash +gh extension upgrade aw +``` + +to upgrade to latest. If it is not installed, run the installation script from the main branch of the gh-aw repository: + +```bash +curl -sL https://raw.githubusercontent.com/github/gh-aw/main/install-gh-aw.sh | bash +``` + +**What this does**: Downloads and installs the gh-aw binary to `~/.local/share/gh/extensions/gh-aw/` + +**Verify installation**: + +```bash +gh aw version +``` + +You should see version information displayed. If you encounter an error, check that: + +- GitHub CLI (`gh`) is installed and authenticated +- The installation script completed without errors +- `~/.local/share/gh/extensions` is in your PATH + +## Step 2: Create the Workflow or do Other Actions + +Follow carefully the instructions in the appropriate prompt file based on the user's request. Read ALL the instructions in the prompt file before taking any action. + +Below, ROOT is the location where you found this file. For example, + +- if this file is at `https://raw.githubusercontent.com/github/gh-aw/main/create.md` then the ROOT is `https://raw.githubusercontent.com/github/gh-aw/main` +- if this file is at `https://raw.githubusercontent.com/github/gh-aw/v0.35.1/create.md` then the ROOT is `https://github.com/github/gh-aw/v0.35.1/main` + +Here are the common actions you may be asked to do, with links to the appropriate prompt files: + +### Create New Workflow + +**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet + +**Prompt file**: `ROOT/.github/aw/create-agentic-workflow.md` + +**Use cases**: + +- "Create a workflow that triages issues" +- "I need a workflow to label pull requests" +- "Design a weekly research automation" + +### Update Existing Workflow + +**Load when**: User wants to modify, improve, or refactor an existing workflow + +**Prompt file**: `ROOT/.github/aw/update-agentic-workflow.md` + +**Use cases**: + +- "Add web-fetch tool to the issue-classifier workflow" +- "Update the PR reviewer to use discussions instead of issues" +- "Improve the prompt for the weekly-research workflow" + +### Debug Workflow + +**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors + +**Prompt file**: `ROOT/.github/aw/debug-agentic-workflow.md` + +**Use cases**: + +- "Why is this workflow failing?" +- "Analyze the logs for workflow X" +- "Investigate missing tool calls in run #12345" + +### Upgrade Agentic Workflows + +**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations + +**Prompt file**: `ROOT/.github/aw/upgrade-agentic-workflows.md` + +**Use cases**: + +- "Upgrade all workflows to the latest version" +- "Fix deprecated fields in workflows" +- "Apply breaking changes from the new release" + +### Create Shared Agentic Workflow + +**Load when**: User wants to create a reusable workflow component or wrap an MCP server + +**Prompt file**: `ROOT/.github/aw/create-shared-agentic-workflow.md` + +**Use cases**: + +- "Create a shared component for Notion integration" +- "Wrap the Slack MCP server as a reusable component" +- "Design a shared workflow for database queries" + +If you need to clarify requirements or discuss options, and you are working in an interactive agent chat system, do so interactively with the user. If running non-interactively, make reasonable assumptions based on the repository context. + +## Step 3: Review Changes + +Check what files were changed or created: + +```bash +git status +``` + +If creating a workflow, the actual files you created will be under `.github/workflows/`. There should be at least one workflow file and one lock file. + +- `.github/workflows/.md` +- `.github/workflows/.lock.yml` + +If creating a workflow, check the .gitattributes file and make sure it exists and contains at least the following line: + +```text +.github/workflows/*.lock.yml linguist-generated=true merge=ours +``` + +You do not need to run `gh aw init` as part of your workflow creation. However if you did run this you may also see: + +- `.github/aw/github-agentic-workflows.md` +- `.github/agents/agentic-workflows.agent.md` +- `.vscode/settings.json` +- `.vscode/mcp.json` +- And several other configuration files + +Don't remove these but don't add them if not already present in the repo. Unless instructed otherwise do NOT commit the changes to ANY files except the gitattributes file and workflow files. + +- `.gitattributes` +- `.github/workflows/.md` +- `.github/workflows/.lock.yml` + +## Step 4: Commit and Push Changes + +Commit the changes, e.g. + +```bash +git add .gitattributes .github/workflows/.md .github/workflows/.lock.yml +git commit -m "Initialize repository for GitHub Agentic Workflows" +git push +``` + +If there is branch protection on the default branch, create a pull request instead and report the link to the pull request. + +## Troubleshooting + +See the separate guides on troubleshooting common issues. + +## Instructions + +When a user interacts with you: + +1. **Identify the task type** from the user's request +2. **Fetch and read the appropriate prompt** +3. **Follow the loaded prompt's instructions** exactly +4. **If uncertain**, ask clarifying questions to determine the right prompt + +## Quick Reference + +```bash +# Create a new workflow +gh aw new + +# Compile workflows +gh aw compile [workflow-name] + +# Debug workflow runs +gh aw logs [workflow-name] +gh aw audit + +# Upgrade workflows +gh aw fix --write +gh aw compile --validate +``` + +## Key Features of gh-aw + +- **Natural Language Workflows**: Write workflows in markdown with YAML frontmatter +- **AI Engine Support**: Copilot, Claude, Codex, or custom engines +- **MCP Server Integration**: Connect to Model Context Protocol servers for tools +- **Safe Outputs**: Structured communication between AI and GitHub API +- **Strict Mode**: Security-first validation and sandboxing +- **Shared Components**: Reusable workflow building blocks +- **Repo Memory**: Persistent git-backed storage for agents + +## Important Notes + +- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions +- Follow security best practices: minimal permissions, explicit network access, no template injection From 5034245d7a6cdd9475325b180ee51188f7f922b4 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 16:54:12 -0500 Subject: [PATCH 032/147] Add weekly upstream sync workflow documentation - Introduced a new GitHub Actions workflow for weekly upstream sync with the Copilot SDK. - The workflow checks for new commits, creates issues for changes, and manages stale issues. - Detailed documentation includes workflow triggers, permissions, secrets, and step-by-step processes. --- .github/agents/agentic-workflows.agent.md | 167 +++ .github/agents/ghaw.md | 201 --- .github/workflows/copilot-setup-steps.yml | 26 + .../workflows/weekly-upstream-sync.lock.yml | 1110 +++++++++++++++++ .github/workflows/weekly-upstream-sync.md | 158 +++ .vscode/mcp.json | 12 + 6 files changed, 1473 insertions(+), 201 deletions(-) create mode 100644 .github/agents/agentic-workflows.agent.md delete mode 100644 .github/agents/ghaw.md create mode 100644 .github/workflows/copilot-setup-steps.yml create mode 100644 .github/workflows/weekly-upstream-sync.lock.yml create mode 100644 .github/workflows/weekly-upstream-sync.md create mode 100644 .vscode/mcp.json diff --git a/.github/agents/agentic-workflows.agent.md b/.github/agents/agentic-workflows.agent.md new file mode 100644 index 000000000..5ba0b82fd --- /dev/null +++ b/.github/agents/agentic-workflows.agent.md @@ -0,0 +1,167 @@ +--- +description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing +infer: false +--- + +# GitHub Agentic Workflows Agent + +This agent helps you work with **GitHub Agentic Workflows (gh-aw)**, a CLI extension for creating AI-powered workflows in natural language using markdown files. + +## What This Agent Does + +This is a **dispatcher agent** that routes your request to the appropriate specialized prompt based on your task: + +- **Creating new workflows**: Routes to `create` prompt +- **Updating existing workflows**: Routes to `update` prompt +- **Debugging workflows**: Routes to `debug` prompt +- **Upgrading workflows**: Routes to `upgrade-agentic-workflows` prompt +- **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt + +Workflows may optionally include: + +- **Project tracking / monitoring** (GitHub Projects updates, status reporting) +- **Orchestration / coordination** (one workflow assigning agents or dispatching and coordinating other workflows) + +## Files This Applies To + +- Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md` +- Workflow lock files: `.github/workflows/*.lock.yml` +- Shared components: `.github/workflows/shared/*.md` +- Configuration: https://github.com/github/gh-aw/blob/v0.42.17/.github/aw/github-agentic-workflows.md + +## Problems This Solves + +- **Workflow Creation**: Design secure, validated agentic workflows with proper triggers, tools, and permissions +- **Workflow Debugging**: Analyze logs, identify missing tools, investigate failures, and fix configuration issues +- **Version Upgrades**: Migrate workflows to new gh-aw versions, apply codemods, fix breaking changes +- **Component Design**: Create reusable shared workflow components that wrap MCP servers + +## How to Use + +When you interact with this agent, it will: + +1. **Understand your intent** - Determine what kind of task you're trying to accomplish +2. **Route to the right prompt** - Load the specialized prompt file for your task +3. **Execute the task** - Follow the detailed instructions in the loaded prompt + +## Available Prompts + +### Create New Workflow +**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.42.17/.github/aw/create-agentic-workflow.md + +**Use cases**: +- "Create a workflow that triages issues" +- "I need a workflow to label pull requests" +- "Design a weekly research automation" + +### Update Existing Workflow +**Load when**: User wants to modify, improve, or refactor an existing workflow + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.42.17/.github/aw/update-agentic-workflow.md + +**Use cases**: +- "Add web-fetch tool to the issue-classifier workflow" +- "Update the PR reviewer to use discussions instead of issues" +- "Improve the prompt for the weekly-research workflow" + +### Debug Workflow +**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.42.17/.github/aw/debug-agentic-workflow.md + +**Use cases**: +- "Why is this workflow failing?" +- "Analyze the logs for workflow X" +- "Investigate missing tool calls in run #12345" + +### Upgrade Agentic Workflows +**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.42.17/.github/aw/upgrade-agentic-workflows.md + +**Use cases**: +- "Upgrade all workflows to the latest version" +- "Fix deprecated fields in workflows" +- "Apply breaking changes from the new release" + +### Create Shared Agentic Workflow +**Load when**: User wants to create a reusable workflow component or wrap an MCP server + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.42.17/.github/aw/create-shared-agentic-workflow.md + +**Use cases**: +- "Create a shared component for Notion integration" +- "Wrap the Slack MCP server as a reusable component" +- "Design a shared workflow for database queries" + +### Orchestration and Delegation + +**Load when**: Creating or updating workflows that coordinate multiple agents or dispatch work to other workflows + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.42.17/.github/aw/orchestration.md + +**Use cases**: +- Assigning work to AI coding agents +- Dispatching specialized worker workflows +- Using correlation IDs for tracking +- Orchestration design patterns + +### GitHub Projects Integration + +**Load when**: Creating or updating workflows that manage GitHub Projects v2 + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.42.17/.github/aw/projects.md + +**Use cases**: +- Tracking items and fields with update-project +- Posting periodic run summaries +- Creating new projects +- Projects v2 authentication and configuration + +## Instructions + +When a user interacts with you: + +1. **Identify the task type** from the user's request +2. **Load the appropriate prompt** from the GitHub repository URLs listed above +3. **Follow the loaded prompt's instructions** exactly +4. **If uncertain**, ask clarifying questions to determine the right prompt + +## Quick Reference + +```bash +# Initialize repository for agentic workflows +gh aw init + +# Compile workflows +gh aw compile [workflow-name] + +# Debug workflow runs +gh aw logs [workflow-name] +gh aw audit + +# Upgrade workflows +gh aw fix --write +gh aw compile --validate +``` + +## Key Features of gh-aw + +- **Natural Language Workflows**: Write workflows in markdown with YAML frontmatter +- **AI Engine Support**: Copilot, Claude, Codex, or custom engines +- **MCP Server Integration**: Connect to Model Context Protocol servers for tools +- **Safe Outputs**: Structured communication between AI and GitHub API +- **Strict Mode**: Security-first validation and sandboxing +- **Shared Components**: Reusable workflow building blocks +- **Repo Memory**: Persistent git-backed storage for agents +- **Sandboxed Execution**: All workflows run in the Agent Workflow Firewall (AWF) sandbox, enabling full `bash` and `edit` tools by default + +## Important Notes + +- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.42.17/.github/aw/github-agentic-workflows.md for complete documentation +- Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud +- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions +- **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF +- Follow security best practices: minimal permissions, explicit network access, no template injection diff --git a/.github/agents/ghaw.md b/.github/agents/ghaw.md deleted file mode 100644 index 1f7d83066..000000000 --- a/.github/agents/ghaw.md +++ /dev/null @@ -1,201 +0,0 @@ -# Creating Agentic Workflows and Other Actions - -This prompt guides you, a coding agent, to create, debug, update or do other actions related to **GitHub Agentic Workflows (gh-aw)** in a repository. - -## Step 1: Install GitHub Agentic Workflows CLI Extension - -Check if `gh aw` is installed by running - -```bash -gh aw version -``` - -If it is installed, run: - -```bash -gh extension upgrade aw -``` - -to upgrade to latest. If it is not installed, run the installation script from the main branch of the gh-aw repository: - -```bash -curl -sL https://raw.githubusercontent.com/github/gh-aw/main/install-gh-aw.sh | bash -``` - -**What this does**: Downloads and installs the gh-aw binary to `~/.local/share/gh/extensions/gh-aw/` - -**Verify installation**: - -```bash -gh aw version -``` - -You should see version information displayed. If you encounter an error, check that: - -- GitHub CLI (`gh`) is installed and authenticated -- The installation script completed without errors -- `~/.local/share/gh/extensions` is in your PATH - -## Step 2: Create the Workflow or do Other Actions - -Follow carefully the instructions in the appropriate prompt file based on the user's request. Read ALL the instructions in the prompt file before taking any action. - -Below, ROOT is the location where you found this file. For example, - -- if this file is at `https://raw.githubusercontent.com/github/gh-aw/main/create.md` then the ROOT is `https://raw.githubusercontent.com/github/gh-aw/main` -- if this file is at `https://raw.githubusercontent.com/github/gh-aw/v0.35.1/create.md` then the ROOT is `https://github.com/github/gh-aw/v0.35.1/main` - -Here are the common actions you may be asked to do, with links to the appropriate prompt files: - -### Create New Workflow - -**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet - -**Prompt file**: `ROOT/.github/aw/create-agentic-workflow.md` - -**Use cases**: - -- "Create a workflow that triages issues" -- "I need a workflow to label pull requests" -- "Design a weekly research automation" - -### Update Existing Workflow - -**Load when**: User wants to modify, improve, or refactor an existing workflow - -**Prompt file**: `ROOT/.github/aw/update-agentic-workflow.md` - -**Use cases**: - -- "Add web-fetch tool to the issue-classifier workflow" -- "Update the PR reviewer to use discussions instead of issues" -- "Improve the prompt for the weekly-research workflow" - -### Debug Workflow - -**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors - -**Prompt file**: `ROOT/.github/aw/debug-agentic-workflow.md` - -**Use cases**: - -- "Why is this workflow failing?" -- "Analyze the logs for workflow X" -- "Investigate missing tool calls in run #12345" - -### Upgrade Agentic Workflows - -**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations - -**Prompt file**: `ROOT/.github/aw/upgrade-agentic-workflows.md` - -**Use cases**: - -- "Upgrade all workflows to the latest version" -- "Fix deprecated fields in workflows" -- "Apply breaking changes from the new release" - -### Create Shared Agentic Workflow - -**Load when**: User wants to create a reusable workflow component or wrap an MCP server - -**Prompt file**: `ROOT/.github/aw/create-shared-agentic-workflow.md` - -**Use cases**: - -- "Create a shared component for Notion integration" -- "Wrap the Slack MCP server as a reusable component" -- "Design a shared workflow for database queries" - -If you need to clarify requirements or discuss options, and you are working in an interactive agent chat system, do so interactively with the user. If running non-interactively, make reasonable assumptions based on the repository context. - -## Step 3: Review Changes - -Check what files were changed or created: - -```bash -git status -``` - -If creating a workflow, the actual files you created will be under `.github/workflows/`. There should be at least one workflow file and one lock file. - -- `.github/workflows/.md` -- `.github/workflows/.lock.yml` - -If creating a workflow, check the .gitattributes file and make sure it exists and contains at least the following line: - -```text -.github/workflows/*.lock.yml linguist-generated=true merge=ours -``` - -You do not need to run `gh aw init` as part of your workflow creation. However if you did run this you may also see: - -- `.github/aw/github-agentic-workflows.md` -- `.github/agents/agentic-workflows.agent.md` -- `.vscode/settings.json` -- `.vscode/mcp.json` -- And several other configuration files - -Don't remove these but don't add them if not already present in the repo. Unless instructed otherwise do NOT commit the changes to ANY files except the gitattributes file and workflow files. - -- `.gitattributes` -- `.github/workflows/.md` -- `.github/workflows/.lock.yml` - -## Step 4: Commit and Push Changes - -Commit the changes, e.g. - -```bash -git add .gitattributes .github/workflows/.md .github/workflows/.lock.yml -git commit -m "Initialize repository for GitHub Agentic Workflows" -git push -``` - -If there is branch protection on the default branch, create a pull request instead and report the link to the pull request. - -## Troubleshooting - -See the separate guides on troubleshooting common issues. - -## Instructions - -When a user interacts with you: - -1. **Identify the task type** from the user's request -2. **Fetch and read the appropriate prompt** -3. **Follow the loaded prompt's instructions** exactly -4. **If uncertain**, ask clarifying questions to determine the right prompt - -## Quick Reference - -```bash -# Create a new workflow -gh aw new - -# Compile workflows -gh aw compile [workflow-name] - -# Debug workflow runs -gh aw logs [workflow-name] -gh aw audit - -# Upgrade workflows -gh aw fix --write -gh aw compile --validate -``` - -## Key Features of gh-aw - -- **Natural Language Workflows**: Write workflows in markdown with YAML frontmatter -- **AI Engine Support**: Copilot, Claude, Codex, or custom engines -- **MCP Server Integration**: Connect to Model Context Protocol servers for tools -- **Safe Outputs**: Structured communication between AI and GitHub API -- **Strict Mode**: Security-first validation and sandboxing -- **Shared Components**: Reusable workflow building blocks -- **Repo Memory**: Persistent git-backed storage for agents - -## Important Notes - -- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions -- Follow security best practices: minimal permissions, explicit network access, no template injection diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000..afffff707 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,26 @@ +name: "Copilot Setup Steps" + +# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set minimal permissions for setup steps + # Copilot Agent receives its own token with appropriate permissions + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install gh-aw extension + uses: github/gh-aw/actions/setup-cli@v0.42.17 + with: + version: v0.42.17 diff --git a/.github/workflows/weekly-upstream-sync.lock.yml b/.github/workflows/weekly-upstream-sync.lock.yml new file mode 100644 index 000000000..5baf1c024 --- /dev/null +++ b/.github/workflows/weekly-upstream-sync.lock.yml @@ -0,0 +1,1110 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.42.17). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md +# +# Weekly upstream sync workflow. Checks for new commits in the official +# Copilot SDK (github/copilot-sdk) and assigns to Copilot to port changes. +# +# frontmatter-hash: 172529183e30b0941e8ae1f70262649342226ab48d4099c1b3f98a406492b7ba + +name: "Weekly Upstream Sync Agentic Workflow" +"on": + schedule: + - cron: "39 8 * * 2" + # Friendly format: weekly (scattered) + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Weekly Upstream Sync Agentic Workflow" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_WORKFLOW_FILE: "weekly-upstream-sync.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + issues: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + 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_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + model: ${{ steps.generate_aw_info.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Checkout .github and .agents folders + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + with: + sparse-checkout: | + .github + .agents + depth: 1 + persist-credentials: false + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.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('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.13.12 + - name: Determine automatic lockdown mode for GitHub MCP server + id: determine-automatic-lockdown + env: + TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + if: env.TOKEN_CHECK != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.13.12 ghcr.io/github/gh-aw-firewall/squid:0.13.12 ghcr.io/github/gh-aw-mcpg:v0.0.113 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p /opt/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' + {"add_comment":{"max":10,"target":"*"},"close_issue":{"max":10,"required_labels":["upstream-sync"]},"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' + [ + { + "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[upstream-sync] \". Labels [upstream-sync] will be automatically added. Assignees [copilot] will be automatically assigned.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "Detailed issue description in Markdown. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate.", + "type": "string" + }, + "labels": { + "description": "Labels to categorize the issue (e.g., 'bug', 'enhancement'). Labels must exist in the repository.", + "items": { + "type": "string" + }, + "type": "array" + }, + "parent": { + "description": "Parent issue number for creating sub-issues. This is the numeric ID from the GitHub URL (e.g., 42 in github.com/owner/repo/issues/42). Can also be a temporary_id (e.g., 'aw_abc123def456') from a previously created issue in the same workflow run.", + "type": [ + "number", + "string" + ] + }, + "temporary_id": { + "description": "Unique temporary identifier for referencing this issue before it's created. Format: 'aw_' followed by 12 hex characters (e.g., 'aw_abc123def456'). Use '#aw_ID' in body text to reference other issues by their temporary_id; these are replaced with actual issue numbers after creation.", + "type": "string" + }, + "title": { + "description": "Concise issue title summarizing the bug, feature, or task. The title appears as the main heading, so keep it brief and descriptive.", + "type": "string" + } + }, + "required": [ + "title", + "body" + ], + "type": "object" + }, + "name": "create_issue" + }, + { + "description": "Close a GitHub issue with a closing comment. Use this when work is complete, the issue is no longer relevant, or it's a duplicate. The closing comment should explain the resolution or reason for closing. CONSTRAINTS: Maximum 10 issue(s) can be closed. Target: *.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "Closing comment explaining why the issue is being closed and summarizing any resolution, workaround, or conclusion.", + "type": "string" + }, + "issue_number": { + "description": "Issue number to close. This is the numeric ID from the GitHub URL (e.g., 901 in github.com/owner/repo/issues/901). If omitted, closes the issue that triggered this workflow (requires an issue event trigger).", + "type": [ + "number", + "string" + ] + } + }, + "required": [ + "body" + ], + "type": "object" + }, + "name": "close_issue" + }, + { + "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 10 comment(s) can be added. Target: *.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "The comment text in Markdown format. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation.", + "type": "string" + }, + "item_number": { + "description": "The issue, pull request, or discussion number to comment on. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123). If omitted, the tool will attempt to resolve the target from the current workflow context (triggering issue, PR, or discussion).", + "type": "number" + } + }, + "required": [ + "body" + ], + "type": "object" + }, + "name": "add_comment" + }, + { + "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "reason": { + "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", + "type": "string" + }, + "tool": { + "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", + "type": "string" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "name": "missing_tool" + }, + { + "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "name": "noop" + }, + { + "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "context": { + "description": "Additional context about the missing data or where it should come from (max 256 characters).", + "type": "string" + }, + "data_type": { + "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", + "type": "string" + }, + "reason": { + "description": "Explanation of why this data is needed to complete the task (max 256 characters).", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "name": "missing_data" + } + ] + EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + } + } + }, + "close_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "issue_number": { + "optionalPositiveInteger": true + } + } + }, + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "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 + } + } + } + } + EOF + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + API_KEY="" + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + PORT=3001 + + # Register API key as secret to mask it from logs + echo "::add-mask::${API_KEY}" + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash /opt/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY="" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export DEBUG="*" + + # Register API key as secret to mask it from logs + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -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_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_LOCKDOWN -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 GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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 ghcr.io/github/gh-aw-mcpg:v0.0.113' + + mkdir -p /home/runner/.copilot + cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.30.3", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues" + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + MCPCONFIG_EOF + - name: Generate agentic run info + id: generate_aw_info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "copilot", + engine_name: "GitHub Copilot CLI", + model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", + version: "", + agent_version: "0.0.405", + cli_version: "v0.42.17", + workflow_name: "Weekly Upstream Sync Agentic Workflow", + experimental: false, + supports_tools_allowlist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults","github"], + firewall_enabled: true, + awf_version: "v0.13.12", + awmg_version: "v0.0.113", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in PR + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/jobs + core.setOutput('model', awInfo.model); + - name: Generate workflow overview + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); + await generateWorkflowOverview(core); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" + + PROMPT_EOF + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + + GitHub API Access Instructions + + The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. + + + To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. + + Discover available tools from the safeoutputs MCP server. + + **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. + + **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. + + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + PROMPT_EOF + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + + PROMPT_EOF + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/workflows/weekly-upstream-sync.md}} + PROMPT_EOF + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + 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 + } + }); + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.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' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.13.12 --skip-pull \ + -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' \ + 2>&1 | tee /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: | + # Copy Copilot session state files to logs folder for artifact collection + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; then + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true + echo "Session state files copied successfully" + else + echo "No session-state directory found at $SESSION_STATE_DIR" + fi + - 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 /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + 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: Upload Safe Outputs + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: safe-output + path: ${{ env.GH_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output + id: collect_output + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.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" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Upload sanitized agent output + if: always() && env.GH_AW_AGENT_OUTPUT + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-output + path: ${{ env.GH_AW_AGENT_OUTPUT }} + if-no-files-found: warn + - name: Upload engine output files + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent_outputs + path: | + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + if-no-files-found: ignore + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + outputs: + 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 + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: 1 + GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Handle No-Op Message + id: handle_noop_message + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/notify_comment_error.cjs'); + await main(); + + detection: + needs: agent + if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' + runs-on: ubuntu-latest + permissions: {} + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + timeout-minutes: 10 + outputs: + success: ${{ steps.parse_results.outputs.success }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Download agent artifacts + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-artifacts + path: /tmp/gh-aw/threat-detection/ + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/threat-detection/ + - name: Echo agent output types + env: + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + run: | + echo "Agent output-types: $AGENT_OUTPUT_TYPES" + - name: Setup threat detection + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" + WORKFLOW_DESCRIPTION: "Weekly upstream sync workflow. Checks for new commits in the official\nCopilot SDK (github/copilot-sdk) and assigns to Copilot to port changes." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool shell(cat) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(tail) + # --allow-tool shell(wc) + timeout-minutes: 20 + run: | + set -o pipefail + COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" + mkdir -p /tmp/ + mkdir -p /tmp/gh-aw/ + mkdir -p /tmp/gh-aw/agent/ + mkdir -p /tmp/gh-aw/sandbox/agent/logs/ + copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection results + id: parse_results + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection log + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: threat-detection.log + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + + safe_outputs: + needs: + - agent + - detection + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_ENGINE_ID: "copilot" + GH_AW_WORKFLOW_ID: "weekly-upstream-sync" + GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" + outputs: + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + 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 }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@v0.42.17 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"close_issue\":{\"max\":10,\"required_labels\":[\"upstream-sync\"],\"target\":\"*\"},\"create_issue\":{\"assignees\":[\"copilot\"],\"labels\":[\"upstream-sync\"],\"max\":1,\"title_prefix\":\"[upstream-sync] \"},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_ASSIGN_COPILOT: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Assign Copilot to created issues + if: steps.process_safe_outputs.outputs.issues_to_assign_copilot != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_ISSUES_TO_ASSIGN_COPILOT: ${{ steps.process_safe_outputs.outputs.issues_to_assign_copilot }} + with: + github-token: ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/assign_copilot_to_created_issues.cjs'); + await main(); + diff --git a/.github/workflows/weekly-upstream-sync.md b/.github/workflows/weekly-upstream-sync.md new file mode 100644 index 000000000..15d18ee46 --- /dev/null +++ b/.github/workflows/weekly-upstream-sync.md @@ -0,0 +1,158 @@ +--- +description: | + Weekly upstream sync workflow. Checks for new commits in the official + Copilot SDK (github/copilot-sdk) and assigns to Copilot to port changes. + +on: + schedule: weekly + workflow_dispatch: + +permissions: + contents: read + actions: read + issues: read + +network: + allowed: + - defaults + - github + +tools: + github: + toolsets: [context, repos, issues] + +safe-outputs: + create-issue: + title-prefix: "[upstream-sync] " + assignees: [copilot] + labels: [upstream-sync] + close-issue: + required-labels: [upstream-sync] + target: "*" + max: 10 + add-comment: + target: "*" + max: 10 + noop: +--- +# Weekly Upstream Sync Agentic Workflow +This document describes the `weekly-upstream-sync.yml` GitHub Actions workflow, which automates the detection of new changes in the official [Copilot SDK](https://github.com/github/copilot-sdk) and delegates the merge work to the Copilot coding agent. + +## Overview + +The workflow runs on a **weekly schedule** (every Monday at 10:00 UTC) and can also be triggered manually. It does **not** perform the actual merge — instead, it detects upstream changes and creates a GitHub issue assigned to `copilot`, which then follows the [agentic-merge-upstream](../prompts/agentic-merge-upstream.prompt.md) prompt to port the changes. + +## Trigger + +| Trigger | Schedule | +|---|---| +| `schedule` | Every Monday at 10:00 UTC (`0 10 * * 1`) | +| `workflow_dispatch` | Manual trigger from the Actions tab | + +## Permissions + +| Permission | Level | Purpose | +|---|---|---| +| `contents` | `write` | Read `.lastmerge` file | +| `issues` | `write` | Create/close upstream-sync issues | +| `pull-requests` | `write` | Allow Copilot agent to create PRs | + +## Secrets + +| Secret | Purpose | +|---|---| +| `COPILOT_ISSUE_PR_AGENTIC_WORKFLOW` | PAT used as `GH_TOKEN` for `gh` CLI operations (issue create/close/comment). Required because the default `GITHUB_TOKEN` cannot assign issues to `copilot-swe-agent`. | + +## Workflow Steps + +### 1. Checkout repository + +Checks out the repo to read the `.lastmerge` file, which contains the SHA of the last upstream commit that was merged into the Java SDK. + +### 2. Check for upstream changes + +- Reads the last merged commit hash from `.lastmerge` +- Clones the upstream `github/copilot-sdk` repository +- Compares `.lastmerge` against upstream `HEAD` +- If they match: sets `has_changes=false` +- If they differ: counts new commits, generates a summary (up to 20 most recent), and sets outputs (`commit_count`, `upstream_head`, `last_merge`, `summary`) + +### 3. Close previous upstream-sync issues (when changes found) + +**Condition:** `has_changes == true` + +Before creating a new issue, closes any existing open issues with the `upstream-sync` label. This prevents stale issues from accumulating when previous sync attempts were incomplete or superseded. Each closed issue receives a comment explaining it was superseded. + +### 4. Close stale upstream-sync issues (when no changes found) + +**Condition:** `has_changes == false` + +If the upstream is already up to date, closes any lingering open `upstream-sync` issues with a comment noting that no changes were detected. This handles the case where a previous issue was created but the changes were merged manually (updating `.lastmerge`) before the agent completed. + +### 5. Create issue and assign to Copilot + +**Condition:** `has_changes == true` + +Creates a new GitHub issue with: + +- **Title:** `Upstream sync: N new commits (YYYY-MM-DD)` +- **Label:** `upstream-sync` +- **Assignee:** `copilot-swe-agent` +- **Body:** Contains commit count, commit range links, a summary of recent commits, and a link to the merge prompt + +The Copilot coding agent picks up the issue, creates a branch and PR, then follows the merge prompt to port the changes. + +### 6. Summary + +Writes a GitHub Actions step summary with: + +- Whether changes were detected +- Commit count and range +- Recent upstream commits +- Link to the created issue (if any) + +## Flow Diagram + +``` +┌─────────────────────┐ +│ Schedule / Manual │ +└──────────┬──────────┘ + │ + ▼ +┌─────────────────────┐ +│ Read .lastmerge │ +│ Clone upstream SDK │ +│ Compare commits │ +└──────────┬──────────┘ + │ + ┌─────┴─────┐ + │ │ + changes? no changes + │ │ + ▼ ▼ +┌──────────┐ ┌──────────────────┐ +│ Close old│ │ Close stale │ +│ issues │ │ issues │ +└────┬─────┘ └──────────────────┘ + │ + ▼ +┌──────────────────────────┐ +│ Create issue assigned to │ +│ copilot-swe-agent │ +└──────────────────────────┘ + │ + ▼ +┌──────────────────────────┐ +│ Agent follows prompt to │ +│ port changes → PR │ +└──────────────────────────┘ +``` + +## Related Files + +| File | Purpose | +|---|---| +| `.lastmerge` | Stores the SHA of the last merged upstream commit | +| [agentic-merge-upstream.prompt.md](../prompts/agentic-merge-upstream.prompt.md) | Detailed instructions the Copilot agent follows to port changes | +| [upstream-sync.md](upstream-sync.md) | gh-aw agentic workflow (alternative approach that runs the agent directly in CI) | +| `.github/scripts/upstream-sync/` | Helper scripts used by the merge prompt | diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 000000000..6699af564 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,12 @@ +{ + "servers": { + "github-agentic-workflows": { + "command": "gh", + "args": [ + "aw", + "mcp-server" + ], + "cwd": "${workspaceFolder}" + } + } +} \ No newline at end of file From 33b0f077f88a9ab7ec037247ae020e49656e8a9a Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 16:55:33 -0500 Subject: [PATCH 033/147] Name --- .github/agents/agentic-workflows.agent.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/agents/agentic-workflows.agent.md b/.github/agents/agentic-workflows.agent.md index 5ba0b82fd..3cf66a3b2 100644 --- a/.github/agents/agentic-workflows.agent.md +++ b/.github/agents/agentic-workflows.agent.md @@ -1,4 +1,5 @@ --- +name: Agentic Workflows description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing infer: false --- From ca50f7f020d98a3961d748614ee9aae24720758d Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 17:45:54 -0500 Subject: [PATCH 034/147] feat: add workflow to create a sample test issue assigned to Copilot. --- .github/workflows/copilot-issue.yml | 69 -- .github/workflows/copilot-setup-steps.txt | 56 - .github/workflows/copilot-setup-steps.yml | 29 +- ....lock.yml => create-sample-issue.lock.yml} | 78 +- .github/workflows/create-sample-issue.md | 29 + .github/workflows/daily-repo-status.md | 50 - .../workflows/increase-test-coverage.lock.yml | 1097 ----------------- .github/workflows/increase-test-coverage.md | 174 --- .github/workflows/upstream-sync.lock.yml | 1066 ---------------- .github/workflows/upstream-sync.md | 234 ---- 10 files changed, 96 insertions(+), 2786 deletions(-) delete mode 100644 .github/workflows/copilot-issue.yml delete mode 100644 .github/workflows/copilot-setup-steps.txt rename .github/workflows/{daily-repo-status.lock.yml => create-sample-issue.lock.yml} (93%) create mode 100644 .github/workflows/create-sample-issue.md delete mode 100644 .github/workflows/daily-repo-status.md delete mode 100644 .github/workflows/increase-test-coverage.lock.yml delete mode 100644 .github/workflows/increase-test-coverage.md delete mode 100644 .github/workflows/upstream-sync.lock.yml delete mode 100644 .github/workflows/upstream-sync.md diff --git a/.github/workflows/copilot-issue.yml b/.github/workflows/copilot-issue.yml deleted file mode 100644 index 3a1d1898f..000000000 --- a/.github/workflows/copilot-issue.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Create Issue for Copilot Coding Agent - -on: - workflow_dispatch: - inputs: - title: - description: 'Issue title' - required: false - type: string - push: - branches: [main] - paths: - - '.github/workflows/copilot-issue.yml' - - '.github/scripts/ci/create-issue-assigned-to-copilot.ts' - -jobs: - create-issue: - runs-on: ubuntu-latest - permissions: - issues: write - steps: - - name: Create issue and assign to Copilot - env: - GH_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - run: | - TITLE="${{ inputs.title || 'Investigation needed from Copilot Coding Agent' }}" - gh api \ - --method POST \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - /repos/${{ github.repository }}/issues \ - --input - <<< "$(jq -n \ - --arg title "$TITLE" \ - --arg repo "${{ github.repository }}" \ - '{ - title: $title, - agent_assignment: { - target_repo: $repo, - base_branch: "main", - custom_instructions: "", - custom_agent: "", - model: "" - } - }')" - - create-issue-graphql: - runs-on: ubuntu-latest - permissions: - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '22' - - - name: Install dependencies - run: npm install @octokit/rest tsx - working-directory: .github/scripts/ci - - - name: Create issue via GraphQL - env: - GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GITHUB_REPO_OWNER: ${{ github.repository_owner }} - GITHUB_REPO_NAME: ${{ github.event.repository.name }} - run: npx tsx create-issue-assigned-to-copilot.ts "${{ inputs.title || 'Investigation needed from Copilot Coding Agent' }}" - working-directory: .github/scripts/ci diff --git a/.github/workflows/copilot-setup-steps.txt b/.github/workflows/copilot-setup-steps.txt deleted file mode 100644 index f92d7f699..000000000 --- a/.github/workflows/copilot-setup-steps.txt +++ /dev/null @@ -1,56 +0,0 @@ -name: "Copilot Setup Steps" - -on: - workflow_dispatch: - push: - paths: - - .github/workflows/copilot-setup-steps.yml - pull_request: - paths: - - .github/workflows/copilot-setup-steps.yml - -jobs: - copilot-setup-steps: - runs-on: ubuntu-latest - - permissions: - contents: read - issues: write - pull-requests: write - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version: 22 - - - name: Set up JDK 17 - uses: actions/setup-java@v5 - with: - java-version: '17' - distribution: 'temurin' - cache: 'maven' - - - name: Verify Java version - run: java -version - - - name: Download dependencies - run: mvn dependency:go-offline -B - - # Install gh-aw extension for advanced GitHub CLI features - - name: Install gh-aw extension - run: | - curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/refs/heads/main/install-gh-aw.sh | bash - - # Verify installations - - name: Verify tool installations - run: | - echo "=== Verifying installations ===" - node --version - npm --version - java -version - gh --version - gh aw version - echo "✅ All tools installed successfully" diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index afffff707..8d2964558 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -18,9 +18,36 @@ jobs: contents: read steps: + # Clone the repository - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 + + # Install GitHub CLI and gh-aw extension for Copilot Agent interaction - name: Install gh-aw extension uses: github/gh-aw/actions/setup-cli@v0.42.17 with: version: v0.42.17 + + # Setup Node.js + - uses: actions/setup-node@v6 + with: + node-version: 22 + + # Set up JDK 17 + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' + + # Verify installations + - name: Verify tool installations + run: | + echo "=== Verifying installations ===" + node --version + npm --version + java -version + gh --version + gh aw version + echo "✅ All tools installed successfully" diff --git a/.github/workflows/daily-repo-status.lock.yml b/.github/workflows/create-sample-issue.lock.yml similarity index 93% rename from .github/workflows/daily-repo-status.lock.yml rename to .github/workflows/create-sample-issue.lock.yml index aac922eaa..13678c0c3 100644 --- a/.github/workflows/daily-repo-status.lock.yml +++ b/.github/workflows/create-sample-issue.lock.yml @@ -15,24 +15,16 @@ # # This file was automatically generated by gh-aw (v0.42.17). DO NOT EDIT. # -# To update this file, edit githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323 and run: +# To update this file, edit the corresponding .md file and run: # gh aw compile # For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md # -# This workflow creates daily repo status reports. It gathers recent repository -# activity (issues, PRs, discussions, releases, code changes) and generates -# engaging GitHub issues with productivity insights, community highlights, -# and project recommendations. +# Creates a sample test issue and assigns it to Copilot. # -# Source: githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323 -# -# frontmatter-hash: 7936e337cf8a6040e3d48eb456ec675afa36d9ef413fef734ff36966c0d72be1 +# frontmatter-hash: 389d6be230745d9867d266207c5d2a00c790b059fa1fa9779e3242b02bade05c -name: "Daily Repo Status" +name: "Create Sample Issue" "on": - schedule: - - cron: "13 19 * * *" - # Friendly format: daily (scattered) workflow_dispatch: permissions: {} @@ -40,7 +32,7 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" -run-name: "Daily Repo Status" +run-name: "Create Sample Issue" jobs: activation: @@ -58,7 +50,7 @@ jobs: - name: Check workflow file timestamps uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: - GH_AW_WORKFLOW_FILE: "daily-repo-status.lock.yml" + GH_AW_WORKFLOW_FILE: "create-sample-issue.lock.yml" with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); @@ -70,9 +62,11 @@ jobs: needs: activation runs-on: ubuntu-latest permissions: + actions: read contents: read issues: read - pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" @@ -161,7 +155,7 @@ jobs: cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' [ { - "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[repo-status] \". Labels [report daily-status] will be automatically added.", + "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[test] \". Labels [test] will be automatically added. Assignees [copilot] will be automatically assigned.", "inputSchema": { "additionalProperties": false, "properties": { @@ -417,7 +411,7 @@ jobs: "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + "GITHUB_TOOLSETS": "context,issues" } }, "safeoutputs": { @@ -450,7 +444,7 @@ jobs: version: "", agent_version: "0.0.405", cli_version: "v0.42.17", - workflow_name: "Daily Repo Status", + workflow_name: "Create Sample Issue", experimental: false, supports_tools_allowlist: true, supports_http_transport: true, @@ -555,7 +549,7 @@ jobs: PROMPT_EOF cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - {{#runtime-import .github/workflows/daily-repo-status.md}} + {{#runtime-import .github/workflows/create-sample-issue.md}} PROMPT_EOF - name: Substitute placeholders uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 @@ -798,9 +792,7 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: 1 - GH_AW_WORKFLOW_NAME: "Daily Repo Status" - GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/36514b53398b42745c0de75b3e7edeb839890323/workflows/daily-repo-status.md" + GH_AW_WORKFLOW_NAME: "Create Sample Issue" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -813,9 +805,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Daily Repo Status" - GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/36514b53398b42745c0de75b3e7edeb839890323/workflows/daily-repo-status.md" + GH_AW_WORKFLOW_NAME: "Create Sample Issue" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -828,9 +818,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Daily Repo Status" - GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/36514b53398b42745c0de75b3e7edeb839890323/workflows/daily-repo-status.md" + GH_AW_WORKFLOW_NAME: "Create Sample Issue" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} @@ -847,9 +835,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Daily Repo Status" - GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/36514b53398b42745c0de75b3e7edeb839890323/workflows/daily-repo-status.md" + GH_AW_WORKFLOW_NAME: "Create Sample Issue" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} @@ -869,7 +855,7 @@ jobs: GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_WORKFLOW_NAME: "Daily Repo Status" + GH_AW_WORKFLOW_NAME: "Create Sample Issue" GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} with: @@ -885,6 +871,8 @@ jobs: if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' runs-on: ubuntu-latest permissions: {} + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" timeout-minutes: 10 outputs: success: ${{ steps.parse_results.outputs.success }} @@ -913,8 +901,8 @@ jobs: - name: Setup threat detection uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: - WORKFLOW_NAME: "Daily Repo Status" - WORKFLOW_DESCRIPTION: "This workflow creates daily repo status reports. It gathers recent repository\nactivity (issues, PRs, discussions, releases, code changes) and generates\nengaging GitHub issues with productivity insights, community highlights,\nand project recommendations." + WORKFLOW_NAME: "Create Sample Issue" + WORKFLOW_DESCRIPTION: "Creates a sample test issue and assigns it to Copilot." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | @@ -990,10 +978,9 @@ jobs: issues: write timeout-minutes: 15 env: - GH_AW_WORKFLOW_ID: "daily-repo-status" - GH_AW_WORKFLOW_NAME: "Daily Repo Status" - GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/36514b53398b42745c0de75b3e7edeb839890323/workflows/daily-repo-status.md" + GH_AW_ENGINE_ID: "copilot" + GH_AW_WORKFLOW_ID: "create-sample-issue" + GH_AW_WORKFLOW_NAME: "Create Sample Issue" outputs: create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} @@ -1020,7 +1007,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"labels\":[\"report\",\"daily-status\"],\"max\":1,\"title_prefix\":\"[repo-status] \"},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"assignees\":[\"copilot\"],\"labels\":[\"test\"],\"max\":1,\"title_prefix\":\"[test] \"},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_ASSIGN_COPILOT: "true" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1028,4 +1016,16 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); + - name: Assign Copilot to created issues + if: steps.process_safe_outputs.outputs.issues_to_assign_copilot != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_ISSUES_TO_ASSIGN_COPILOT: ${{ steps.process_safe_outputs.outputs.issues_to_assign_copilot }} + with: + github-token: ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/assign_copilot_to_created_issues.cjs'); + await main(); diff --git a/.github/workflows/create-sample-issue.md b/.github/workflows/create-sample-issue.md new file mode 100644 index 000000000..e5bc29f7f --- /dev/null +++ b/.github/workflows/create-sample-issue.md @@ -0,0 +1,29 @@ +--- +description: Creates a sample test issue and assigns it to Copilot. + +on: + workflow_dispatch: + +permissions: + contents: read + actions: read + issues: read + +tools: + github: + toolsets: [context, issues] + +safe-outputs: + create-issue: + title-prefix: "[test] " + assignees: [copilot] + labels: [test] +--- +# Create Sample Issue + +Create a test issue in this repository assigned to Copilot. The issue should contain: + +- **Title:** `Sample test issue` +- **Body:** `This is an automated test issue created by the create-sample-issue workflow. Feel free to close it.` + +| `.github/scripts/upstream-sync/` | Helper scripts used by the merge prompt | diff --git a/.github/workflows/daily-repo-status.md b/.github/workflows/daily-repo-status.md deleted file mode 100644 index d6d6b2931..000000000 --- a/.github/workflows/daily-repo-status.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -description: | - This workflow creates daily repo status reports. It gathers recent repository - activity (issues, PRs, discussions, releases, code changes) and generates - engaging GitHub issues with productivity insights, community highlights, - and project recommendations. - -on: - schedule: daily - workflow_dispatch: - -permissions: - contents: read - issues: read - pull-requests: read - -network: defaults - -tools: - github: - -safe-outputs: - create-issue: - title-prefix: "[repo-status] " - labels: [report, daily-status] -source: githubnext/agentics/workflows/daily-repo-status.md@36514b53398b42745c0de75b3e7edeb839890323 ---- - -# Daily Repo Status - -Create an upbeat daily status report for the repo as a GitHub issue. - -## What to include - -- Recent repository activity (issues, PRs, discussions, releases, code changes) -- Progress tracking, goal reminders and highlights -- Project status and recommendations -- Actionable next steps for maintainers - -## Style - -- Be positive, encouraging, and helpful 🌟 -- Use emojis moderately for engagement -- Keep it concise - adjust length based on actual activity - -## Process - -1. Gather recent activity from the repository -2. Study the repository, its issues and its pull requests -3. Create a new GitHub issue with your findings and insights diff --git a/.github/workflows/increase-test-coverage.lock.yml b/.github/workflows/increase-test-coverage.lock.yml deleted file mode 100644 index 553e38673..000000000 --- a/.github/workflows/increase-test-coverage.lock.yml +++ /dev/null @@ -1,1097 +0,0 @@ -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# This file was automatically generated by gh-aw (v0.42.17). DO NOT EDIT. -# -# To update this file, edit the corresponding .md file and run: -# gh aw compile -# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md -# -# Automated workflow to identify untested code and add comprehensive tests to increase coverage. -# Groups related tests sensibly and creates PRs for human review. -# -# frontmatter-hash: 356158feac1509bba005f2374c86a35bd82440bae39e70a1aefb4cd2d5c7ff11 - -name: "Increase Test Coverage" -"on": - workflow_dispatch: - inputs: - min_coverage_threshold: - default: 80 - description: "Minimum coverage percentage to aim for (default: 80)" - required: false - type: number - target_package: - description: Specific package to focus on (e.g., com.github.copilot.sdk.json, com.github.copilot.sdk.events, or leave empty for auto-select) - required: false - type: string - -permissions: {} - -concurrency: - group: "gh-aw-${{ github.workflow }}" - -run-name: "Increase Test Coverage" - -jobs: - activation: - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - comment_id: "" - comment_repo: "" - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_WORKFLOW_FILE: "increase-test-coverage.lock.yml" - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); - await main(); - - agent: - needs: activation - runs-on: ubuntu-latest - permissions: - contents: read - issues: read - pull-requests: read - concurrency: - group: "gh-aw-copilot-${{ github.workflow }}" - 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_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json - outputs: - checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} - has_patch: ${{ steps.collect_output.outputs.has_patch }} - model: ${{ steps.generate_aw_info.outputs.model }} - output: ${{ steps.collect_output.outputs.output }} - output_types: ${{ steps.collect_output.outputs.output_types }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Checkout .github and .agents folders - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - with: - sparse-checkout: | - .github - .agents - depth: 1 - persist-credentials: false - - name: Create gh-aw temp directory - run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Checkout PR branch - id: checkout-pr - if: | - github.event.pull_request - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.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('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); - await main(); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 - - name: Install awf binary - run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.13.12 - - name: Determine automatic lockdown mode for GitHub MCP server - id: determine-automatic-lockdown - env: - TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - if: env.TOKEN_CHECK != '' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); - - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.13.12 ghcr.io/github/gh-aw-firewall/squid:0.13.12 ghcr.io/github/gh-aw-mcpg:v0.0.113 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine - - name: Write Safe Outputs Config - run: | - mkdir -p /opt/gh-aw/safeoutputs - mkdir -p /tmp/gh-aw/safeoutputs - mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' - {"add_comment":{"max":1},"create_pull_request":{},"missing_data":{},"missing_tool":{},"noop":{"max":1}} - EOF - cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' - [ - { - "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 1 comment(s) can be added.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "body": { - "description": "The comment text in Markdown format. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation.", - "type": "string" - }, - "item_number": { - "description": "The issue, pull request, or discussion number to comment on. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123). If omitted, the tool will attempt to resolve the target from the current workflow context (triggering issue, PR, or discussion).", - "type": "number" - } - }, - "required": [ - "body" - ], - "type": "object" - }, - "name": "add_comment" - }, - { - "description": "Create a new GitHub pull request to propose code changes. Use this after making file edits to submit them for review and merging. The PR will be created from the current branch with your committed changes. For code review comments on an existing PR, use create_pull_request_review_comment instead. CONSTRAINTS: Maximum 1 pull request(s) can be created.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "body": { - "description": "Detailed PR description in Markdown. Include what changes were made, why, testing notes, and any breaking changes. Do NOT repeat the title as a heading.", - "type": "string" - }, - "branch": { - "description": "Source branch name containing the changes. If omitted, uses the current working branch.", - "type": "string" - }, - "labels": { - "description": "Labels to categorize the PR (e.g., 'enhancement', 'bugfix'). Labels must exist in the repository.", - "items": { - "type": "string" - }, - "type": "array" - }, - "title": { - "description": "Concise PR title describing the changes. Follow repository conventions (e.g., conventional commits). The title appears as the main heading.", - "type": "string" - } - }, - "required": [ - "title", - "body" - ], - "type": "object" - }, - "name": "create_pull_request" - }, - { - "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "alternatives": { - "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", - "type": "string" - }, - "reason": { - "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", - "type": "string" - }, - "tool": { - "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", - "type": "string" - } - }, - "required": [ - "reason" - ], - "type": "object" - }, - "name": "missing_tool" - }, - { - "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "message": { - "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", - "type": "string" - } - }, - "required": [ - "message" - ], - "type": "object" - }, - "name": "noop" - }, - { - "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "alternatives": { - "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", - "type": "string" - }, - "context": { - "description": "Additional context about the missing data or where it should come from (max 256 characters).", - "type": "string" - }, - "data_type": { - "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", - "type": "string" - }, - "reason": { - "description": "Explanation of why this data is needed to complete the task (max 256 characters).", - "type": "string" - } - }, - "required": [], - "type": "object" - }, - "name": "missing_data" - } - ] - EOF - cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' - { - "add_comment": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "item_number": { - "issueOrPRNumber": true - } - } - }, - "create_pull_request": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "branch": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "labels": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "title": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "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 - } - } - } - } - EOF - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - API_KEY="" - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - PORT=3001 - - # Register API key as secret to mask it from logs - echo "::add-mask::${API_KEY}" - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash /opt/gh-aw/actions/start_safe_outputs_server.sh - - - name: Start MCP gateway - id: start-mcp-gateway - env: - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - set -eo pipefail - mkdir -p /tmp/gh-aw/mcp-config - - # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" - export MCP_GATEWAY_DOMAIN="host.docker.internal" - MCP_GATEWAY_API_KEY="" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - export MCP_GATEWAY_API_KEY - export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" - mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" - export DEBUG="*" - - # Register API key as secret to mask it from logs - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -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_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_LOCKDOWN -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 GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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 ghcr.io/github/gh-aw-mcpg:v0.0.113' - - mkdir -p /home/runner/.copilot - cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh - { - "mcpServers": { - "github": { - "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v0.30.3", - "env": { - "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", - "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "repos,pull_requests,issues" - } - }, - "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" - } - } - }, - "gateway": { - "port": $MCP_GATEWAY_PORT, - "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", - "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" - } - } - MCPCONFIG_EOF - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "copilot", - engine_name: "GitHub Copilot CLI", - model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", - version: "", - agent_version: "0.0.405", - cli_version: "v0.42.17", - workflow_name: "Increase Test Coverage", - experimental: false, - supports_tools_allowlist: true, - supports_http_transport: true, - run_id: context.runId, - run_number: context.runNumber, - run_attempt: process.env.GITHUB_RUN_ATTEMPT, - repository: context.repo.owner + '/' + context.repo.repo, - ref: context.ref, - sha: context.sha, - actor: context.actor, - event_name: context.eventName, - staged: false, - allowed_domains: ["defaults"], - firewall_enabled: true, - awf_version: "v0.13.12", - awmg_version: "v0.0.113", - steps: { - firewall: "squid" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/aw_info.json'; - fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); - console.log('Generated aw_info.json at:', tmpPath); - console.log(JSON.stringify(awInfo, null, 2)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); - await generateWorkflowOverview(core); - - name: Create prompt with built-in context - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - run: | - bash /opt/gh-aw/actions/create_prompt_first.sh - cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" - - PROMPT_EOF - cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" - cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - GitHub API Access Instructions - - The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. - - - To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. - - Discover available tools from the safeoutputs MCP server. - - **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. - - **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. - - - - The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ - {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - {{#runtime-import .github/workflows/increase-test-coverage.md}} - PROMPT_EOF - - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - with: - script: | - const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); - - // Call the substitution function - return await substitutePlaceholders({ - file: process.env.GH_AW_PROMPT, - substitutions: { - GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, - 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 - } - }); - - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); - await main(); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/print_prompt_summary.sh - - name: Execute GitHub Copilot CLI - id: agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 30 - run: | - set -o pipefail - sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.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 --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.13.12 --skip-pull \ - -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' \ - 2>&1 | tee /tmp/gh-aw/agent-stdio.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json - GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_WORKSPACE: ${{ github.workspace }} - XDG_CONFIG_HOME: /home/runner - - name: Copy Copilot session state files to logs - if: always() - continue-on-error: true - run: | - # Copy Copilot session state files to logs folder for artifact collection - # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them - SESSION_STATE_DIR="$HOME/.copilot/session-state" - LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" - - if [ -d "$SESSION_STATE_DIR" ]; then - echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" - mkdir -p "$LOGS_DIR" - cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true - echo "Session state files copied successfully" - else - echo "No session-state directory found at $SESSION_STATE_DIR" - fi - - 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 /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" - - name: Redact secrets in logs - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); - await main(); - env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - 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: Upload Safe Outputs - if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: safe-output - path: ${{ env.GH_AW_SAFE_OUTPUTS }} - if-no-files-found: warn - - name: Ingest agent output - id: collect_output - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.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" - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_API_URL: ${{ github.api_url }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); - await main(); - - name: Upload sanitized agent output - if: always() && env.GH_AW_AGENT_OUTPUT - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: agent-output - path: ${{ env.GH_AW_AGENT_OUTPUT }} - if-no-files-found: warn - - name: Upload engine output files - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: agent_outputs - path: | - /tmp/gh-aw/sandbox/agent/logs/ - /tmp/gh-aw/redacted-urls.log - if-no-files-found: ignore - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); - await main(); - - name: Parse MCP gateway logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); - await main(); - - name: Print firewall logs - if: always() - continue-on-error: true - env: - AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - - name: Upload agent artifacts - if: always() - continue-on-error: true - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: agent-artifacts - path: | - /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/aw_info.json - /tmp/gh-aw/mcp-logs/ - /tmp/gh-aw/sandbox/firewall/logs/ - /tmp/gh-aw/agent-stdio.log - /tmp/gh-aw/agent/ - /tmp/gh-aw/aw.patch - if-no-files-found: ignore - - conclusion: - needs: - - activation - - agent - - detection - - safe_outputs - if: (always()) && (needs.agent.result != 'skipped') - runs-on: ubuntu-slim - permissions: - contents: read - discussions: write - issues: write - pull-requests: write - outputs: - 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 - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Debug job inputs - env: - COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - AGENT_CONCLUSION: ${{ needs.agent.result }} - run: | - echo "Comment ID: $COMMENT_ID" - echo "Comment Repo: $COMMENT_REPO" - echo "Agent Output Types: $AGENT_OUTPUT_TYPES" - echo "Agent Conclusion: $AGENT_CONCLUSION" - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/safeoutputs/ - - name: Setup agent output environment variable - run: | - mkdir -p /tmp/gh-aw/safeoutputs/ - find "/tmp/gh-aw/safeoutputs/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - - name: Process No-Op Messages - id: noop - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_NOOP_MAX: 1 - GH_AW_WORKFLOW_NAME: "Increase Test Coverage" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/noop.cjs'); - await main(); - - name: Record Missing Tool - id: missing_tool - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Increase Test Coverage" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); - await main(); - - name: Handle Agent Failure - id: handle_agent_failure - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Increase Test Coverage" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} - GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); - await main(); - - name: Handle No-Op Message - id: handle_noop_message - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Increase Test Coverage" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); - await main(); - - name: Handle Create Pull Request Error - id: handle_create_pr_error - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Increase Test Coverage" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_create_pr_error.cjs'); - await main(); - - name: Update reaction comment with completion status - id: conclusion - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_WORKFLOW_NAME: "Increase Test Coverage" - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/notify_comment_error.cjs'); - await main(); - - detection: - needs: agent - if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' - runs-on: ubuntu-latest - permissions: {} - concurrency: - group: "gh-aw-copilot-${{ github.workflow }}" - timeout-minutes: 10 - outputs: - success: ${{ steps.parse_results.outputs.success }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Download agent artifacts - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-artifacts - path: /tmp/gh-aw/threat-detection/ - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/threat-detection/ - - name: Echo agent output types - env: - AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - run: | - echo "Agent output-types: $AGENT_OUTPUT_TYPES" - - name: Setup threat detection - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - WORKFLOW_NAME: "Increase Test Coverage" - WORKFLOW_DESCRIPTION: "Automated workflow to identify untested code and add comprehensive tests to increase coverage.\nGroups related tests sensibly and creates PRs for human review." - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); - await main(); - - name: Ensure threat-detection directory and log - run: | - mkdir -p /tmp/gh-aw/threat-detection - touch /tmp/gh-aw/threat-detection/detection.log - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 - - name: Execute GitHub Copilot CLI - id: agentic_execution - # Copilot CLI tool arguments (sorted): - # --allow-tool shell(cat) - # --allow-tool shell(grep) - # --allow-tool shell(head) - # --allow-tool shell(jq) - # --allow-tool shell(ls) - # --allow-tool shell(tail) - # --allow-tool shell(wc) - timeout-minutes: 20 - run: | - set -o pipefail - COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" - mkdir -p /tmp/ - mkdir -p /tmp/gh-aw/ - mkdir -p /tmp/gh-aw/agent/ - mkdir -p /tmp/gh-aw/sandbox/agent/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_WORKSPACE: ${{ github.workspace }} - XDG_CONFIG_HOME: /home/runner - - name: Parse threat detection results - id: parse_results - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - - name: Upload threat detection log - if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: threat-detection.log - path: /tmp/gh-aw/threat-detection/detection.log - if-no-files-found: ignore - - safe_outputs: - needs: - - activation - - agent - - detection - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') - runs-on: ubuntu-slim - permissions: - contents: write - discussions: write - issues: write - pull-requests: write - timeout-minutes: 15 - env: - GH_AW_ENGINE_ID: "copilot" - GH_AW_WORKFLOW_ID: "increase-test-coverage" - GH_AW_WORKFLOW_NAME: "Increase Test Coverage" - outputs: - create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} - create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} - 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 }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/safeoutputs/ - - name: Setup agent output environment variable - run: | - mkdir -p /tmp/gh-aw/safeoutputs/ - find "/tmp/gh-aw/safeoutputs/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - - name: Download patch artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-artifacts - path: /tmp/gh-aw/ - - name: Checkout repository - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - with: - token: ${{ github.token }} - persist-credentials: false - fetch-depth: 1 - - name: Configure Git credentials - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - GIT_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Process Safe Outputs - id: process_safe_outputs - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":false,\"max\":1,\"max_patch_size\":1024},\"missing_data\":{},\"missing_tool\":{}}" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); - await main(); - diff --git a/.github/workflows/increase-test-coverage.md b/.github/workflows/increase-test-coverage.md deleted file mode 100644 index 0a286030a..000000000 --- a/.github/workflows/increase-test-coverage.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -name: Increase Test Coverage -description: | - Automated workflow to identify untested code and add comprehensive tests to increase coverage. - Groups related tests sensibly and creates PRs for human review. - -on: - workflow_dispatch: - inputs: - target_package: - description: 'Specific package to focus on (e.g., com.github.copilot.sdk.json, com.github.copilot.sdk.events, or leave empty for auto-select)' - required: false - type: string - min_coverage_threshold: - description: 'Minimum coverage percentage to aim for (default: 80)' - required: false - type: number - default: 80 - -permissions: - contents: read - pull-requests: read - issues: read - -tools: - github: - toolsets: - - repos - - pull_requests - - issues - edit: - -safe-outputs: - create-pull-request: - draft: false - add-comment: {} - -timeout-minutes: 30 - ---- - -# Test Coverage Enhancement Agent - -You are a test coverage expert for the `copilot-sdk-java` repository. Your mission is to identify code that lacks adequate test coverage and create comprehensive, high-quality tests. - -## Your Task - -1. **Analyze Current Coverage** - - Run `mvn clean test jacoco:report` to generate coverage reports - - Examine the JaCoCo report at `target/site/jacoco-coverage/index.html` - - Identify files and packages with low coverage (below {{ inputs.min_coverage_threshold }}%) - - Look at the detailed coverage reports to find specific untested methods and branches - -2. **Prioritize Testing Work** - {% if inputs.target_package %} - - Focus specifically on the `{{ inputs.target_package }}` package as requested - {% else %} - - Prioritize core SDK classes in `com.github.copilot.sdk` package first - - Then focus on `com.github.copilot.sdk.json` and `com.github.copilot.sdk.events` packages - {% endif %} - - Focus on: - - Public API methods that aren't tested - - Error handling paths (exception cases) - - Edge cases and boundary conditions - - Branch coverage (if/else, switch statements) - - Complex methods with low cyclomatic complexity coverage - -3. **Group Tests Logically** - - Group related functionality together (e.g., all tests for error handling in a single class) - - Create test classes that mirror the structure of source classes - - Follow existing test patterns in the repository (see `src/test/java` for examples) - - Tests should be in the same package as the code they test - -4. **Write High-Quality Tests** - - Follow the existing test patterns in the repository (examine existing test files) - - Use JUnit 5 (already configured in the project) - - Follow the E2E test pattern using `E2ETestContext` when testing Copilot client/session functionality - - For unit tests, use standard JUnit assertions - - Test both success and failure paths - - Include descriptive test names that explain what is being tested - - Add comments only when necessary to explain complex test scenarios - - Ensure tests are deterministic and don't depend on external state - -5. **Build Commands** (from repository instructions) - - Build and test: `mvn clean verify` - - Run specific test: `mvn test -Dtest=ClassName#methodName` - - Format code: `mvn spotless:apply` (REQUIRED before committing) - - Check format: `mvn spotless:check` - -6. **Verify Your Changes** - - Run `mvn spotless:apply` to format the code - - Run the new tests: `mvn test -Dtest=YourNewTestClass` - - Run all tests to ensure nothing broke: `mvn clean verify` - - Generate new coverage report: `mvn jacoco:report` - - Verify coverage increased in target areas - -7. **Create a Pull Request** - - Use descriptive PR title: "Add tests for [functionality/package] to increase coverage" - - In the PR body, include: - - Which code/package you added tests for - - Coverage before and after (percentage and specific methods covered) - - Number of new test cases added - - Any important testing patterns or decisions - - Use the `create-pull-request` safe output with: - - `title`: Clear description of test additions - - `body`: Detailed summary as described above - - `head`: Create branch named `test-coverage-[package-or-feature]` - - `base`: Target the default branch - - `draft`: Set to false so it's ready for review - -## Important Guidelines - -- **Minimal Changes**: Only add tests - do NOT modify production code unless absolutely necessary for testability -- **Follow Repository Patterns**: Study existing tests before writing new ones - - E2E tests use `E2ETestContext.create()` and snapshot-based testing - - Unit tests follow standard JUnit patterns - - Test file naming: `*Test.java` (e.g., `CopilotClientTest.java`) -- **Code Style**: This project uses Spotless for formatting - ALWAYS run `mvn spotless:apply` before committing -- **Test Quality**: Tests should be clear, maintainable, and actually test the intended behavior -- **Branch Names**: Use descriptive names like `test-coverage-json-package` or `test-coverage-error-handling` -- **Safety**: Never commit files from `target/`, `node_modules/`, or other build artifacts - -## Example Workflow - -```bash -# 1. Generate coverage report -mvn clean test jacoco:report - -# 2. Examine coverage (use view/grep tools to read the HTML report) -# Look at target/site/jacoco-coverage/index.html and package-specific reports - -# 3. Identify low-coverage files -# Example: SessionConfig.java shows 65% coverage, missing tests for edge cases - -# 4. Create test file (if it doesn't exist) or add to existing test -# Follow pattern: src/test/java/com/github/copilot/sdk/json/SessionConfigTest.java - -# 5. Write tests following existing patterns - -# 6. Format and verify -mvn spotless:apply -mvn test -Dtest=SessionConfigTest -mvn clean verify - -# 7. Check new coverage -mvn jacoco:report - -# 8. Create PR using safe output -``` - -## Repository Context - -- **Language**: Java 17+ -- **Build Tool**: Maven -- **Test Framework**: JUnit 5 -- **Coverage Tool**: JaCoCo -- **Code Formatter**: Spotless (Eclipse style, 4-space indent) -- **Current Coverage**: ~66% (aim for {{ inputs.min_coverage_threshold }}%+ overall) - -## Key Packages to Focus On - -1. **com.github.copilot.sdk** - Core SDK classes (CopilotClient, CopilotSession, JsonRpcClient) -2. **com.github.copilot.sdk.json** - DTOs and request/response types -3. **com.github.copilot.sdk.events** - Event types and handlers - -## Remember - -- Quality over quantity - a few well-written tests are better than many shallow tests -- Test behavior, not implementation details -- Focus on increasing coverage of important code paths -- Make the PR reviewable by grouping related tests together -- Always run spotless:apply before creating the PR - -Good luck! Your work will help ensure the SDK is reliable and maintainable. diff --git a/.github/workflows/upstream-sync.lock.yml b/.github/workflows/upstream-sync.lock.yml deleted file mode 100644 index b8005dbd6..000000000 --- a/.github/workflows/upstream-sync.lock.yml +++ /dev/null @@ -1,1066 +0,0 @@ -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# This file was automatically generated by gh-aw (v0.42.17). DO NOT EDIT. -# -# To update this file, edit the corresponding .md file and run: -# gh aw compile -# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md -# -# Weekly upstream sync workflow. Checks for new commits in the official -# Copilot SDK (github/copilot-sdk), analyzes changes, ports them to the -# Java SDK, runs tests, and creates a pull request with the ported changes. -# -# frontmatter-hash: e9e6426a74c083476677690bbc5ce163344277d23cb3d49207075ea1c49c6757 - -name: "Upstream Sync Agent" -"on": - schedule: - - cron: "47 13 * * 4" - # Friendly format: weekly (scattered) - workflow_dispatch: - -permissions: {} - -concurrency: - group: "gh-aw-${{ github.workflow }}" - -run-name: "Upstream Sync Agent" - -jobs: - activation: - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - comment_id: "" - comment_repo: "" - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_WORKFLOW_FILE: "upstream-sync.lock.yml" - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); - await main(); - - agent: - needs: activation - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - issues: read - pull-requests: read - concurrency: - group: "gh-aw-copilot-${{ github.workflow }}" - 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_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json - outputs: - checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} - has_patch: ${{ steps.collect_output.outputs.has_patch }} - model: ${{ steps.generate_aw_info.outputs.model }} - output: ${{ steps.collect_output.outputs.output }} - output_types: ${{ steps.collect_output.outputs.output_types }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Checkout .github and .agents folders - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - with: - sparse-checkout: | - .github - .agents - depth: 1 - persist-credentials: false - - name: Create gh-aw temp directory - run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh - - name: Set up Java 17 - uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4 - with: - distribution: temurin - java-version: "17" - - name: Set up Node.js 22 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: "22" - - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Checkout PR branch - id: checkout-pr - if: | - github.event.pull_request - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.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('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); - await main(); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 - - name: Install awf binary - run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.13.12 - - name: Determine automatic lockdown mode for GitHub MCP server - id: determine-automatic-lockdown - env: - TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - if: env.TOKEN_CHECK != '' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); - - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.13.12 ghcr.io/github/gh-aw-firewall/squid:0.13.12 ghcr.io/github/gh-aw-mcpg:v0.0.113 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine - - name: Write Safe Outputs Config - run: | - mkdir -p /opt/gh-aw/safeoutputs - mkdir -p /tmp/gh-aw/safeoutputs - mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' - {"create_pull_request":{},"missing_data":{},"missing_tool":{},"noop":{"max":1}} - EOF - cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' - [ - { - "description": "Create a new GitHub pull request to propose code changes. Use this after making file edits to submit them for review and merging. The PR will be created from the current branch with your committed changes. For code review comments on an existing PR, use create_pull_request_review_comment instead. CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[upstream-sync] \". Labels [upstream-sync] will be automatically added. PRs will be created as drafts.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "body": { - "description": "Detailed PR description in Markdown. Include what changes were made, why, testing notes, and any breaking changes. Do NOT repeat the title as a heading.", - "type": "string" - }, - "branch": { - "description": "Source branch name containing the changes. If omitted, uses the current working branch.", - "type": "string" - }, - "labels": { - "description": "Labels to categorize the PR (e.g., 'enhancement', 'bugfix'). Labels must exist in the repository.", - "items": { - "type": "string" - }, - "type": "array" - }, - "title": { - "description": "Concise PR title describing the changes. Follow repository conventions (e.g., conventional commits). The title appears as the main heading.", - "type": "string" - } - }, - "required": [ - "title", - "body" - ], - "type": "object" - }, - "name": "create_pull_request" - }, - { - "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "alternatives": { - "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", - "type": "string" - }, - "reason": { - "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", - "type": "string" - }, - "tool": { - "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", - "type": "string" - } - }, - "required": [ - "reason" - ], - "type": "object" - }, - "name": "missing_tool" - }, - { - "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "message": { - "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", - "type": "string" - } - }, - "required": [ - "message" - ], - "type": "object" - }, - "name": "noop" - }, - { - "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "alternatives": { - "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", - "type": "string" - }, - "context": { - "description": "Additional context about the missing data or where it should come from (max 256 characters).", - "type": "string" - }, - "data_type": { - "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", - "type": "string" - }, - "reason": { - "description": "Explanation of why this data is needed to complete the task (max 256 characters).", - "type": "string" - } - }, - "required": [], - "type": "object" - }, - "name": "missing_data" - } - ] - EOF - cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' - { - "create_pull_request": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "branch": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "labels": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "title": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "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 - } - } - } - } - EOF - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - API_KEY="" - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - PORT=3001 - - # Register API key as secret to mask it from logs - echo "::add-mask::${API_KEY}" - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash /opt/gh-aw/actions/start_safe_outputs_server.sh - - - name: Start MCP gateway - id: start-mcp-gateway - env: - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - set -eo pipefail - mkdir -p /tmp/gh-aw/mcp-config - - # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" - export MCP_GATEWAY_DOMAIN="host.docker.internal" - MCP_GATEWAY_API_KEY="" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - export MCP_GATEWAY_API_KEY - export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" - mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" - export DEBUG="*" - - # Register API key as secret to mask it from logs - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -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_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_LOCKDOWN -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 GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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 ghcr.io/github/gh-aw-mcpg:v0.0.113' - - mkdir -p /home/runner/.copilot - cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh - { - "mcpServers": { - "github": { - "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v0.30.3", - "env": { - "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", - "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" - } - }, - "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" - } - } - }, - "gateway": { - "port": $MCP_GATEWAY_PORT, - "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", - "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" - } - } - MCPCONFIG_EOF - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "copilot", - engine_name: "GitHub Copilot CLI", - model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", - version: "", - agent_version: "0.0.405", - cli_version: "v0.42.17", - workflow_name: "Upstream Sync Agent", - experimental: false, - supports_tools_allowlist: true, - supports_http_transport: true, - run_id: context.runId, - run_number: context.runNumber, - run_attempt: process.env.GITHUB_RUN_ATTEMPT, - repository: context.repo.owner + '/' + context.repo.repo, - ref: context.ref, - sha: context.sha, - actor: context.actor, - event_name: context.eventName, - staged: false, - allowed_domains: ["defaults","java","node"], - firewall_enabled: true, - awf_version: "v0.13.12", - awmg_version: "v0.0.113", - steps: { - firewall: "squid" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/aw_info.json'; - fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); - console.log('Generated aw_info.json at:', tmpPath); - console.log(JSON.stringify(awInfo, null, 2)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); - await generateWorkflowOverview(core); - - name: Create prompt with built-in context - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - run: | - bash /opt/gh-aw/actions/create_prompt_first.sh - cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" - - PROMPT_EOF - cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" - cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - GitHub API Access Instructions - - The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. - - - To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. - - Discover available tools from the safeoutputs MCP server. - - **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. - - **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. - - - - The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ - {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - {{#runtime-import .github/workflows/upstream-sync.md}} - PROMPT_EOF - - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - with: - script: | - const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); - - // Call the substitution function - return await substitutePlaceholders({ - file: process.env.GH_AW_PROMPT, - substitutions: { - GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, - 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 - } - }); - - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); - await main(); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/print_prompt_summary.sh - - name: Execute GitHub Copilot CLI - id: agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 - run: | - set -o pipefail - sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.jsr.io,adoptium.net,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.azul.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,dlcdn.apache.org,download.eclipse.org,download.java.net,download.oracle.com,get.pnpm.io,github.com,gradle.org,host.docker.internal,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven.apache.org,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,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,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,s.symcb.com,s.symcd.com,security.ubuntu.com,services.gradle.org,skimdb.npmjs.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.java.com,www.npmjs.com,www.npmjs.org,yarnpkg.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.13.12 --skip-pull \ - -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' \ - 2>&1 | tee /tmp/gh-aw/agent-stdio.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json - GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_WORKSPACE: ${{ github.workspace }} - XDG_CONFIG_HOME: /home/runner - - name: Copy Copilot session state files to logs - if: always() - continue-on-error: true - run: | - # Copy Copilot session state files to logs folder for artifact collection - # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them - SESSION_STATE_DIR="$HOME/.copilot/session-state" - LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" - - if [ -d "$SESSION_STATE_DIR" ]; then - echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" - mkdir -p "$LOGS_DIR" - cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true - echo "Session state files copied successfully" - else - echo "No session-state directory found at $SESSION_STATE_DIR" - fi - - 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 /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" - - name: Redact secrets in logs - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); - await main(); - env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - 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: Upload Safe Outputs - if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: safe-output - path: ${{ env.GH_AW_SAFE_OUTPUTS }} - if-no-files-found: warn - - name: Ingest agent output - id: collect_output - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "*.jsr.io,adoptium.net,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.azul.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,dlcdn.apache.org,download.eclipse.org,download.java.net,download.oracle.com,get.pnpm.io,github.com,gradle.org,host.docker.internal,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven.apache.org,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,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,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,s.symcb.com,s.symcd.com,security.ubuntu.com,services.gradle.org,skimdb.npmjs.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.java.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_API_URL: ${{ github.api_url }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); - await main(); - - name: Upload sanitized agent output - if: always() && env.GH_AW_AGENT_OUTPUT - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: agent-output - path: ${{ env.GH_AW_AGENT_OUTPUT }} - if-no-files-found: warn - - name: Upload engine output files - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: agent_outputs - path: | - /tmp/gh-aw/sandbox/agent/logs/ - /tmp/gh-aw/redacted-urls.log - if-no-files-found: ignore - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); - await main(); - - name: Parse MCP gateway logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); - await main(); - - name: Print firewall logs - if: always() - continue-on-error: true - env: - AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - - name: Upload agent artifacts - if: always() - continue-on-error: true - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: agent-artifacts - path: | - /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/aw_info.json - /tmp/gh-aw/mcp-logs/ - /tmp/gh-aw/sandbox/firewall/logs/ - /tmp/gh-aw/agent-stdio.log - /tmp/gh-aw/agent/ - /tmp/gh-aw/aw.patch - if-no-files-found: ignore - - conclusion: - needs: - - activation - - agent - - detection - - safe_outputs - if: (always()) && (needs.agent.result != 'skipped') - runs-on: ubuntu-slim - permissions: - contents: read - discussions: write - issues: write - pull-requests: write - outputs: - 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 - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Debug job inputs - env: - COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - AGENT_CONCLUSION: ${{ needs.agent.result }} - run: | - echo "Comment ID: $COMMENT_ID" - echo "Comment Repo: $COMMENT_REPO" - echo "Agent Output Types: $AGENT_OUTPUT_TYPES" - echo "Agent Conclusion: $AGENT_CONCLUSION" - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/safeoutputs/ - - name: Setup agent output environment variable - run: | - mkdir -p /tmp/gh-aw/safeoutputs/ - find "/tmp/gh-aw/safeoutputs/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - - name: Process No-Op Messages - id: noop - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_NOOP_MAX: 1 - GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/noop.cjs'); - await main(); - - name: Record Missing Tool - id: missing_tool - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); - await main(); - - name: Handle Agent Failure - id: handle_agent_failure - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} - GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); - await main(); - - name: Handle No-Op Message - id: handle_noop_message - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); - await main(); - - name: Handle Create Pull Request Error - id: handle_create_pr_error - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_create_pr_error.cjs'); - await main(); - - name: Update reaction comment with completion status - id: conclusion - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/notify_comment_error.cjs'); - await main(); - - detection: - needs: agent - if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' - runs-on: ubuntu-latest - permissions: {} - concurrency: - group: "gh-aw-copilot-${{ github.workflow }}" - timeout-minutes: 10 - outputs: - success: ${{ steps.parse_results.outputs.success }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Download agent artifacts - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-artifacts - path: /tmp/gh-aw/threat-detection/ - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/threat-detection/ - - name: Echo agent output types - env: - AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - run: | - echo "Agent output-types: $AGENT_OUTPUT_TYPES" - - name: Setup threat detection - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - WORKFLOW_NAME: "Upstream Sync Agent" - WORKFLOW_DESCRIPTION: "Weekly upstream sync workflow. Checks for new commits in the official\nCopilot SDK (github/copilot-sdk), analyzes changes, ports them to the\nJava SDK, runs tests, and creates a pull request with the ported changes." - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); - await main(); - - name: Ensure threat-detection directory and log - run: | - mkdir -p /tmp/gh-aw/threat-detection - touch /tmp/gh-aw/threat-detection/detection.log - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 - - name: Execute GitHub Copilot CLI - id: agentic_execution - # Copilot CLI tool arguments (sorted): - # --allow-tool shell(cat) - # --allow-tool shell(grep) - # --allow-tool shell(head) - # --allow-tool shell(jq) - # --allow-tool shell(ls) - # --allow-tool shell(tail) - # --allow-tool shell(wc) - timeout-minutes: 20 - run: | - set -o pipefail - COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" - mkdir -p /tmp/ - mkdir -p /tmp/gh-aw/ - mkdir -p /tmp/gh-aw/agent/ - mkdir -p /tmp/gh-aw/sandbox/agent/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_WORKSPACE: ${{ github.workspace }} - XDG_CONFIG_HOME: /home/runner - - name: Parse threat detection results - id: parse_results - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - - name: Upload threat detection log - if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: threat-detection.log - path: /tmp/gh-aw/threat-detection/detection.log - if-no-files-found: ignore - - safe_outputs: - needs: - - activation - - agent - - detection - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') - runs-on: ubuntu-slim - permissions: - contents: write - issues: write - pull-requests: write - timeout-minutes: 15 - env: - GH_AW_ENGINE_ID: "copilot" - GH_AW_WORKFLOW_ID: "upstream-sync" - GH_AW_WORKFLOW_NAME: "Upstream Sync Agent" - outputs: - create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} - create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} - 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 }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/safeoutputs/ - - name: Setup agent output environment variable - run: | - mkdir -p /tmp/gh-aw/safeoutputs/ - find "/tmp/gh-aw/safeoutputs/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - - name: Download patch artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-artifacts - path: /tmp/gh-aw/ - - name: Checkout repository - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - with: - token: ${{ github.token }} - persist-credentials: false - fetch-depth: 1 - - name: Configure Git credentials - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - GIT_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Process Safe Outputs - id: process_safe_outputs - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":true,\"labels\":[\"upstream-sync\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[upstream-sync] \"},\"missing_data\":{},\"missing_tool\":{}}" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); - await main(); - diff --git a/.github/workflows/upstream-sync.md b/.github/workflows/upstream-sync.md deleted file mode 100644 index 1551132f1..000000000 --- a/.github/workflows/upstream-sync.md +++ /dev/null @@ -1,234 +0,0 @@ ---- -description: | - Weekly upstream sync workflow. Checks for new commits in the official - Copilot SDK (github/copilot-sdk), analyzes changes, ports them to the - Java SDK, runs tests, and creates a pull request with the ported changes. - -on: - schedule: weekly - workflow_dispatch: - -permissions: - contents: read - actions: read - issues: read - pull-requests: read - -network: - allowed: - - defaults - - java - - node - -steps: - - name: Set up Java 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - name: Set up Node.js 22 - uses: actions/setup-node@v4 - with: - node-version: '22' - -tools: - github: - toolsets: [default] - -safe-outputs: - create-pull-request: - title-prefix: "[upstream-sync] " - labels: [upstream-sync] - draft: true - noop: ---- - -# Upstream Sync Agent - -You are an expert Java developer. Your job is to check the official Copilot SDK ([github/copilot-sdk](https://github.com/github/copilot-sdk)) for new changes and port them to this Java SDK. - -## ⚠️ CRITICAL: Java SDK Design Takes Priority - -The current design and architecture of the Java SDK is the priority. When porting: - -1. **Adapt, don't copy** — Translate features to fit Java patterns, naming conventions, and architecture -2. **Preserve Java idioms** — The SDK should feel natural to Java developers, not like a C# port -3. **Maintain consistency** — New code must match the existing codebase style -4. **Evaluate before porting** — Not every upstream change applies; some may conflict with Java SDK design - -Before making any changes, **read and understand the existing Java SDK implementation**. - -## Workflow - -### Phase 1: Check for upstream changes - -Run this to detect new commits: - -```bash -LAST_MERGE=$(cat .lastmerge) -echo "Last merged commit: $LAST_MERGE" - -git clone --depth=200 https://github.com/github/copilot-sdk.git /tmp/upstream-sdk -cd /tmp/upstream-sdk - -UPSTREAM_HEAD=$(git rev-parse HEAD) -echo "Upstream HEAD: $UPSTREAM_HEAD" - -if [ "$LAST_MERGE" = "$UPSTREAM_HEAD" ]; then - echo "NO_CHANGES=true" -else - COMMIT_COUNT=$(git rev-list --count "$LAST_MERGE".."$UPSTREAM_HEAD") - echo "Found $COMMIT_COUNT new upstream commits" - git log --oneline "$LAST_MERGE".."$UPSTREAM_HEAD" -fi -``` - -**If there are NO new changes**: Call the `noop` safe output with a message like "No new upstream changes detected. The Java SDK is up to date with commit ``." and stop. - -### Phase 2: Analyze changes - -Examine the upstream diff grouped by area: - -```bash -cd /tmp/upstream-sdk -LAST_MERGE=$(cat $GITHUB_WORKSPACE/.lastmerge) - -echo "=== .NET Source (primary reference) ===" -git diff --stat "$LAST_MERGE"..HEAD -- dotnet/src/ - -echo "=== .NET Tests ===" -git diff --stat "$LAST_MERGE"..HEAD -- dotnet/test/ - -echo "=== Test Snapshots ===" -git diff --stat "$LAST_MERGE"..HEAD -- test/snapshots/ - -echo "=== Documentation ===" -git diff --stat "$LAST_MERGE"..HEAD -- docs/ - -echo "=== Protocol / Config ===" -git diff --stat "$LAST_MERGE"..HEAD -- sdk-protocol-version.json -``` - -For each area with changes, read the full diffs: - -```bash -git diff "$LAST_MERGE"..HEAD -- dotnet/src/ -git diff "$LAST_MERGE"..HEAD -- dotnet/test/ -git diff "$LAST_MERGE"..HEAD -- docs/ -``` - -### Phase 3: Plan the port - -For each upstream change, determine: - -- **New Features**: New methods, classes, or capabilities -- **Bug Fixes**: Corrections to existing functionality -- **API Changes**: Changes to public interfaces -- **Protocol Updates**: JSON-RPC message type changes -- **Test Updates**: New or modified test cases - -Use this mapping to locate the Java equivalents: - -| Upstream (.NET) | Java SDK Equivalent | -|---|---| -| `dotnet/src/Client.cs` | `src/main/java/com/github/copilot/sdk/CopilotClient.java` | -| `dotnet/src/Session.cs` | `src/main/java/com/github/copilot/sdk/CopilotSession.java` | -| `dotnet/src/Types.cs` | `src/main/java/com/github/copilot/sdk/types/*.java` | -| `dotnet/src/Generated/*.cs` | `src/main/java/com/github/copilot/sdk/types/*.java` | -| `dotnet/test/*.cs` | `src/test/java/com/github/copilot/sdk/*Test.java` | -| `docs/getting-started.md` | `README.md` and `src/site/markdown/*.md` | - -### Phase 4: Port changes - -Read the existing Java implementation before modifying anything. Apply these conventions: - -**Type mappings:** -- `string` → `String`, `Task` → `CompletableFuture`, `JsonElement` → `JsonNode` (Jackson) -- C# PascalCase → Java camelCase for methods/variables -- C# `async/await` → `CompletableFuture` -- C# properties → Java getters/setters or fluent setters -- C# nullable `?` → `@Nullable` or `Optional` - -**Code style:** -- 4-space indentation (enforced by Spotless with Eclipse formatter) -- Fluent setter pattern for configuration classes -- Jackson for JSON serialization (`ObjectMapper`, `@JsonProperty`) -- `@JsonInclude(JsonInclude.Include.NON_NULL)` on DTOs -- Public APIs require Javadoc (except `json` and `events` packages) - -**Commit incrementally** as you work: -```bash -git add -git commit -m "Port from upstream" -``` - -### Phase 5: Port tests - -For each new or modified test in `dotnet/test/`: - -1. Create or update the corresponding Java test class in `src/test/java/com/github/copilot/sdk/` -2. Follow existing test patterns (look at `PermissionsTest.java`, `HooksTest.java`) -3. Use `E2ETestContext` for tests requiring the test harness -4. If the test harness doesn't support new RPC methods yet, mark with `@Disabled("Requires test harness update")` - -### Phase 6: Update CLI version in README - -Check the CLI version: -```bash -copilot --version 2>/dev/null || echo "CLI not available in CI" -``` - -If the CLI version changed, update requirements in both `README.md` and `src/site/markdown/index.md`. - -### Phase 7: Update documentation - -For each new feature ported: -- **`README.md`** — Update if there are user-facing changes -- **`src/site/markdown/documentation.md`** — New basic usage patterns -- **`src/site/markdown/advanced.md`** — New advanced features -- **Javadoc** — All new/changed public APIs -- **`src/site/site.xml`** — Update if new pages added - -### Phase 8: Format and test - -```bash -mvn spotless:apply -mvn clean verify -``` - -If tests fail: -1. Read the error output carefully -2. Fix the issue in the Java code -3. Re-run `mvn clean verify` -4. Repeat until all tests pass - -### Phase 9: Finalize - -Update `.lastmerge` with the upstream HEAD commit: - -```bash -cd /tmp/upstream-sdk -UPSTREAM_HEAD=$(git rev-parse HEAD) -cd $GITHUB_WORKSPACE -echo "$UPSTREAM_HEAD" > .lastmerge -git add .lastmerge -git commit -m "Update .lastmerge to $(cat .lastmerge | cut -c1-12)" -``` - -### Phase 10: Create Pull Request - -Create a pull request with your changes using the `create-pull-request` safe output. The PR body should include: - -- **Title**: `Merge upstream SDK changes (YYYY-MM-DD)` -- **Summary**: Number of upstream commits analyzed with commit range -- **Changes ported**: Table of commit hash + description -- **Not ported**: List with reasons (if any) -- **Verification**: Test count, build status - -## Guidelines - -- **SECURITY**: Never commit secrets, tokens, or credentials -- Use `try-with-resources` for streams and readers -- Use `StandardCharsets.UTF_8` when creating InputStreamReader/OutputStreamWriter -- Prefer existing Java SDK abstractions over upstream .NET patterns -- When in doubt, match the style of surrounding Java code From 58c7858869706c118fbd684ae00db03b3a562144 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 18:47:12 -0500 Subject: [PATCH 035/147] feat: enhance create-sample-issue workflow to assign Copilot agent and update documentation --- .../workflows/.github/aw/actions-lock.json | 9 +++ .../workflows/create-sample-issue.lock.yml | 71 ++++++++++++++++--- .github/workflows/create-sample-issue.md | 8 ++- .github/workflows/weekly-upstream-sync.backup | 6 +- .github/workflows/weekly-upstream-sync.md | 2 +- 5 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/.github/aw/actions-lock.json diff --git a/.github/workflows/.github/aw/actions-lock.json b/.github/workflows/.github/aw/actions-lock.json new file mode 100644 index 000000000..708bdf24a --- /dev/null +++ b/.github/workflows/.github/aw/actions-lock.json @@ -0,0 +1,9 @@ +{ + "entries": { + "actions/github-script@v8": { + "repo": "actions/github-script", + "version": "v8", + "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" + } + } +} diff --git a/.github/workflows/create-sample-issue.lock.yml b/.github/workflows/create-sample-issue.lock.yml index 13678c0c3..0362ed616 100644 --- a/.github/workflows/create-sample-issue.lock.yml +++ b/.github/workflows/create-sample-issue.lock.yml @@ -21,7 +21,7 @@ # # Creates a sample test issue and assigns it to Copilot. # -# frontmatter-hash: 389d6be230745d9867d266207c5d2a00c790b059fa1fa9779e3242b02bade05c +# frontmatter-hash: 64751f386c7bb6b1f28418404d978f05905aa582cb69411471d5f6e92576efdc name: "Create Sample Issue" "on": @@ -150,12 +150,12 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' - {"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + {"assign_to_agent":{"default_agent":"copilot","target":"*"},"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' [ { - "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[test] \". Labels [test] will be automatically added. Assignees [copilot] will be automatically assigned.", + "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[test] \". Labels [test] will be automatically added.", "inputSchema": { "additionalProperties": false, "properties": { @@ -194,6 +194,34 @@ jobs: }, "name": "create_issue" }, + { + "description": "Assign the GitHub Copilot coding agent to work on an issue or pull request. The agent will analyze the issue/PR and attempt to implement a solution, creating a pull request when complete. Use this to delegate coding tasks to Copilot. Example usage: assign_to_agent(issue_number=123, agent=\"copilot\") or assign_to_agent(pull_number=456, agent=\"copilot\")", + "inputSchema": { + "additionalProperties": false, + "properties": { + "agent": { + "description": "Agent identifier to assign. Defaults to 'copilot' (the Copilot coding agent) if not specified.", + "type": "string" + }, + "issue_number": { + "description": "Issue number to assign the Copilot agent to. This is the numeric ID from the GitHub URL (e.g., 234 in github.com/owner/repo/issues/234). Can also be a temporary_id (e.g., 'aw_abc123def456') from an issue created earlier in the same workflow run. The issue should contain clear, actionable requirements. Either issue_number or pull_number must be provided, but not both.", + "type": [ + "number", + "string" + ] + }, + "pull_number": { + "description": "Pull request number to assign the Copilot agent to. This is the numeric ID from the GitHub URL (e.g., 456 in github.com/owner/repo/pull/456). Either issue_number or pull_number must be provided, but not both.", + "type": [ + "number", + "string" + ] + } + }, + "type": "object" + }, + "name": "assign_to_agent" + }, { "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", "inputSchema": { @@ -267,6 +295,23 @@ jobs: EOF cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' { + "assign_to_agent": { + "defaultMax": 1, + "fields": { + "agent": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "issue_number": { + "issueNumberOrTemporaryId": true + }, + "pull_number": { + "optionalPositiveInteger": true + } + }, + "customValidation": "requiresOneOf:issue_number,pull_number" + }, "create_issue": { "defaultMax": 1, "fields": { @@ -823,6 +868,8 @@ jobs: GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_ASSIGNMENT_ERRORS: ${{ needs.safe_outputs.outputs.assign_to_agent_assignment_errors }} + GH_AW_ASSIGNMENT_ERROR_COUNT: ${{ needs.safe_outputs.outputs.assign_to_agent_assignment_error_count }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -982,6 +1029,9 @@ jobs: GH_AW_WORKFLOW_ID: "create-sample-issue" GH_AW_WORKFLOW_NAME: "Create Sample Issue" outputs: + assign_to_agent_assigned: ${{ steps.assign_to_agent.outputs.assigned }} + assign_to_agent_assignment_error_count: ${{ steps.assign_to_agent.outputs.assignment_error_count }} + assign_to_agent_assignment_errors: ${{ steps.assign_to_agent.outputs.assignment_errors }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} @@ -1007,8 +1057,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"assignees\":[\"copilot\"],\"labels\":[\"test\"],\"max\":1,\"title_prefix\":\"[test] \"},\"missing_data\":{},\"missing_tool\":{}}" - GH_AW_ASSIGN_COPILOT: "true" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"labels\":[\"test\"],\"max\":1,\"title_prefix\":\"[test] \"},\"missing_data\":{},\"missing_tool\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1016,16 +1065,20 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Assign Copilot to created issues - if: steps.process_safe_outputs.outputs.issues_to_assign_copilot != '' + - name: Assign To Agent + id: assign_to_agent + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'assign_to_agent')) uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: - GH_AW_ISSUES_TO_ASSIGN_COPILOT: ${{ steps.process_safe_outputs.outputs.issues_to_assign_copilot }} + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_AGENT_DEFAULT: "copilot" + GH_AW_AGENT_TARGET: "*" + GH_AW_TEMPORARY_ID_MAP: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} with: github-token: ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/assign_copilot_to_created_issues.cjs'); + const { main } = require('/opt/gh-aw/actions/assign_to_agent.cjs'); await main(); diff --git a/.github/workflows/create-sample-issue.md b/.github/workflows/create-sample-issue.md index e5bc29f7f..d85459574 100644 --- a/.github/workflows/create-sample-issue.md +++ b/.github/workflows/create-sample-issue.md @@ -16,14 +16,16 @@ tools: safe-outputs: create-issue: title-prefix: "[test] " - assignees: [copilot] labels: [test] + assign-to-agent: + name: "copilot" + target: "*" --- # Create Sample Issue -Create a test issue in this repository assigned to Copilot. The issue should contain: +Create a test issue in this repository and then assign Copilot to it. The issue should contain: - **Title:** `Sample test issue` - **Body:** `This is an automated test issue created by the create-sample-issue workflow. Feel free to close it.` -| `.github/scripts/upstream-sync/` | Helper scripts used by the merge prompt | +After creating the issue, assign the `copilot` agent to it. diff --git a/.github/workflows/weekly-upstream-sync.backup b/.github/workflows/weekly-upstream-sync.backup index 84ef95fc5..0684fe622 100644 --- a/.github/workflows/weekly-upstream-sync.backup +++ b/.github/workflows/weekly-upstream-sync.backup @@ -56,7 +56,7 @@ jobs: - name: Close previous upstream-sync issues if: steps.check.outputs.has_changes == 'true' env: - GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} + GH_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN }} run: | # Find all open issues with the upstream-sync label OPEN_ISSUES=$(gh issue list \ @@ -79,7 +79,7 @@ jobs: - name: Close stale upstream-sync issues (no changes) if: steps.check.outputs.has_changes == 'false' env: - GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} + GH_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN }} run: | OPEN_ISSUES=$(gh issue list \ --repo "${{ github.repository }}" \ @@ -102,7 +102,7 @@ jobs: id: create-issue if: steps.check.outputs.has_changes == 'true' env: - GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} + GH_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN }} run: | COMMIT_COUNT="${{ steps.check.outputs.commit_count }}" LAST_MERGE="${{ steps.check.outputs.last_merge }}" diff --git a/.github/workflows/weekly-upstream-sync.md b/.github/workflows/weekly-upstream-sync.md index 15d18ee46..fbdd028b4 100644 --- a/.github/workflows/weekly-upstream-sync.md +++ b/.github/workflows/weekly-upstream-sync.md @@ -61,7 +61,7 @@ The workflow runs on a **weekly schedule** (every Monday at 10:00 UTC) and can a | Secret | Purpose | |---|---| -| `COPILOT_ISSUE_PR_AGENTIC_WORKFLOW` | PAT used as `GH_TOKEN` for `gh` CLI operations (issue create/close/comment). Required because the default `GITHUB_TOKEN` cannot assign issues to `copilot-swe-agent`. | +| `GH_AW_AGENT_TOKEN` | PAT used as `GH_TOKEN` for `gh` CLI operations (issue create/close/comment). Required because the default `GITHUB_TOKEN` cannot assign issues to `copilot-swe-agent`. | ## Workflow Steps From b905301a11532d08ae1485a9661ce45f35ef4047 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 18:53:04 -0500 Subject: [PATCH 036/147] fix name --- src/site/site.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/site.xml b/src/site/site.xml index 7ca83b462..4c09172f5 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -36,7 +36,7 @@ - + From e916d4600c0f9970dda5f361fd69c10bf72cf9fd Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 18:56:54 -0500 Subject: [PATCH 037/147] fix: remove redundant environment variable definitions in weekly upstream sync workflow --- .github/workflows/weekly-upstream-sync.backup | 178 ------------------ .github/workflows/weekly-upstream-sync.yml | 11 +- 2 files changed, 5 insertions(+), 184 deletions(-) delete mode 100644 .github/workflows/weekly-upstream-sync.backup diff --git a/.github/workflows/weekly-upstream-sync.backup b/.github/workflows/weekly-upstream-sync.backup deleted file mode 100644 index 0684fe622..000000000 --- a/.github/workflows/weekly-upstream-sync.backup +++ /dev/null @@ -1,178 +0,0 @@ -name: "Weekly Upstream Sync" - -on: - schedule: - # Every Monday at 10:00 UTC (5:00 AM EST / 6:00 AM EDT) - - cron: '0 10 * * 1' - workflow_dispatch: - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - - check-and-sync: - name: "Check upstream & trigger Copilot merge" - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Check for upstream changes - id: check - run: | - LAST_MERGE=$(cat .lastmerge) - echo "Last merged commit: $LAST_MERGE" - - git clone --quiet https://github.com/github/copilot-sdk.git /tmp/upstream - cd /tmp/upstream - - UPSTREAM_HEAD=$(git rev-parse HEAD) - echo "Upstream HEAD: $UPSTREAM_HEAD" - - if [ "$LAST_MERGE" = "$UPSTREAM_HEAD" ]; then - echo "No new upstream changes since last merge." - echo "has_changes=false" >> "$GITHUB_OUTPUT" - else - COMMIT_COUNT=$(git rev-list --count "$LAST_MERGE".."$UPSTREAM_HEAD") - echo "Found $COMMIT_COUNT new upstream commits." - echo "has_changes=true" >> "$GITHUB_OUTPUT" - echo "commit_count=$COMMIT_COUNT" >> "$GITHUB_OUTPUT" - echo "upstream_head=$UPSTREAM_HEAD" >> "$GITHUB_OUTPUT" - echo "last_merge=$LAST_MERGE" >> "$GITHUB_OUTPUT" - - # Generate a short summary of changes - SUMMARY=$(git log --oneline "$LAST_MERGE".."$UPSTREAM_HEAD" | head -20) - { - echo "summary<> "$GITHUB_OUTPUT" - fi - - - name: Close previous upstream-sync issues - if: steps.check.outputs.has_changes == 'true' - env: - GH_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN }} - run: | - # Find all open issues with the upstream-sync label - OPEN_ISSUES=$(gh issue list \ - --repo "${{ github.repository }}" \ - --label "upstream-sync" \ - --state open \ - --json number \ - --jq '.[].number') - - for ISSUE_NUM in $OPEN_ISSUES; do - echo "Closing superseded issue #${ISSUE_NUM}" - gh issue comment "$ISSUE_NUM" \ - --repo "${{ github.repository }}" \ - --body "Superseded by a newer upstream sync issue. Closing this one." - gh issue close "$ISSUE_NUM" \ - --repo "${{ github.repository }}" \ - --reason "not planned" - done - - - name: Close stale upstream-sync issues (no changes) - if: steps.check.outputs.has_changes == 'false' - env: - GH_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN }} - run: | - OPEN_ISSUES=$(gh issue list \ - --repo "${{ github.repository }}" \ - --label "upstream-sync" \ - --state open \ - --json number \ - --jq '.[].number') - - for ISSUE_NUM in $OPEN_ISSUES; do - echo "Closing stale issue #${ISSUE_NUM} — upstream is up to date" - gh issue comment "$ISSUE_NUM" \ - --repo "${{ github.repository }}" \ - --body "No new upstream changes detected. The Java SDK is up to date. Closing." - gh issue close "$ISSUE_NUM" \ - --repo "${{ github.repository }}" \ - --reason "completed" - done - - - name: Create issue and assign to Copilot - id: create-issue - if: steps.check.outputs.has_changes == 'true' - env: - GH_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN }} - run: | - COMMIT_COUNT="${{ steps.check.outputs.commit_count }}" - LAST_MERGE="${{ steps.check.outputs.last_merge }}" - UPSTREAM_HEAD="${{ steps.check.outputs.upstream_head }}" - SUMMARY="${{ steps.check.outputs.summary }}" - DATE=$(date -u +"%Y-%m-%d") - REPO="${{ github.repository }}" - - BODY="## Automated Upstream Sync - - There are **${COMMIT_COUNT}** new commits in the [official Copilot SDK](https://github.com/github/copilot-sdk) since the last merge. - - - **Last merged commit:** [\`${LAST_MERGE}\`](https://github.com/github/copilot-sdk/commit/${LAST_MERGE}) - - **Upstream HEAD:** [\`${UPSTREAM_HEAD}\`](https://github.com/github/copilot-sdk/commit/${UPSTREAM_HEAD}) - - ### Recent upstream commits - - \`\`\` - ${SUMMARY} - \`\`\` - - ### Instructions - - Follow the [agentic-merge-upstream](.github/prompts/agentic-merge-upstream.prompt.md) prompt to port these changes to the Java SDK." - - # Create the issue and assign to Copilot coding agent - ISSUE_URL=$(gh issue create \ - --repo "$REPO" \ - --title "Upstream sync: ${COMMIT_COUNT} new commits (${DATE})" \ - --body "$BODY" \ - --label "upstream-sync" \ - --assignee "copilot-swe-agent") - - echo "issue_url=$ISSUE_URL" >> "$GITHUB_OUTPUT" - echo "✅ Issue created and assigned to Copilot coding agent: $ISSUE_URL" - - - name: Summary - if: always() - run: | - HAS_CHANGES="${{ steps.check.outputs.has_changes }}" - COMMIT_COUNT="${{ steps.check.outputs.commit_count }}" - LAST_MERGE="${{ steps.check.outputs.last_merge }}" - UPSTREAM_HEAD="${{ steps.check.outputs.upstream_head }}" - ISSUE_URL="${{ steps.create-issue.outputs.issue_url }}" - - { - echo "## Weekly Upstream Sync" - echo "" - if [ "$HAS_CHANGES" = "true" ]; then - echo "### ✅ New upstream changes detected" - echo "" - echo "| | |" - echo "|---|---|" - echo "| **New commits** | ${COMMIT_COUNT} |" - echo "| **Last merged** | \`${LAST_MERGE:0:12}\` |" - echo "| **Upstream HEAD** | \`${UPSTREAM_HEAD:0:12}\` |" - echo "" - echo "An issue has been created and assigned to the Copilot coding agent: " - echo " -> ${ISSUE_URL}" - echo "" - echo "### Recent upstream commits" - echo "" - echo '```' - echo "${{ steps.check.outputs.summary }}" - echo '```' - else - echo "### ⏭️ No changes" - echo "" - echo "The Java SDK is already up to date with the upstream Copilot SDK." - echo "" - echo "**Last merged commit:** \`${LAST_MERGE:0:12}\`" - fi - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/weekly-upstream-sync.yml b/.github/workflows/weekly-upstream-sync.yml index 84ef95fc5..aeedf542a 100644 --- a/.github/workflows/weekly-upstream-sync.yml +++ b/.github/workflows/weekly-upstream-sync.yml @@ -11,6 +11,11 @@ permissions: issues: write pull-requests: write +env: + # Used for `gh` CLI operations to create/close/comment issues. Must be a PAT with repo permissions because the default + GH_AW_AGENT_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN }} + GH_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN }} + jobs: check-and-sync: @@ -55,8 +60,6 @@ jobs: - name: Close previous upstream-sync issues if: steps.check.outputs.has_changes == 'true' - env: - GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} run: | # Find all open issues with the upstream-sync label OPEN_ISSUES=$(gh issue list \ @@ -78,8 +81,6 @@ jobs: - name: Close stale upstream-sync issues (no changes) if: steps.check.outputs.has_changes == 'false' - env: - GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} run: | OPEN_ISSUES=$(gh issue list \ --repo "${{ github.repository }}" \ @@ -101,8 +102,6 @@ jobs: - name: Create issue and assign to Copilot id: create-issue if: steps.check.outputs.has_changes == 'true' - env: - GH_TOKEN: ${{ secrets.COPILOT_ISSUE_PR_AGENTIC_WORKFLOW }} run: | COMMIT_COUNT="${{ steps.check.outputs.commit_count }}" LAST_MERGE="${{ steps.check.outputs.last_merge }}" From f1f1315c40085e277f1875cb6c92dd4e9221b791 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 19:11:02 -0500 Subject: [PATCH 038/147] fix: correct output variable assignment for last merge in upstream sync workflow --- .github/workflows/weekly-upstream-sync.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/weekly-upstream-sync.yml b/.github/workflows/weekly-upstream-sync.yml index aeedf542a..f1549c5bb 100644 --- a/.github/workflows/weekly-upstream-sync.yml +++ b/.github/workflows/weekly-upstream-sync.yml @@ -38,6 +38,8 @@ jobs: UPSTREAM_HEAD=$(git rev-parse HEAD) echo "Upstream HEAD: $UPSTREAM_HEAD" + echo "last_merge=$LAST_MERGE" >> "$GITHUB_OUTPUT" + if [ "$LAST_MERGE" = "$UPSTREAM_HEAD" ]; then echo "No new upstream changes since last merge." echo "has_changes=false" >> "$GITHUB_OUTPUT" @@ -47,7 +49,6 @@ jobs: echo "has_changes=true" >> "$GITHUB_OUTPUT" echo "commit_count=$COMMIT_COUNT" >> "$GITHUB_OUTPUT" echo "upstream_head=$UPSTREAM_HEAD" >> "$GITHUB_OUTPUT" - echo "last_merge=$LAST_MERGE" >> "$GITHUB_OUTPUT" # Generate a short summary of changes SUMMARY=$(git log --oneline "$LAST_MERGE".."$UPSTREAM_HEAD" | head -20) From b8a73cd4c2e95abaf109dd760e96b315a812b85d Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 19:37:22 -0500 Subject: [PATCH 039/147] fix: update documentation to clarify issue assignment to Copilot agent --- .github/workflows/weekly-upstream-sync.md | 35 +++++++---------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/.github/workflows/weekly-upstream-sync.md b/.github/workflows/weekly-upstream-sync.md index fbdd028b4..47e2048fc 100644 --- a/.github/workflows/weekly-upstream-sync.md +++ b/.github/workflows/weekly-upstream-sync.md @@ -40,7 +40,7 @@ This document describes the `weekly-upstream-sync.yml` GitHub Actions workflow, ## Overview -The workflow runs on a **weekly schedule** (every Monday at 10:00 UTC) and can also be triggered manually. It does **not** perform the actual merge — instead, it detects upstream changes and creates a GitHub issue assigned to `copilot`, which then follows the [agentic-merge-upstream](../prompts/agentic-merge-upstream.prompt.md) prompt to port the changes. +The workflow runs on a **weekly schedule** (every Monday at 10:00 UTC) and can also be triggered manually. It does **not** perform the actual merge — instead, it detects upstream changes and creates a GitHub issue assigned to `copilot`, instructing the agent to follow the [agentic-merge-upstream](../prompts/agentic-merge-upstream.prompt.md) prompt to port the changes. ## Trigger @@ -49,20 +49,6 @@ The workflow runs on a **weekly schedule** (every Monday at 10:00 UTC) and can a | `schedule` | Every Monday at 10:00 UTC (`0 10 * * 1`) | | `workflow_dispatch` | Manual trigger from the Actions tab | -## Permissions - -| Permission | Level | Purpose | -|---|---|---| -| `contents` | `write` | Read `.lastmerge` file | -| `issues` | `write` | Create/close upstream-sync issues | -| `pull-requests` | `write` | Allow Copilot agent to create PRs | - -## Secrets - -| Secret | Purpose | -|---|---| -| `GH_AW_AGENT_TOKEN` | PAT used as `GH_TOKEN` for `gh` CLI operations (issue create/close/comment). Required because the default `GITHUB_TOKEN` cannot assign issues to `copilot-swe-agent`. | - ## Workflow Steps ### 1. Checkout repository @@ -97,7 +83,7 @@ Creates a new GitHub issue with: - **Title:** `Upstream sync: N new commits (YYYY-MM-DD)` - **Label:** `upstream-sync` -- **Assignee:** `copilot-swe-agent` +- **Assignee:** `copilot` - **Body:** Contains commit count, commit range links, a summary of recent commits, and a link to the merge prompt The Copilot coding agent picks up the issue, creates a branch and PR, then follows the merge prompt to port the changes. @@ -115,21 +101,21 @@ Writes a GitHub Actions step summary with: ``` ┌─────────────────────┐ -│ Schedule / Manual │ +│ Schedule / Manual │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ -│ Read .lastmerge │ -│ Clone upstream SDK │ -│ Compare commits │ +│ Read .lastmerge │ +│ Clone upstream SDK │ +│ Compare commits │ └──────────┬──────────┘ │ ┌─────┴─────┐ - │ │ + │ │ changes? no changes - │ │ - ▼ ▼ + │ │ + ▼ ▼ ┌──────────┐ ┌──────────────────┐ │ Close old│ │ Close stale │ │ issues │ │ issues │ @@ -138,7 +124,7 @@ Writes a GitHub Actions step summary with: ▼ ┌──────────────────────────┐ │ Create issue assigned to │ -│ copilot-swe-agent │ +│ copilot │ └──────────────────────────┘ │ ▼ @@ -154,5 +140,4 @@ Writes a GitHub Actions step summary with: |---|---| | `.lastmerge` | Stores the SHA of the last merged upstream commit | | [agentic-merge-upstream.prompt.md](../prompts/agentic-merge-upstream.prompt.md) | Detailed instructions the Copilot agent follows to port changes | -| [upstream-sync.md](upstream-sync.md) | gh-aw agentic workflow (alternative approach that runs the agent directly in CI) | | `.github/scripts/upstream-sync/` | Helper scripts used by the merge prompt | From b5af3c60aa5c61f521882698579672f32c7e7fe5 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 19:39:35 -0500 Subject: [PATCH 040/147] fix: update actions versions in actions-lock.json --- .../workflows/.github/aw/actions-lock.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/.github/aw/actions-lock.json b/.github/workflows/.github/aw/actions-lock.json index 708bdf24a..2d919a1de 100644 --- a/.github/workflows/.github/aw/actions-lock.json +++ b/.github/workflows/.github/aw/actions-lock.json @@ -1,9 +1,24 @@ { "entries": { - "actions/github-script@v8": { + "actions/checkout@v6": { + "repo": "actions/checkout", + "version": "v6", + "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd" + }, + "actions/download-artifact@v6.0.0": { + "repo": "actions/download-artifact", + "version": "v6.0.0", + "sha": "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53" + }, + "actions/github-script@v8.0.0": { "repo": "actions/github-script", - "version": "v8", + "version": "v8.0.0", "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" + }, + "actions/upload-artifact@v6.0.0": { + "repo": "actions/upload-artifact", + "version": "v6.0.0", + "sha": "b7c566a772e6b6bfb58ed0dc250532a479d7789f" } } } From 63c1e21ce63d677461b08985206b89f66f0e501c Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 19:44:39 -0500 Subject: [PATCH 041/147] fix: update weekly upstream sync workflow to include assign_to_agent functionality --- .../workflows/weekly-upstream-sync.lock.yml | 71 ++++++++++++++++--- .github/workflows/weekly-upstream-sync.md | 5 +- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/.github/workflows/weekly-upstream-sync.lock.yml b/.github/workflows/weekly-upstream-sync.lock.yml index 5baf1c024..2f93073f3 100644 --- a/.github/workflows/weekly-upstream-sync.lock.yml +++ b/.github/workflows/weekly-upstream-sync.lock.yml @@ -22,7 +22,7 @@ # Weekly upstream sync workflow. Checks for new commits in the official # Copilot SDK (github/copilot-sdk) and assigns to Copilot to port changes. # -# frontmatter-hash: 172529183e30b0941e8ae1f70262649342226ab48d4099c1b3f98a406492b7ba +# frontmatter-hash: d5db7ac8fa73ef34ff095a70b8f0340f6d339711fe9c0c414bdf3245eadc41c7 name: "Weekly Upstream Sync Agentic Workflow" "on": @@ -154,12 +154,12 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' - {"add_comment":{"max":10,"target":"*"},"close_issue":{"max":10,"required_labels":["upstream-sync"]},"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + {"add_comment":{"max":10,"target":"*"},"assign_to_agent":{"default_agent":"copilot","target":"*"},"close_issue":{"max":10,"required_labels":["upstream-sync"]},"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' [ { - "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[upstream-sync] \". Labels [upstream-sync] will be automatically added. Assignees [copilot] will be automatically assigned.", + "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[upstream-sync] \". Labels [upstream-sync] will be automatically added.", "inputSchema": { "additionalProperties": false, "properties": { @@ -243,6 +243,34 @@ jobs: }, "name": "add_comment" }, + { + "description": "Assign the GitHub Copilot coding agent to work on an issue or pull request. The agent will analyze the issue/PR and attempt to implement a solution, creating a pull request when complete. Use this to delegate coding tasks to Copilot. Example usage: assign_to_agent(issue_number=123, agent=\"copilot\") or assign_to_agent(pull_number=456, agent=\"copilot\")", + "inputSchema": { + "additionalProperties": false, + "properties": { + "agent": { + "description": "Agent identifier to assign. Defaults to 'copilot' (the Copilot coding agent) if not specified.", + "type": "string" + }, + "issue_number": { + "description": "Issue number to assign the Copilot agent to. This is the numeric ID from the GitHub URL (e.g., 234 in github.com/owner/repo/issues/234). Can also be a temporary_id (e.g., 'aw_abc123def456') from an issue created earlier in the same workflow run. The issue should contain clear, actionable requirements. Either issue_number or pull_number must be provided, but not both.", + "type": [ + "number", + "string" + ] + }, + "pull_number": { + "description": "Pull request number to assign the Copilot agent to. This is the numeric ID from the GitHub URL (e.g., 456 in github.com/owner/repo/pull/456). Either issue_number or pull_number must be provided, but not both.", + "type": [ + "number", + "string" + ] + } + }, + "type": "object" + }, + "name": "assign_to_agent" + }, { "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", "inputSchema": { @@ -330,6 +358,23 @@ jobs: } } }, + "assign_to_agent": { + "defaultMax": 1, + "fields": { + "agent": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "issue_number": { + "issueNumberOrTemporaryId": true + }, + "pull_number": { + "optionalPositiveInteger": true + } + }, + "customValidation": "requiresOneOf:issue_number,pull_number" + }, "close_issue": { "defaultMax": 1, "fields": { @@ -900,6 +945,8 @@ jobs: GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_ASSIGNMENT_ERRORS: ${{ needs.safe_outputs.outputs.assign_to_agent_assignment_errors }} + GH_AW_ASSIGNMENT_ERROR_COUNT: ${{ needs.safe_outputs.outputs.assign_to_agent_assignment_error_count }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1061,6 +1108,9 @@ jobs: GH_AW_WORKFLOW_ID: "weekly-upstream-sync" GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" outputs: + assign_to_agent_assigned: ${{ steps.assign_to_agent.outputs.assigned }} + assign_to_agent_assignment_error_count: ${{ steps.assign_to_agent.outputs.assignment_error_count }} + assign_to_agent_assignment_errors: ${{ steps.assign_to_agent.outputs.assignment_errors }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} @@ -1086,8 +1136,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"close_issue\":{\"max\":10,\"required_labels\":[\"upstream-sync\"],\"target\":\"*\"},\"create_issue\":{\"assignees\":[\"copilot\"],\"labels\":[\"upstream-sync\"],\"max\":1,\"title_prefix\":\"[upstream-sync] \"},\"missing_data\":{},\"missing_tool\":{}}" - GH_AW_ASSIGN_COPILOT: "true" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"close_issue\":{\"max\":10,\"required_labels\":[\"upstream-sync\"],\"target\":\"*\"},\"create_issue\":{\"labels\":[\"upstream-sync\"],\"max\":1,\"title_prefix\":\"[upstream-sync] \"},\"missing_data\":{},\"missing_tool\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1095,16 +1144,20 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Assign Copilot to created issues - if: steps.process_safe_outputs.outputs.issues_to_assign_copilot != '' + - name: Assign To Agent + id: assign_to_agent + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'assign_to_agent')) uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: - GH_AW_ISSUES_TO_ASSIGN_COPILOT: ${{ steps.process_safe_outputs.outputs.issues_to_assign_copilot }} + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_AGENT_DEFAULT: "copilot" + GH_AW_AGENT_TARGET: "*" + GH_AW_TEMPORARY_ID_MAP: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} with: github-token: ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/assign_copilot_to_created_issues.cjs'); + const { main } = require('/opt/gh-aw/actions/assign_to_agent.cjs'); await main(); diff --git a/.github/workflows/weekly-upstream-sync.md b/.github/workflows/weekly-upstream-sync.md index 47e2048fc..839e288bc 100644 --- a/.github/workflows/weekly-upstream-sync.md +++ b/.github/workflows/weekly-upstream-sync.md @@ -24,7 +24,6 @@ tools: safe-outputs: create-issue: title-prefix: "[upstream-sync] " - assignees: [copilot] labels: [upstream-sync] close-issue: required-labels: [upstream-sync] @@ -33,7 +32,9 @@ safe-outputs: add-comment: target: "*" max: 10 - noop: + assign-to-agent: + name: "copilot" + target: "*" --- # Weekly Upstream Sync Agentic Workflow This document describes the `weekly-upstream-sync.yml` GitHub Actions workflow, which automates the detection of new changes in the official [Copilot SDK](https://github.com/github/copilot-sdk) and delegates the merge work to the Copilot coding agent. From 405424d9a560178dbfac4acd9c09ab631c90f45c Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 19:46:49 -0500 Subject: [PATCH 042/147] fix: simplify paths-ignore in build-test workflow to exclude all .github files --- .../workflows/.github/aw/actions-lock.json | 24 ------------------- .github/workflows/build-test.yml | 19 ++------------- 2 files changed, 2 insertions(+), 41 deletions(-) delete mode 100644 .github/workflows/.github/aw/actions-lock.json diff --git a/.github/workflows/.github/aw/actions-lock.json b/.github/workflows/.github/aw/actions-lock.json deleted file mode 100644 index 2d919a1de..000000000 --- a/.github/workflows/.github/aw/actions-lock.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "entries": { - "actions/checkout@v6": { - "repo": "actions/checkout", - "version": "v6", - "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd" - }, - "actions/download-artifact@v6.0.0": { - "repo": "actions/download-artifact", - "version": "v6.0.0", - "sha": "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53" - }, - "actions/github-script@v8.0.0": { - "repo": "actions/github-script", - "version": "v8.0.0", - "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" - }, - "actions/upload-artifact@v6.0.0": { - "repo": "actions/upload-artifact", - "version": "v6.0.0", - "sha": "b7c566a772e6b6bfb58ed0dc250532a479d7789f" - } - } -} diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 2ed3ad968..5e1f5c628 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -9,27 +9,12 @@ on: paths-ignore: - 'README.md' - 'LICENSE' - - '.github/prompts/**' - - '.github/skills/**' - - '.github/scripts/**' - - '.github/templates/**' - - '.github/dependabot.yml' - - '.github/release.yml' - - '.github/workflows/publish-maven.yml' - - '.github/workflows/weekly-upstream-sync.yml' - - '.github/workflows/copilot-issue.yml' + - '.github/**' pull_request: paths-ignore: - 'README.md' - 'LICENSE' - - '.github/prompts/**' - - '.github/skills/**' - - '.github/scripts/**' - - '.github/templates/**' - - '.github/dependabot.yml' - - '.github/release.yml' - - '.github/workflows/publish-maven.yml' - - '.github/workflows/weekly-upstream-sync.yml' + - '.github/**' workflow_dispatch: merge_group: From e200e60be24837bd0f025d6a40cfdad2372db8e2 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 19:49:16 -0500 Subject: [PATCH 043/147] Remove the create-sample-issue workflow documentation file --- .../workflows/create-sample-issue.lock.yml | 1084 ----------------- .github/workflows/create-sample-issue.md | 31 - 2 files changed, 1115 deletions(-) delete mode 100644 .github/workflows/create-sample-issue.lock.yml delete mode 100644 .github/workflows/create-sample-issue.md diff --git a/.github/workflows/create-sample-issue.lock.yml b/.github/workflows/create-sample-issue.lock.yml deleted file mode 100644 index 0362ed616..000000000 --- a/.github/workflows/create-sample-issue.lock.yml +++ /dev/null @@ -1,1084 +0,0 @@ -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# This file was automatically generated by gh-aw (v0.42.17). DO NOT EDIT. -# -# To update this file, edit the corresponding .md file and run: -# gh aw compile -# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md -# -# Creates a sample test issue and assigns it to Copilot. -# -# frontmatter-hash: 64751f386c7bb6b1f28418404d978f05905aa582cb69411471d5f6e92576efdc - -name: "Create Sample Issue" -"on": - workflow_dispatch: - -permissions: {} - -concurrency: - group: "gh-aw-${{ github.workflow }}" - -run-name: "Create Sample Issue" - -jobs: - activation: - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - comment_id: "" - comment_repo: "" - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_WORKFLOW_FILE: "create-sample-issue.lock.yml" - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); - await main(); - - agent: - needs: activation - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - issues: read - concurrency: - group: "gh-aw-copilot-${{ github.workflow }}" - 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_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json - outputs: - checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} - has_patch: ${{ steps.collect_output.outputs.has_patch }} - model: ${{ steps.generate_aw_info.outputs.model }} - output: ${{ steps.collect_output.outputs.output }} - output_types: ${{ steps.collect_output.outputs.output_types }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Checkout .github and .agents folders - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - with: - sparse-checkout: | - .github - .agents - depth: 1 - persist-credentials: false - - name: Create gh-aw temp directory - run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Checkout PR branch - id: checkout-pr - if: | - github.event.pull_request - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.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('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); - await main(); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 - - name: Install awf binary - run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.13.12 - - name: Determine automatic lockdown mode for GitHub MCP server - id: determine-automatic-lockdown - env: - TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - if: env.TOKEN_CHECK != '' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); - - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.13.12 ghcr.io/github/gh-aw-firewall/squid:0.13.12 ghcr.io/github/gh-aw-mcpg:v0.0.113 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine - - name: Write Safe Outputs Config - run: | - mkdir -p /opt/gh-aw/safeoutputs - mkdir -p /tmp/gh-aw/safeoutputs - mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' - {"assign_to_agent":{"default_agent":"copilot","target":"*"},"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} - EOF - cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' - [ - { - "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[test] \". Labels [test] will be automatically added.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "body": { - "description": "Detailed issue description in Markdown. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate.", - "type": "string" - }, - "labels": { - "description": "Labels to categorize the issue (e.g., 'bug', 'enhancement'). Labels must exist in the repository.", - "items": { - "type": "string" - }, - "type": "array" - }, - "parent": { - "description": "Parent issue number for creating sub-issues. This is the numeric ID from the GitHub URL (e.g., 42 in github.com/owner/repo/issues/42). Can also be a temporary_id (e.g., 'aw_abc123def456') from a previously created issue in the same workflow run.", - "type": [ - "number", - "string" - ] - }, - "temporary_id": { - "description": "Unique temporary identifier for referencing this issue before it's created. Format: 'aw_' followed by 12 hex characters (e.g., 'aw_abc123def456'). Use '#aw_ID' in body text to reference other issues by their temporary_id; these are replaced with actual issue numbers after creation.", - "type": "string" - }, - "title": { - "description": "Concise issue title summarizing the bug, feature, or task. The title appears as the main heading, so keep it brief and descriptive.", - "type": "string" - } - }, - "required": [ - "title", - "body" - ], - "type": "object" - }, - "name": "create_issue" - }, - { - "description": "Assign the GitHub Copilot coding agent to work on an issue or pull request. The agent will analyze the issue/PR and attempt to implement a solution, creating a pull request when complete. Use this to delegate coding tasks to Copilot. Example usage: assign_to_agent(issue_number=123, agent=\"copilot\") or assign_to_agent(pull_number=456, agent=\"copilot\")", - "inputSchema": { - "additionalProperties": false, - "properties": { - "agent": { - "description": "Agent identifier to assign. Defaults to 'copilot' (the Copilot coding agent) if not specified.", - "type": "string" - }, - "issue_number": { - "description": "Issue number to assign the Copilot agent to. This is the numeric ID from the GitHub URL (e.g., 234 in github.com/owner/repo/issues/234). Can also be a temporary_id (e.g., 'aw_abc123def456') from an issue created earlier in the same workflow run. The issue should contain clear, actionable requirements. Either issue_number or pull_number must be provided, but not both.", - "type": [ - "number", - "string" - ] - }, - "pull_number": { - "description": "Pull request number to assign the Copilot agent to. This is the numeric ID from the GitHub URL (e.g., 456 in github.com/owner/repo/pull/456). Either issue_number or pull_number must be provided, but not both.", - "type": [ - "number", - "string" - ] - } - }, - "type": "object" - }, - "name": "assign_to_agent" - }, - { - "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "alternatives": { - "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", - "type": "string" - }, - "reason": { - "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", - "type": "string" - }, - "tool": { - "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", - "type": "string" - } - }, - "required": [ - "reason" - ], - "type": "object" - }, - "name": "missing_tool" - }, - { - "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "message": { - "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", - "type": "string" - } - }, - "required": [ - "message" - ], - "type": "object" - }, - "name": "noop" - }, - { - "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "alternatives": { - "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", - "type": "string" - }, - "context": { - "description": "Additional context about the missing data or where it should come from (max 256 characters).", - "type": "string" - }, - "data_type": { - "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", - "type": "string" - }, - "reason": { - "description": "Explanation of why this data is needed to complete the task (max 256 characters).", - "type": "string" - } - }, - "required": [], - "type": "object" - }, - "name": "missing_data" - } - ] - EOF - cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' - { - "assign_to_agent": { - "defaultMax": 1, - "fields": { - "agent": { - "type": "string", - "sanitize": true, - "maxLength": 128 - }, - "issue_number": { - "issueNumberOrTemporaryId": true - }, - "pull_number": { - "optionalPositiveInteger": true - } - }, - "customValidation": "requiresOneOf:issue_number,pull_number" - }, - "create_issue": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "labels": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "parent": { - "issueOrPRNumber": true - }, - "repo": { - "type": "string", - "maxLength": 256 - }, - "temporary_id": { - "type": "string" - }, - "title": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "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 - } - } - } - } - EOF - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - API_KEY="" - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - PORT=3001 - - # Register API key as secret to mask it from logs - echo "::add-mask::${API_KEY}" - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash /opt/gh-aw/actions/start_safe_outputs_server.sh - - - name: Start MCP gateway - id: start-mcp-gateway - env: - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - set -eo pipefail - mkdir -p /tmp/gh-aw/mcp-config - - # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" - export MCP_GATEWAY_DOMAIN="host.docker.internal" - MCP_GATEWAY_API_KEY="" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - export MCP_GATEWAY_API_KEY - export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" - mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" - export DEBUG="*" - - # Register API key as secret to mask it from logs - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -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_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_LOCKDOWN -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 GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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 ghcr.io/github/gh-aw-mcpg:v0.0.113' - - mkdir -p /home/runner/.copilot - cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh - { - "mcpServers": { - "github": { - "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v0.30.3", - "env": { - "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", - "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "context,issues" - } - }, - "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" - } - } - }, - "gateway": { - "port": $MCP_GATEWAY_PORT, - "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", - "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" - } - } - MCPCONFIG_EOF - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "copilot", - engine_name: "GitHub Copilot CLI", - model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", - version: "", - agent_version: "0.0.405", - cli_version: "v0.42.17", - workflow_name: "Create Sample Issue", - experimental: false, - supports_tools_allowlist: true, - supports_http_transport: true, - run_id: context.runId, - run_number: context.runNumber, - run_attempt: process.env.GITHUB_RUN_ATTEMPT, - repository: context.repo.owner + '/' + context.repo.repo, - ref: context.ref, - sha: context.sha, - actor: context.actor, - event_name: context.eventName, - staged: false, - allowed_domains: ["defaults"], - firewall_enabled: true, - awf_version: "v0.13.12", - awmg_version: "v0.0.113", - steps: { - firewall: "squid" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/aw_info.json'; - fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); - console.log('Generated aw_info.json at:', tmpPath); - console.log(JSON.stringify(awInfo, null, 2)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); - await generateWorkflowOverview(core); - - name: Create prompt with built-in context - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - run: | - bash /opt/gh-aw/actions/create_prompt_first.sh - cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" - - PROMPT_EOF - cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" - cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - GitHub API Access Instructions - - The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. - - - To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. - - Discover available tools from the safeoutputs MCP server. - - **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. - - **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. - - - - The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ - {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - {{#runtime-import .github/workflows/create-sample-issue.md}} - PROMPT_EOF - - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - with: - script: | - const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); - - // Call the substitution function - return await substitutePlaceholders({ - file: process.env.GH_AW_PROMPT, - substitutions: { - GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, - 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 - } - }); - - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); - await main(); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/print_prompt_summary.sh - - name: Execute GitHub Copilot CLI - id: agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 - run: | - set -o pipefail - sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.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 --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.13.12 --skip-pull \ - -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' \ - 2>&1 | tee /tmp/gh-aw/agent-stdio.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json - GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_WORKSPACE: ${{ github.workspace }} - XDG_CONFIG_HOME: /home/runner - - name: Copy Copilot session state files to logs - if: always() - continue-on-error: true - run: | - # Copy Copilot session state files to logs folder for artifact collection - # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them - SESSION_STATE_DIR="$HOME/.copilot/session-state" - LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" - - if [ -d "$SESSION_STATE_DIR" ]; then - echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" - mkdir -p "$LOGS_DIR" - cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true - echo "Session state files copied successfully" - else - echo "No session-state directory found at $SESSION_STATE_DIR" - fi - - 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 /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" - - name: Redact secrets in logs - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); - await main(); - env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - 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: Upload Safe Outputs - if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: safe-output - path: ${{ env.GH_AW_SAFE_OUTPUTS }} - if-no-files-found: warn - - name: Ingest agent output - id: collect_output - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.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" - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_API_URL: ${{ github.api_url }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); - await main(); - - name: Upload sanitized agent output - if: always() && env.GH_AW_AGENT_OUTPUT - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: agent-output - path: ${{ env.GH_AW_AGENT_OUTPUT }} - if-no-files-found: warn - - name: Upload engine output files - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: agent_outputs - path: | - /tmp/gh-aw/sandbox/agent/logs/ - /tmp/gh-aw/redacted-urls.log - if-no-files-found: ignore - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); - await main(); - - name: Parse MCP gateway logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); - await main(); - - name: Print firewall logs - if: always() - continue-on-error: true - env: - AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - - name: Upload agent artifacts - if: always() - continue-on-error: true - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: agent-artifacts - path: | - /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/aw_info.json - /tmp/gh-aw/mcp-logs/ - /tmp/gh-aw/sandbox/firewall/logs/ - /tmp/gh-aw/agent-stdio.log - /tmp/gh-aw/agent/ - if-no-files-found: ignore - - conclusion: - needs: - - activation - - agent - - detection - - safe_outputs - if: (always()) && (needs.agent.result != 'skipped') - runs-on: ubuntu-slim - permissions: - contents: read - discussions: write - issues: write - pull-requests: write - outputs: - 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 - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Debug job inputs - env: - COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - AGENT_CONCLUSION: ${{ needs.agent.result }} - run: | - echo "Comment ID: $COMMENT_ID" - echo "Comment Repo: $COMMENT_REPO" - echo "Agent Output Types: $AGENT_OUTPUT_TYPES" - echo "Agent Conclusion: $AGENT_CONCLUSION" - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/safeoutputs/ - - name: Setup agent output environment variable - run: | - mkdir -p /tmp/gh-aw/safeoutputs/ - find "/tmp/gh-aw/safeoutputs/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - - name: Process No-Op Messages - id: noop - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_NOOP_MAX: 1 - GH_AW_WORKFLOW_NAME: "Create Sample Issue" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/noop.cjs'); - await main(); - - name: Record Missing Tool - id: missing_tool - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Create Sample Issue" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); - await main(); - - name: Handle Agent Failure - id: handle_agent_failure - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Create Sample Issue" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} - GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} - GH_AW_ASSIGNMENT_ERRORS: ${{ needs.safe_outputs.outputs.assign_to_agent_assignment_errors }} - GH_AW_ASSIGNMENT_ERROR_COUNT: ${{ needs.safe_outputs.outputs.assign_to_agent_assignment_error_count }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); - await main(); - - name: Handle No-Op Message - id: handle_noop_message - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Create Sample Issue" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); - await main(); - - name: Update reaction comment with completion status - id: conclusion - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_WORKFLOW_NAME: "Create Sample Issue" - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/notify_comment_error.cjs'); - await main(); - - detection: - needs: agent - if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' - runs-on: ubuntu-latest - permissions: {} - concurrency: - group: "gh-aw-copilot-${{ github.workflow }}" - timeout-minutes: 10 - outputs: - success: ${{ steps.parse_results.outputs.success }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Download agent artifacts - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-artifacts - path: /tmp/gh-aw/threat-detection/ - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/threat-detection/ - - name: Echo agent output types - env: - AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - run: | - echo "Agent output-types: $AGENT_OUTPUT_TYPES" - - name: Setup threat detection - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - WORKFLOW_NAME: "Create Sample Issue" - WORKFLOW_DESCRIPTION: "Creates a sample test issue and assigns it to Copilot." - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); - await main(); - - name: Ensure threat-detection directory and log - run: | - mkdir -p /tmp/gh-aw/threat-detection - touch /tmp/gh-aw/threat-detection/detection.log - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.405 - - name: Execute GitHub Copilot CLI - id: agentic_execution - # Copilot CLI tool arguments (sorted): - # --allow-tool shell(cat) - # --allow-tool shell(grep) - # --allow-tool shell(head) - # --allow-tool shell(jq) - # --allow-tool shell(ls) - # --allow-tool shell(tail) - # --allow-tool shell(wc) - timeout-minutes: 20 - run: | - set -o pipefail - COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" - mkdir -p /tmp/ - mkdir -p /tmp/gh-aw/ - mkdir -p /tmp/gh-aw/agent/ - mkdir -p /tmp/gh-aw/sandbox/agent/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_WORKSPACE: ${{ github.workspace }} - XDG_CONFIG_HOME: /home/runner - - name: Parse threat detection results - id: parse_results - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - - name: Upload threat detection log - if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: threat-detection.log - path: /tmp/gh-aw/threat-detection/detection.log - if-no-files-found: ignore - - safe_outputs: - needs: - - agent - - detection - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') - runs-on: ubuntu-slim - permissions: - contents: read - issues: write - timeout-minutes: 15 - env: - GH_AW_ENGINE_ID: "copilot" - GH_AW_WORKFLOW_ID: "create-sample-issue" - GH_AW_WORKFLOW_NAME: "Create Sample Issue" - outputs: - assign_to_agent_assigned: ${{ steps.assign_to_agent.outputs.assigned }} - assign_to_agent_assignment_error_count: ${{ steps.assign_to_agent.outputs.assignment_error_count }} - assign_to_agent_assignment_errors: ${{ steps.assign_to_agent.outputs.assignment_errors }} - create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} - create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} - 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 }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 - with: - destination: /opt/gh-aw/actions - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/safeoutputs/ - - name: Setup agent output environment variable - run: | - mkdir -p /tmp/gh-aw/safeoutputs/ - find "/tmp/gh-aw/safeoutputs/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - - name: Process Safe Outputs - id: process_safe_outputs - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"labels\":[\"test\"],\"max\":1,\"title_prefix\":\"[test] \"},\"missing_data\":{},\"missing_tool\":{}}" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); - await main(); - - name: Assign To Agent - id: assign_to_agent - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'assign_to_agent')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_AGENT_DEFAULT: "copilot" - GH_AW_AGENT_TARGET: "*" - GH_AW_TEMPORARY_ID_MAP: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} - with: - github-token: ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/assign_to_agent.cjs'); - await main(); - diff --git a/.github/workflows/create-sample-issue.md b/.github/workflows/create-sample-issue.md deleted file mode 100644 index d85459574..000000000 --- a/.github/workflows/create-sample-issue.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -description: Creates a sample test issue and assigns it to Copilot. - -on: - workflow_dispatch: - -permissions: - contents: read - actions: read - issues: read - -tools: - github: - toolsets: [context, issues] - -safe-outputs: - create-issue: - title-prefix: "[test] " - labels: [test] - assign-to-agent: - name: "copilot" - target: "*" ---- -# Create Sample Issue - -Create a test issue in this repository and then assign Copilot to it. The issue should contain: - -- **Title:** `Sample test issue` -- **Body:** `This is an automated test issue created by the create-sample-issue workflow. Feel free to close it.` - -After creating the issue, assign the `copilot` agent to it. From 5e0e13939d1b8d8bb27fe4aa94ba29b92e70c2ec Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 21:44:18 -0500 Subject: [PATCH 044/147] fix: update cron schedule to run build-test workflow weekly on Sundays --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/weekly-upstream-sync.lock.yml | 8 ++++---- .github/workflows/weekly-upstream-sync.md | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 5e1f5c628..4d7e7b2e4 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -2,8 +2,8 @@ name: "Build & Test" on: schedule: - # Run once a day at 00:00 UTC - - cron: '0 0 * * *' + # Run once a week on Sundays at 00:00 UTC + - cron: '0 0 * * 0' push: branches: [main] paths-ignore: diff --git a/.github/workflows/weekly-upstream-sync.lock.yml b/.github/workflows/weekly-upstream-sync.lock.yml index 2f93073f3..17c726f5d 100644 --- a/.github/workflows/weekly-upstream-sync.lock.yml +++ b/.github/workflows/weekly-upstream-sync.lock.yml @@ -22,7 +22,7 @@ # Weekly upstream sync workflow. Checks for new commits in the official # Copilot SDK (github/copilot-sdk) and assigns to Copilot to port changes. # -# frontmatter-hash: d5db7ac8fa73ef34ff095a70b8f0340f6d339711fe9c0c414bdf3245eadc41c7 +# frontmatter-hash: d578661b88d0b1225af55ed3d5ad443003b08248f078bbe0fc306f89af2ad801 name: "Weekly Upstream Sync Agentic Workflow" "on": @@ -154,7 +154,7 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' - {"add_comment":{"max":10,"target":"*"},"assign_to_agent":{"default_agent":"copilot","target":"*"},"close_issue":{"max":10,"required_labels":["upstream-sync"]},"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + {"add_comment":{"max":10,"target":"*"},"assign_to_agent":{"default_agent":"copilot","target":"*"},"close_issue":{"max":10,"required_labels":["upstream-sync"]},"create_issue":{"expires":144,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' [ @@ -963,7 +963,7 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1136,7 +1136,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"close_issue\":{\"max\":10,\"required_labels\":[\"upstream-sync\"],\"target\":\"*\"},\"create_issue\":{\"labels\":[\"upstream-sync\"],\"max\":1,\"title_prefix\":\"[upstream-sync] \"},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"close_issue\":{\"max\":10,\"required_labels\":[\"upstream-sync\"],\"target\":\"*\"},\"create_issue\":{\"expires\":144,\"labels\":[\"upstream-sync\"],\"max\":1,\"title_prefix\":\"[upstream-sync] \"},\"missing_data\":{},\"missing_tool\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/weekly-upstream-sync.md b/.github/workflows/weekly-upstream-sync.md index 839e288bc..c2ae5840d 100644 --- a/.github/workflows/weekly-upstream-sync.md +++ b/.github/workflows/weekly-upstream-sync.md @@ -25,6 +25,7 @@ safe-outputs: create-issue: title-prefix: "[upstream-sync] " labels: [upstream-sync] + expires: 6 close-issue: required-labels: [upstream-sync] target: "*" @@ -35,6 +36,8 @@ safe-outputs: assign-to-agent: name: "copilot" target: "*" + noop: + report-as-issue: false --- # Weekly Upstream Sync Agentic Workflow This document describes the `weekly-upstream-sync.yml` GitHub Actions workflow, which automates the detection of new changes in the official [Copilot SDK](https://github.com/github/copilot-sdk) and delegates the merge work to the Copilot coding agent. From 16088cbb0c7242e8c19fe1459e98f28c69560369 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 21:48:25 -0500 Subject: [PATCH 045/147] docs: add CI/CD workflows section to README and create WORKFLOWS.md for detailed workflow descriptions --- README.md | 6 ++++ WORKFLOWS.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 WORKFLOWS.md diff --git a/README.md b/README.md index 1704e88ad..5e7b8a0f6 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,12 @@ jbang https://github.com/copilot-community-sdk/copilot-sdk-java/blob/latest/jban > Want to add your project? Open a PR! +## CI/CD Workflows + +This project uses several GitHub Actions workflows for building, testing, releasing, and syncing with the upstream SDK. + +See [WORKFLOWS.md](WORKFLOWS.md) for a full overview and details on each workflow. + ## Contributing Contributions are welcome! Please see the [Contributing Guide](CONTRIBUTING.md) for details. diff --git a/WORKFLOWS.md b/WORKFLOWS.md new file mode 100644 index 000000000..74afbd337 --- /dev/null +++ b/WORKFLOWS.md @@ -0,0 +1,100 @@ +# Workflows + +## Overview + +| Workflow | Description | Triggers | Schedule | +|----------|-------------|----------|----------| +| [Build & Test](workflows/build-test.yml) | Builds, lints, and tests the Java SDK | `push` (main), `pull_request`, `merge_group`, `workflow_dispatch` | Sundays at 00:00 UTC | +| [Deploy Documentation](workflows/deploy-site.yml) | Generates and deploys versioned docs to GitHub Pages | `workflow_run` (after Build & Test), `release`, `workflow_dispatch` | — | +| [Publish to Maven Central](workflows/publish-maven.yml) | Releases the SDK to Maven Central and creates a GitHub Release | `workflow_dispatch` | — | +| [Weekly Upstream Sync](workflows/weekly-upstream-sync.yml) | Checks for new upstream commits and creates an issue for Copilot to merge | `workflow_dispatch` | Mondays at 10:00 UTC | +| [Weekly Upstream Sync (Agentic)](workflows/weekly-upstream-sync.lock.yml) | Compiled agentic workflow that executes the upstream sync via `gh-aw` | `workflow_dispatch` | Tuesdays at 08:39 UTC (scattered) | +| [Copilot Setup Steps](workflows/copilot-setup-steps.yml) | Configures the environment for the GitHub Copilot coding agent | `push` (on self-change), `workflow_dispatch` | — | + +--- + +## Build & Test + +**File:** [`build-test.yml`](workflows/build-test.yml) + +Runs on every push to `main`, on pull requests, and in merge groups. Also runs weekly on Sundays to catch regressions from dependency updates. + +Steps: +1. Checks code formatting with Spotless +2. Compiles the SDK and clones the upstream test harness +3. Verifies Javadoc generation +4. Installs the Copilot CLI from the cloned upstream SDK +5. Runs the full test suite with `mvn verify` +6. Uploads test results (JaCoCo + Surefire) as artifacts for site generation + +Ignores changes to `README.md`, `LICENSE`, and `.github/**`. + +--- + +## Deploy Documentation + +**File:** [`deploy-site.yml`](workflows/deploy-site.yml) + +Builds and deploys the Maven site to GitHub Pages. Supports three publishing modes: + +- **Snapshot** — triggered automatically after a successful Build & Test run on `main`; publishes to `/snapshot/` +- **Release** — triggered on `release` publication; publishes to `/vX.Y.Z/` and `/latest/` +- **Manual** — allows building docs for a specific version tag, optionally publishing as `latest`, or rebuilding all versions + +Only one deployment runs at a time (`concurrency: pages`). + +--- + +## Publish to Maven Central + +**File:** [`publish-maven.yml`](workflows/publish-maven.yml) + +Manual-only workflow that performs a full release: + +1. Determines release and next development versions (auto-derived from `pom.xml` or manually specified) +2. Updates `CHANGELOG.md`, `README.md`, and `jbang-example.java` with the release version +3. Injects the upstream sync commit hash from `.lastmerge` into the changelog +4. Runs `mvn release:prepare` and `mvn release:perform` to deploy to Maven Central +5. Creates a GitHub Release with auto-generated notes +6. Moves the `latest` git tag forward +7. Triggers the Deploy Documentation workflow +8. Rolls back the documentation commit if the release fails + +--- + +## Weekly Upstream Sync + +**File:** [`weekly-upstream-sync.yml`](workflows/weekly-upstream-sync.yml) + +Runs every Monday at 10:00 UTC. Clones the official `github/copilot-sdk` repository and compares `HEAD` against the commit hash stored in `.lastmerge`. + +If new commits are found: +1. Closes any previously open `upstream-sync` issues +2. Creates a new issue with a summary of upstream changes +3. Assigns the issue to `copilot-swe-agent` for automated porting + +If no changes are found, any stale open `upstream-sync` issues are closed. + +--- + +## Weekly Upstream Sync (Agentic Workflow: Experimental) + +**File:** [`weekly-upstream-sync.lock.yml`](workflows/weekly-upstream-sync.lock.yml) + +Auto-generated compiled workflow produced by `gh aw compile` from the corresponding `.md` source. This is the agentic counterpart that actually executes the upstream merge using the `gh-aw` MCP server and Copilot coding agent. + +> **Do not edit this file directly.** Edit the `.md` source and run `gh aw compile`. + +--- + +## Copilot Setup Steps + +**File:** [`copilot-setup-steps.yml`](workflows/copilot-setup-steps.yml) + +Configures the development environment for the GitHub Copilot coding agent (`copilot-swe-agent`). The job **must** be named `copilot-setup-steps` to be recognized. + +Sets up: +- Node.js 22 +- JDK 17 (Temurin) +- `gh-aw` CLI extension +- Maven cache From a558edb97980686bebfeda9136b5eafd50a6f0c6 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 22:48:36 -0500 Subject: [PATCH 046/147] feat: add script to generate JaCoCo coverage badge and update workflow to commit badge on push --- .github/scripts/generate-coverage-badge.sh | 64 ++++++++++++++++++++++ .github/workflows/build-test.yml | 16 +++++- README.md | 1 + 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100755 .github/scripts/generate-coverage-badge.sh diff --git a/.github/scripts/generate-coverage-badge.sh b/.github/scripts/generate-coverage-badge.sh new file mode 100755 index 000000000..1d124d947 --- /dev/null +++ b/.github/scripts/generate-coverage-badge.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Generates an SVG coverage badge from a JaCoCo CSV report. +# +# Usage: generate-coverage-badge.sh [jacoco.csv] [output-dir] +# jacoco.csv - Path to JaCoCo CSV report (default: target/site/jacoco-coverage/jacoco.csv) +# output-dir - Directory for the badge SVG (default: .github/badges) +set -euo pipefail + +CSV="${1:-target/site/jacoco-coverage/jacoco.csv}" +BADGES_DIR="${2:-.github/badges}" + +if [ ! -f "$CSV" ]; then + echo "⚠️ No JaCoCo CSV report found at $CSV" + exit 0 +fi + +# Sum INSTRUCTION_MISSED and INSTRUCTION_COVERED across all rows (skip header) +read -r missed covered <<< "$(awk -F',' 'NR>1 { m+=$4; c+=$5 } END { print m, c }' "$CSV")" +total=$((missed + covered)) +if [ "$total" -eq 0 ]; then + pct="0" +else + pct=$(awk "BEGIN { printf \"%.1f\", ($covered / $total) * 100 }") + # Drop trailing .0 + pct=$(echo "$pct" | sed 's/\.0$//') +fi +echo "Coverage: ${pct}%" + +# Choose badge color based on coverage +color="#e05d44" # red <60 +if awk "BEGIN{exit!($pct>=100)}"; then color="#4c1" # bright green +elif awk "BEGIN{exit!($pct>=90)}"; then color="#97ca00" # green +elif awk "BEGIN{exit!($pct>=80)}"; then color="#a4a61d" # yellow-green +elif awk "BEGIN{exit!($pct>=70)}"; then color="#dfb317" # yellow +elif awk "BEGIN{exit!($pct>=60)}"; then color="#fe7d37" # orange +fi + +# Generate SVG badge +mkdir -p "$BADGES_DIR" +label="coverage" +value="${pct}%" +lw=62; vw=46; tw=$((lw + vw)) +cat > "${BADGES_DIR}/jacoco.svg" < + + + + + + + + + + + + ${label} + ${label} + ${value} + ${value} + + +EOF + +echo "Badge generated at ${BADGES_DIR}/jacoco.svg" diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 4d7e7b2e4..c024758d4 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -19,7 +19,7 @@ on: merge_group: permissions: - contents: read + contents: write checks: write jobs: @@ -86,6 +86,20 @@ jobs: target/surefire-reports/ retention-days: 1 + - name: Generate and commit JaCoCo badge + if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + .github/scripts/generate-coverage-badge.sh + + # Commit if changed + if [[ $(git status --porcelain .github/badges/) ]]; then + git config --global user.name 'github-actions[bot]' + git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add .github/badges/ + git commit -m "Update JaCoCo coverage badge" + git push + fi + - name: Generate Test Report Summary if: always() uses: ./.github/actions/test-report diff --git a/README.md b/README.md index 5e7b8a0f6..5f743abad 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/build-test.yml/badge.svg)](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/build-test.yml) [![Site](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/deploy-site.yml/badge.svg)](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/deploy-site.yml) +[![Coverage](.github/badges/jacoco.svg)](https://copilot-community-sdk.github.io/copilot-sdk-java/snapshot/jacoco/index.html) [![Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://copilot-community-sdk.github.io/copilot-sdk-java/) [![Java 17+](https://img.shields.io/badge/Java-17%2B-blue)](https://openjdk.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) From cdf0187de1089ad0e353dd433bd9dc2e03faaf2d Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 9 Feb 2026 22:56:33 -0500 Subject: [PATCH 047/147] fix: update JaCoCo badge generation condition to remove event name check --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index c024758d4..5749900f6 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -87,7 +87,7 @@ jobs: retention-days: 1 - name: Generate and commit JaCoCo badge - if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main' + if: success() && github.ref == 'refs/heads/main' run: | .github/scripts/generate-coverage-badge.sh From c0b674768d7db2d5ca2a382f982cbf41de76e6c6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 04:09:18 +0000 Subject: [PATCH 048/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/badges/jacoco.svg diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg new file mode 100644 index 000000000..aa16ccc25 --- /dev/null +++ b/.github/badges/jacoco.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + coverage + coverage + 69.7% + 69.7% + + From 29d2022fc7472eb65357bd85c4de31c7457e1c2e Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 00:04:54 -0500 Subject: [PATCH 049/147] Refactor code structure for improved readability and maintainability. --- .../github/copilot/sdk/CopilotSession.java | 2 +- .../copilot/sdk/RpcHandlerDispatcher.java | 2 +- .../sdk/events/SessionEventParser.java | 30 - .../github/copilot/sdk/MetadataApiTest.java | 4 +- .../copilot/sdk/SessionEventParserTest.java | 1376 +++++++++++++++-- 5 files changed, 1283 insertions(+), 131 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index 0a81cd719..ab2f49a29 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -750,7 +750,7 @@ public CompletableFuture> getMessages() { if (response.getEvents() != null) { for (JsonNode eventNode : response.getEvents()) { try { - AbstractSessionEvent event = SessionEventParser.parse(eventNode.toString()); + AbstractSessionEvent event = SessionEventParser.parse(eventNode); if (event != null) { events.add(event); } diff --git a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java index 1932ebe68..9a0997cb1 100644 --- a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java +++ b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java @@ -82,7 +82,7 @@ private void handleSessionEvent(JsonNode params) { CopilotSession session = sessions.get(sessionId); if (session != null && eventNode != null) { - AbstractSessionEvent event = SessionEventParser.parse(eventNode.toString()); + AbstractSessionEvent event = SessionEventParser.parse(eventNode); if (event != null) { session.dispatchEvent(event); } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java index 3ebdf55cb..72e21ef6d 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java @@ -88,36 +88,6 @@ public class SessionEventParser { TYPE_MAP.put("skill.invoked", SkillInvokedEvent.class); } - /** - * Parses a JSON string into the appropriate SessionEvent subclass. - * - * @param json - * the JSON string representing an event - * @return the parsed event, or {@code null} if parsing fails or type is unknown - */ - public static AbstractSessionEvent parse(String json) { - try { - JsonNode node = MAPPER.readTree(json); - String type = node.has("type") ? node.get("type").asText() : null; - - if (type == null) { - LOG.warning("Missing 'type' field in event: " + json); - return null; - } - - Class eventClass = TYPE_MAP.get(type); - if (eventClass == null) { - LOG.fine("Unknown event type: " + type); - return null; - } - - return MAPPER.treeToValue(node, eventClass); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Failed to parse session event", e); - return null; - } - } - /** * Parses a JsonNode into the appropriate SessionEvent subclass. * diff --git a/src/test/java/com/github/copilot/sdk/MetadataApiTest.java b/src/test/java/com/github/copilot/sdk/MetadataApiTest.java index a10b54a3c..857e5ab35 100644 --- a/src/test/java/com/github/copilot/sdk/MetadataApiTest.java +++ b/src/test/java/com/github/copilot/sdk/MetadataApiTest.java @@ -81,7 +81,7 @@ private static String findCopilotInPath() { // ===== ToolExecutionProgressEvent Tests ===== @Test - void testToolExecutionProgressEventParsing() { + void testToolExecutionProgressEventParsing() throws Exception { String json = """ { "type": "tool.execution_progress", @@ -94,7 +94,7 @@ void testToolExecutionProgressEventParsing() { } """; - var event = SessionEventParser.parse(json); + var event = SessionEventParser.parse(MAPPER.readTree(json)); assertNotNull(event); assertInstanceOf(ToolExecutionProgressEvent.class, event); diff --git a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java index e891c03d8..0926eb811 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java @@ -6,11 +6,15 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + import com.github.copilot.sdk.events.*; /** @@ -22,12 +26,23 @@ */ public class SessionEventParserTest { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * Helper to convert a JSON string to a JsonNode and parse via + * {@link SessionEventParser#parse(JsonNode)}. + */ + private static AbstractSessionEvent parseJson(String json) throws Exception { + JsonNode node = MAPPER.readTree(json); + return SessionEventParser.parse(node); + } + // ========================================================================= // Session Events // ========================================================================= @Test - void testParseSessionStartEvent() { + void testParseSessionStartEvent() throws Exception { String json = """ { "type": "session.start", @@ -38,7 +53,7 @@ void testParseSessionStartEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionStartEvent.class, event); assertEquals("session.start", event.getType()); @@ -48,7 +63,7 @@ void testParseSessionStartEvent() { } @Test - void testParseSessionResumeEvent() { + void testParseSessionResumeEvent() throws Exception { String json = """ { "type": "session.resume", @@ -58,14 +73,14 @@ void testParseSessionResumeEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionResumeEvent.class, event); assertEquals("session.resume", event.getType()); } @Test - void testParseSessionErrorEvent() { + void testParseSessionErrorEvent() throws Exception { String json = """ { "type": "session.error", @@ -77,7 +92,7 @@ void testParseSessionErrorEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionErrorEvent.class, event); assertEquals("session.error", event.getType()); @@ -89,7 +104,7 @@ void testParseSessionErrorEvent() { } @Test - void testParseSessionIdleEvent() { + void testParseSessionIdleEvent() throws Exception { String json = """ { "type": "session.idle", @@ -97,14 +112,14 @@ void testParseSessionIdleEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionIdleEvent.class, event); assertEquals("session.idle", event.getType()); } @Test - void testParseSessionInfoEvent() { + void testParseSessionInfoEvent() throws Exception { String json = """ { "type": "session.info", @@ -115,7 +130,7 @@ void testParseSessionInfoEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionInfoEvent.class, event); assertEquals("session.info", event.getType()); @@ -126,7 +141,7 @@ void testParseSessionInfoEvent() { } @Test - void testParseSessionModelChangeEvent() { + void testParseSessionModelChangeEvent() throws Exception { String json = """ { "type": "session.model_change", @@ -137,14 +152,14 @@ void testParseSessionModelChangeEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionModelChangeEvent.class, event); assertEquals("session.model_change", event.getType()); } @Test - void testParseSessionHandoffEvent() { + void testParseSessionHandoffEvent() throws Exception { String json = """ { "type": "session.handoff", @@ -154,14 +169,14 @@ void testParseSessionHandoffEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionHandoffEvent.class, event); assertEquals("session.handoff", event.getType()); } @Test - void testParseSessionTruncationEvent() { + void testParseSessionTruncationEvent() throws Exception { String json = """ { "type": "session.truncation", @@ -171,14 +186,14 @@ void testParseSessionTruncationEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionTruncationEvent.class, event); assertEquals("session.truncation", event.getType()); } @Test - void testParseSessionSnapshotRewindEvent() { + void testParseSessionSnapshotRewindEvent() throws Exception { String json = """ { "type": "session.snapshot_rewind", @@ -188,14 +203,14 @@ void testParseSessionSnapshotRewindEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionSnapshotRewindEvent.class, event); assertEquals("session.snapshot_rewind", event.getType()); } @Test - void testParseSessionUsageInfoEvent() { + void testParseSessionUsageInfoEvent() throws Exception { String json = """ { "type": "session.usage_info", @@ -205,14 +220,14 @@ void testParseSessionUsageInfoEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionUsageInfoEvent.class, event); assertEquals("session.usage_info", event.getType()); } @Test - void testParseSessionCompactionStartEvent() { + void testParseSessionCompactionStartEvent() throws Exception { String json = """ { "type": "session.compaction_start", @@ -220,14 +235,14 @@ void testParseSessionCompactionStartEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionCompactionStartEvent.class, event); assertEquals("session.compaction_start", event.getType()); } @Test - void testParseSessionCompactionCompleteEvent() { + void testParseSessionCompactionCompleteEvent() throws Exception { String json = """ { "type": "session.compaction_complete", @@ -235,7 +250,7 @@ void testParseSessionCompactionCompleteEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionCompactionCompleteEvent.class, event); assertEquals("session.compaction_complete", event.getType()); @@ -246,7 +261,7 @@ void testParseSessionCompactionCompleteEvent() { // ========================================================================= @Test - void testParseUserMessageEvent() { + void testParseUserMessageEvent() throws Exception { String json = """ { "type": "user.message", @@ -257,14 +272,14 @@ void testParseUserMessageEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(UserMessageEvent.class, event); assertEquals("user.message", event.getType()); } @Test - void testParsePendingMessagesModifiedEvent() { + void testParsePendingMessagesModifiedEvent() throws Exception { String json = """ { "type": "pending_messages.modified", @@ -274,7 +289,7 @@ void testParsePendingMessagesModifiedEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(PendingMessagesModifiedEvent.class, event); assertEquals("pending_messages.modified", event.getType()); @@ -285,7 +300,7 @@ void testParsePendingMessagesModifiedEvent() { // ========================================================================= @Test - void testParseAssistantTurnStartEvent() { + void testParseAssistantTurnStartEvent() throws Exception { String json = """ { "type": "assistant.turn_start", @@ -295,7 +310,7 @@ void testParseAssistantTurnStartEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(AssistantTurnStartEvent.class, event); assertEquals("assistant.turn_start", event.getType()); @@ -305,7 +320,7 @@ void testParseAssistantTurnStartEvent() { } @Test - void testParseAssistantIntentEvent() { + void testParseAssistantIntentEvent() throws Exception { String json = """ { "type": "assistant.intent", @@ -315,14 +330,14 @@ void testParseAssistantIntentEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(AssistantIntentEvent.class, event); assertEquals("assistant.intent", event.getType()); } @Test - void testParseAssistantReasoningEvent() { + void testParseAssistantReasoningEvent() throws Exception { String json = """ { "type": "assistant.reasoning", @@ -333,7 +348,7 @@ void testParseAssistantReasoningEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(AssistantReasoningEvent.class, event); assertEquals("assistant.reasoning", event.getType()); @@ -344,7 +359,7 @@ void testParseAssistantReasoningEvent() { } @Test - void testParseAssistantReasoningDeltaEvent() { + void testParseAssistantReasoningDeltaEvent() throws Exception { String json = """ { "type": "assistant.reasoning_delta", @@ -355,14 +370,14 @@ void testParseAssistantReasoningDeltaEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(AssistantReasoningDeltaEvent.class, event); assertEquals("assistant.reasoning_delta", event.getType()); } @Test - void testParseAssistantMessageEvent() { + void testParseAssistantMessageEvent() throws Exception { String json = """ { "type": "assistant.message", @@ -373,7 +388,7 @@ void testParseAssistantMessageEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(AssistantMessageEvent.class, event); assertEquals("assistant.message", event.getType()); @@ -383,7 +398,7 @@ void testParseAssistantMessageEvent() { } @Test - void testParseAssistantMessageDeltaEvent() { + void testParseAssistantMessageDeltaEvent() throws Exception { String json = """ { "type": "assistant.message_delta", @@ -394,14 +409,14 @@ void testParseAssistantMessageDeltaEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(AssistantMessageDeltaEvent.class, event); assertEquals("assistant.message_delta", event.getType()); } @Test - void testParseAssistantTurnEndEvent() { + void testParseAssistantTurnEndEvent() throws Exception { String json = """ { "type": "assistant.turn_end", @@ -411,14 +426,14 @@ void testParseAssistantTurnEndEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(AssistantTurnEndEvent.class, event); assertEquals("assistant.turn_end", event.getType()); } @Test - void testParseAssistantUsageEvent() { + void testParseAssistantUsageEvent() throws Exception { String json = """ { "type": "assistant.usage", @@ -430,7 +445,7 @@ void testParseAssistantUsageEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(AssistantUsageEvent.class, event); assertEquals("assistant.usage", event.getType()); @@ -441,7 +456,7 @@ void testParseAssistantUsageEvent() { // ========================================================================= @Test - void testParseToolUserRequestedEvent() { + void testParseToolUserRequestedEvent() throws Exception { String json = """ { "type": "tool.user_requested", @@ -452,14 +467,14 @@ void testParseToolUserRequestedEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(ToolUserRequestedEvent.class, event); assertEquals("tool.user_requested", event.getType()); } @Test - void testParseToolExecutionStartEvent() { + void testParseToolExecutionStartEvent() throws Exception { String json = """ { "type": "tool.execution_start", @@ -470,14 +485,14 @@ void testParseToolExecutionStartEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(ToolExecutionStartEvent.class, event); assertEquals("tool.execution_start", event.getType()); } @Test - void testParseToolExecutionPartialResultEvent() { + void testParseToolExecutionPartialResultEvent() throws Exception { String json = """ { "type": "tool.execution_partial_result", @@ -488,14 +503,14 @@ void testParseToolExecutionPartialResultEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(ToolExecutionPartialResultEvent.class, event); assertEquals("tool.execution_partial_result", event.getType()); } @Test - void testParseToolExecutionProgressEvent() { + void testParseToolExecutionProgressEvent() throws Exception { String json = """ { "type": "tool.execution_progress", @@ -506,14 +521,14 @@ void testParseToolExecutionProgressEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(ToolExecutionProgressEvent.class, event); assertEquals("tool.execution_progress", event.getType()); } @Test - void testParseToolExecutionCompleteEvent() { + void testParseToolExecutionCompleteEvent() throws Exception { String json = """ { "type": "tool.execution_complete", @@ -528,7 +543,7 @@ void testParseToolExecutionCompleteEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(ToolExecutionCompleteEvent.class, event); assertEquals("tool.execution_complete", event.getType()); @@ -542,7 +557,7 @@ void testParseToolExecutionCompleteEvent() { // ========================================================================= @Test - void testParseSubagentStartedEvent() { + void testParseSubagentStartedEvent() throws Exception { String json = """ { "type": "subagent.started", @@ -555,7 +570,7 @@ void testParseSubagentStartedEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SubagentStartedEvent.class, event); assertEquals("subagent.started", event.getType()); @@ -566,7 +581,7 @@ void testParseSubagentStartedEvent() { } @Test - void testParseSubagentCompletedEvent() { + void testParseSubagentCompletedEvent() throws Exception { String json = """ { "type": "subagent.completed", @@ -577,14 +592,14 @@ void testParseSubagentCompletedEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SubagentCompletedEvent.class, event); assertEquals("subagent.completed", event.getType()); } @Test - void testParseSubagentFailedEvent() { + void testParseSubagentFailedEvent() throws Exception { String json = """ { "type": "subagent.failed", @@ -595,14 +610,14 @@ void testParseSubagentFailedEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SubagentFailedEvent.class, event); assertEquals("subagent.failed", event.getType()); } @Test - void testParseSubagentSelectedEvent() { + void testParseSubagentSelectedEvent() throws Exception { String json = """ { "type": "subagent.selected", @@ -612,7 +627,7 @@ void testParseSubagentSelectedEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SubagentSelectedEvent.class, event); assertEquals("subagent.selected", event.getType()); @@ -623,7 +638,7 @@ void testParseSubagentSelectedEvent() { // ========================================================================= @Test - void testParseHookStartEvent() { + void testParseHookStartEvent() throws Exception { String json = """ { "type": "hook.start", @@ -635,7 +650,7 @@ void testParseHookStartEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(HookStartEvent.class, event); assertEquals("hook.start", event.getType()); @@ -646,7 +661,7 @@ void testParseHookStartEvent() { } @Test - void testParseHookEndEvent() { + void testParseHookEndEvent() throws Exception { String json = """ { "type": "hook.end", @@ -657,7 +672,7 @@ void testParseHookEndEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(HookEndEvent.class, event); assertEquals("hook.end", event.getType()); @@ -668,7 +683,7 @@ void testParseHookEndEvent() { // ========================================================================= @Test - void testParseAbortEvent() { + void testParseAbortEvent() throws Exception { String json = """ { "type": "abort", @@ -678,14 +693,14 @@ void testParseAbortEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(AbortEvent.class, event); assertEquals("abort", event.getType()); } @Test - void testParseSystemMessageEvent() { + void testParseSystemMessageEvent() throws Exception { String json = """ { "type": "system.message", @@ -695,14 +710,14 @@ void testParseSystemMessageEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SystemMessageEvent.class, event); assertEquals("system.message", event.getType()); } @Test - void testParseSessionShutdownEvent() { + void testParseSessionShutdownEvent() throws Exception { String json = """ { "type": "session.shutdown", @@ -722,7 +737,7 @@ void testParseSessionShutdownEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SessionShutdownEvent.class, event); assertEquals("session.shutdown", event.getType()); @@ -736,7 +751,7 @@ void testParseSessionShutdownEvent() { } @Test - void testParseSkillInvokedEvent() { + void testParseSkillInvokedEvent() throws Exception { String json = """ { "type": "skill.invoked", @@ -749,7 +764,7 @@ void testParseSkillInvokedEvent() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event); assertInstanceOf(SkillInvokedEvent.class, event); assertEquals("skill.invoked", event.getType()); @@ -767,7 +782,7 @@ void testParseSkillInvokedEvent() { // ========================================================================= @Test - void testParseUnknownEventType() { + void testParseUnknownEventType() throws Exception { // Unknown types log at FINE level, no need to suppress String json = """ { @@ -776,12 +791,12 @@ void testParseUnknownEventType() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNull(event, "Unknown event types should return null"); } @Test - void testParseMissingTypeField() { + void testParseMissingTypeField() throws Exception { // Suppress logging for this test since missing type logs a WARNING Logger parserLogger = Logger.getLogger(SessionEventParser.class.getName()); Level originalLevel = parserLogger.getLevel(); @@ -796,7 +811,7 @@ void testParseMissingTypeField() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNull(event, "Events without type field should return null"); } finally { parserLogger.setLevel(originalLevel); @@ -804,7 +819,7 @@ void testParseMissingTypeField() { } @Test - void testParseEventWithUnknownFields() { + void testParseEventWithUnknownFields() throws Exception { // Should not fail when there are extra unknown fields String json = """ { @@ -817,42 +832,1209 @@ void testParseEventWithUnknownFields() { } """; - AbstractSessionEvent event = SessionEventParser.parse(json); + AbstractSessionEvent event = parseJson(json); assertNotNull(event, "Events with unknown fields should still parse"); assertInstanceOf(SessionIdleEvent.class, event); } @Test - void testParseInvalidJson() { - // Suppress logging for this test since invalid JSON logs a SEVERE error + void testParseEmptyJson() throws Exception { + // Suppress logging for this test since empty JSON logs a WARNING Logger parserLogger = Logger.getLogger(SessionEventParser.class.getName()); Level originalLevel = parserLogger.getLevel(); parserLogger.setLevel(Level.OFF); try { - String json = "{ this is not valid json }"; + String json = "{}"; - AbstractSessionEvent event = SessionEventParser.parse(json); - assertNull(event, "Invalid JSON should return null"); + AbstractSessionEvent event = parseJson(json); + assertNull(event, "Empty JSON should return null due to missing type"); } finally { parserLogger.setLevel(originalLevel); } } + // ========================================================================= + // All event types in one test + // ========================================================================= + @Test - void testParseEmptyJson() { - // Suppress logging for this test since empty JSON logs a WARNING - Logger parserLogger = Logger.getLogger(SessionEventParser.class.getName()); - Level originalLevel = parserLogger.getLevel(); - parserLogger.setLevel(Level.OFF); + void testParseAllEventTypes() throws Exception { + String[] types = {"session.start", "session.resume", "session.error", "session.idle", "session.info", + "session.model_change", "session.handoff", "session.truncation", "session.snapshot_rewind", + "session.usage_info", "session.compaction_start", "session.compaction_complete", "user.message", + "pending_messages.modified", "assistant.turn_start", "assistant.intent", "assistant.reasoning", + "assistant.reasoning_delta", "assistant.message", "assistant.message_delta", "assistant.turn_end", + "assistant.usage", "abort", "tool.user_requested", "tool.execution_start", + "tool.execution_partial_result", "tool.execution_progress", "tool.execution_complete", + "subagent.started", "subagent.completed", "subagent.failed", "subagent.selected", "hook.start", + "hook.end", "system.message", "session.shutdown", "skill.invoked"}; + + for (String type : types) { + String json = """ + { + "type": "%s", + "data": {} + } + """.formatted(type); + AbstractSessionEvent event = parseJson(json); + assertNotNull(event, "Event type '%s' should parse".formatted(type)); + assertEquals(type, event.getType(), "Parsed type should match for '%s'".formatted(type)); + } + } - try { - String json = "{}"; + // ========================================================================= + // AbstractSessionEvent base fields + // ========================================================================= - AbstractSessionEvent event = SessionEventParser.parse(json); - assertNull(event, "Empty JSON should return null due to missing type"); - } finally { - parserLogger.setLevel(originalLevel); - } + @Test + void testParseBaseFieldsId() throws Exception { + String uuid = "550e8400-e29b-41d4-a716-446655440000"; + String json = """ + { + "type": "session.idle", + "id": "%s", + "data": {} + } + """.formatted(uuid); + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertEquals(UUID.fromString(uuid), event.getId()); + } + + @Test + void testParseBaseFieldsParentId() throws Exception { + String parentUuid = "660e8400-e29b-41d4-a716-446655440001"; + String json = """ + { + "type": "session.idle", + "parentId": "%s", + "data": {} + } + """.formatted(parentUuid); + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertEquals(UUID.fromString(parentUuid), event.getParentId()); + } + + @Test + void testParseBaseFieldsEphemeral() throws Exception { + String json = """ + { + "type": "session.idle", + "ephemeral": true, + "data": {} + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertTrue(event.getEphemeral()); + } + + @Test + void testParseBaseFieldsTimestamp() throws Exception { + String json = """ + { + "type": "session.idle", + "timestamp": "2025-01-15T10:30:00Z", + "data": {} + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertNotNull(event.getTimestamp()); + } + + @Test + void testParseBaseFieldsAllTogether() throws Exception { + String uuid = "550e8400-e29b-41d4-a716-446655440000"; + String parentUuid = "660e8400-e29b-41d4-a716-446655440001"; + String json = """ + { + "type": "assistant.message", + "id": "%s", + "parentId": "%s", + "ephemeral": false, + "timestamp": "2025-06-15T12:00:00+02:00", + "data": { + "content": "Hello" + } + } + """.formatted(uuid, parentUuid); + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertEquals(UUID.fromString(uuid), event.getId()); + assertEquals(UUID.fromString(parentUuid), event.getParentId()); + assertFalse(event.getEphemeral()); + assertNotNull(event.getTimestamp()); + assertInstanceOf(AssistantMessageEvent.class, event); + assertEquals("Hello", ((AssistantMessageEvent) event).getData().getContent()); + } + + @Test + void testParseBaseFieldsNullWhenAbsent() throws Exception { + String json = """ + { + "type": "session.idle", + "data": {} + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertNull(event.getId()); + assertNull(event.getParentId()); + assertNull(event.getEphemeral()); + assertNull(event.getTimestamp()); + } + + // ========================================================================= + // Rich data field assertions + // ========================================================================= + + @Test + void testSessionStartEventAllFields() throws Exception { + String json = """ + { + "type": "session.start", + "data": { + "sessionId": "sess-full", + "version": 2.0, + "producer": "copilot-cli", + "copilotVersion": "1.2.3", + "startTime": "2025-03-01T08:00:00Z", + "selectedModel": "gpt-4-turbo" + } + } + """; + + var event = (SessionStartEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("sess-full", data.getSessionId()); + assertEquals(2.0, data.getVersion()); + assertEquals("copilot-cli", data.getProducer()); + assertEquals("1.2.3", data.getCopilotVersion()); + assertNotNull(data.getStartTime()); + assertEquals("gpt-4-turbo", data.getSelectedModel()); + } + + @Test + void testSessionResumeEventAllFields() throws Exception { + String json = """ + { + "type": "session.resume", + "data": { + "resumeTime": "2025-04-10T09:30:00Z", + "eventCount": 42 + } + } + """; + + var event = (SessionResumeEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertNotNull(data.getResumeTime()); + assertEquals(42.0, data.getEventCount()); + } + + @Test + void testSessionErrorEventAllFields() throws Exception { + String json = """ + { + "type": "session.error", + "data": { + "errorType": "InternalError", + "message": "Something went wrong", + "stack": "at line 42", + "statusCode": 500, + "providerCallId": "prov-err-1" + } + } + """; + + var event = (SessionErrorEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("InternalError", data.getErrorType()); + assertEquals("Something went wrong", data.getMessage()); + assertEquals("at line 42", data.getStack()); + assertEquals(500, data.getStatusCode()); + assertEquals("prov-err-1", data.getProviderCallId()); + } + + @Test + void testSessionModelChangeEventAllFields() throws Exception { + String json = """ + { + "type": "session.model_change", + "data": { + "previousModel": "gpt-4", + "newModel": "gpt-4o" + } + } + """; + + var event = (SessionModelChangeEvent) parseJson(json); + assertNotNull(event); + assertEquals("gpt-4", event.getData().getPreviousModel()); + assertEquals("gpt-4o", event.getData().getNewModel()); + } + + @Test + void testSessionHandoffEventAllFields() throws Exception { + String json = """ + { + "type": "session.handoff", + "data": { + "handoffTime": "2025-05-01T10:00:00Z", + "sourceType": "cli", + "repository": { + "owner": "my-org", + "name": "my-repo", + "branch": "main" + }, + "context": "additional context", + "summary": "handoff summary", + "remoteSessionId": "remote-sess-1" + } + } + """; + + var event = (SessionHandoffEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertNotNull(data.getHandoffTime()); + assertEquals("cli", data.getSourceType()); + assertEquals("additional context", data.getContext()); + assertEquals("handoff summary", data.getSummary()); + assertEquals("remote-sess-1", data.getRemoteSessionId()); + assertNotNull(data.getRepository()); + assertEquals("my-org", data.getRepository().getOwner()); + assertEquals("my-repo", data.getRepository().getName()); + assertEquals("main", data.getRepository().getBranch()); + } + + @Test + void testSessionTruncationEventAllFields() throws Exception { + String json = """ + { + "type": "session.truncation", + "data": { + "tokenLimit": 128000, + "preTruncationTokensInMessages": 150000, + "preTruncationMessagesLength": 100, + "postTruncationTokensInMessages": 120000, + "postTruncationMessagesLength": 80, + "tokensRemovedDuringTruncation": 30000, + "messagesRemovedDuringTruncation": 20, + "performedBy": "system" + } + } + """; + + var event = (SessionTruncationEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals(128000.0, data.getTokenLimit()); + assertEquals(150000.0, data.getPreTruncationTokensInMessages()); + assertEquals(100.0, data.getPreTruncationMessagesLength()); + assertEquals(120000.0, data.getPostTruncationTokensInMessages()); + assertEquals(80.0, data.getPostTruncationMessagesLength()); + assertEquals(30000.0, data.getTokensRemovedDuringTruncation()); + assertEquals(20.0, data.getMessagesRemovedDuringTruncation()); + assertEquals("system", data.getPerformedBy()); + } + + @Test + void testSessionUsageInfoEventAllFields() throws Exception { + String json = """ + { + "type": "session.usage_info", + "data": { + "tokenLimit": 128000, + "currentTokens": 50000, + "messagesLength": 25 + } + } + """; + + var event = (SessionUsageInfoEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals(128000.0, data.getTokenLimit()); + assertEquals(50000.0, data.getCurrentTokens()); + assertEquals(25.0, data.getMessagesLength()); + } + + @Test + void testSessionCompactionCompleteEventAllFields() throws Exception { + String json = """ + { + "type": "session.compaction_complete", + "data": { + "success": true, + "error": null, + "preCompactionTokens": 150000.0, + "postCompactionTokens": 60000.0, + "preCompactionMessagesLength": 100.0, + "messagesRemoved": 50.0, + "tokensRemoved": 90000.0, + "summaryContent": "Compacted conversation", + "checkpointNumber": 3.0, + "checkpointPath": "/checkpoints/3", + "compactionTokensUsed": { + "input": 1000, + "output": 500, + "cachedInput": 200 + }, + "requestId": "req-compact-1" + } + } + """; + + var event = (SessionCompactionCompleteEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertTrue(data.isSuccess()); + assertNull(data.getError()); + assertEquals(150000.0, data.getPreCompactionTokens()); + assertEquals(60000.0, data.getPostCompactionTokens()); + assertEquals(100.0, data.getPreCompactionMessagesLength()); + assertEquals(50.0, data.getMessagesRemoved()); + assertEquals(90000.0, data.getTokensRemoved()); + assertEquals("Compacted conversation", data.getSummaryContent()); + assertEquals(3.0, data.getCheckpointNumber()); + assertEquals("/checkpoints/3", data.getCheckpointPath()); + assertEquals("req-compact-1", data.getRequestId()); + + var tokens = data.getCompactionTokensUsed(); + assertNotNull(tokens); + assertEquals(1000.0, tokens.getInput()); + assertEquals(500.0, tokens.getOutput()); + assertEquals(200.0, tokens.getCachedInput()); + } + + @Test + void testSessionShutdownEventAllFields() throws Exception { + String json = """ + { + "type": "session.shutdown", + "data": { + "shutdownType": "error", + "errorReason": "OOM", + "totalPremiumRequests": 10, + "totalApiDurationMs": 5000.5, + "sessionStartTime": 1700000000000, + "codeChanges": { + "linesAdded": 50, + "linesRemoved": 20, + "filesModified": ["a.java", "b.java", "c.java"] + }, + "modelMetrics": { + "avgLatency": 200 + }, + "currentModel": "gpt-4-turbo" + } + } + """; + + var event = (SessionShutdownEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals(SessionShutdownEvent.ShutdownType.ERROR, data.getShutdownType()); + assertEquals("OOM", data.getErrorReason()); + assertEquals(10.0, data.getTotalPremiumRequests()); + assertEquals(5000.5, data.getTotalApiDurationMs()); + assertEquals(1700000000000.0, data.getSessionStartTime()); + assertEquals("gpt-4-turbo", data.getCurrentModel()); + assertNotNull(data.getModelMetrics()); + + var changes = data.getCodeChanges(); + assertNotNull(changes); + assertEquals(50.0, changes.getLinesAdded()); + assertEquals(20.0, changes.getLinesRemoved()); + assertNotNull(changes.getFilesModified()); + assertEquals(3, changes.getFilesModified().size()); + assertEquals("a.java", changes.getFilesModified().get(0)); + } + + // ========================================================================= + // Assistant events - rich field assertions + // ========================================================================= + + @Test + void testAssistantMessageEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.message", + "data": { + "messageId": "msg-rich", + "content": "Full response", + "toolRequests": [ + { + "toolCallId": "tc-1", + "name": "read_file", + "arguments": {"path": "/tmp/file.txt"} + }, + { + "toolCallId": "tc-2", + "name": "write_file", + "arguments": {"path": "/tmp/out.txt", "content": "hello"} + } + ], + "parentToolCallId": "parent-tc", + "reasoningOpaque": "opaque-data", + "reasoningText": "My reasoning", + "encryptedContent": "enc123" + } + } + """; + + var event = (AssistantMessageEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("msg-rich", data.getMessageId()); + assertEquals("Full response", data.getContent()); + assertEquals("parent-tc", data.getParentToolCallId()); + assertEquals("opaque-data", data.getReasoningOpaque()); + assertEquals("My reasoning", data.getReasoningText()); + assertEquals("enc123", data.getEncryptedContent()); + + assertNotNull(data.getToolRequests()); + assertEquals(2, data.getToolRequests().size()); + assertEquals("tc-1", data.getToolRequests().get(0).getToolCallId()); + assertEquals("read_file", data.getToolRequests().get(0).getName()); + assertNotNull(data.getToolRequests().get(0).getArguments()); + assertEquals("tc-2", data.getToolRequests().get(1).getToolCallId()); + assertEquals("write_file", data.getToolRequests().get(1).getName()); + } + + @Test + void testAssistantMessageDeltaEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.message_delta", + "data": { + "messageId": "msg-delta-1", + "deltaContent": "partial text", + "totalResponseSizeBytes": 4096.0, + "parentToolCallId": "ptc-1" + } + } + """; + + var event = (AssistantMessageDeltaEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("msg-delta-1", data.getMessageId()); + assertEquals("partial text", data.getDeltaContent()); + assertEquals(4096.0, data.getTotalResponseSizeBytes()); + assertEquals("ptc-1", data.getParentToolCallId()); + } + + @Test + void testAssistantUsageEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.usage", + "data": { + "model": "gpt-4-turbo", + "inputTokens": 500, + "outputTokens": 200, + "cacheReadTokens": 50, + "cacheWriteTokens": 150, + "cost": 0.05, + "duration": 1234.5, + "initiator": "user", + "apiCallId": "api-1", + "providerCallId": "prov-1", + "parentToolCallId": "ptc-usage", + "quotaSnapshots": { + "premium": 100, + "standard": 500 + } + } + } + """; + + var event = (AssistantUsageEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("gpt-4-turbo", data.getModel()); + assertEquals(500.0, data.getInputTokens()); + assertEquals(200.0, data.getOutputTokens()); + assertEquals(50.0, data.getCacheReadTokens()); + assertEquals(150.0, data.getCacheWriteTokens()); + assertEquals(0.05, data.getCost()); + assertEquals(1234.5, data.getDuration()); + assertEquals("user", data.getInitiator()); + assertEquals("api-1", data.getApiCallId()); + assertEquals("prov-1", data.getProviderCallId()); + assertEquals("ptc-usage", data.getParentToolCallId()); + assertNotNull(data.getQuotaSnapshots()); + assertEquals(2, data.getQuotaSnapshots().size()); + } + + @Test + void testAssistantReasoningDeltaEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.reasoning_delta", + "data": { + "reasoningId": "r-delta-1", + "deltaContent": "thinking about..." + } + } + """; + + var event = (AssistantReasoningDeltaEvent) parseJson(json); + assertNotNull(event); + assertEquals("r-delta-1", event.getData().getReasoningId()); + assertEquals("thinking about...", event.getData().getDeltaContent()); + } + + @Test + void testAssistantIntentEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.intent", + "data": { + "intent": "refactor_code" + } + } + """; + + var event = (AssistantIntentEvent) parseJson(json); + assertNotNull(event); + assertEquals("refactor_code", event.getData().getIntent()); + } + + @Test + void testAssistantTurnEndEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.turn_end", + "data": { + "turnId": "turn-end-1" + } + } + """; + + var event = (AssistantTurnEndEvent) parseJson(json); + assertNotNull(event); + assertEquals("turn-end-1", event.getData().getTurnId()); + } + + // ========================================================================= + // Tool events - rich field assertions + // ========================================================================= + + @Test + void testToolExecutionStartEventAllFields() throws Exception { + String json = """ + { + "type": "tool.execution_start", + "data": { + "toolCallId": "tc-start-1", + "toolName": "mcp_read_file", + "arguments": {"path": "/tmp/x.txt"}, + "mcpServerName": "filesystem", + "mcpToolName": "read_file", + "parentToolCallId": "ptc-exec" + } + } + """; + + var event = (ToolExecutionStartEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("tc-start-1", data.getToolCallId()); + assertEquals("mcp_read_file", data.getToolName()); + assertNotNull(data.getArguments()); + assertEquals("filesystem", data.getMcpServerName()); + assertEquals("read_file", data.getMcpToolName()); + assertEquals("ptc-exec", data.getParentToolCallId()); + } + + @Test + void testToolExecutionCompleteEventWithError() throws Exception { + String json = """ + { + "type": "tool.execution_complete", + "data": { + "toolCallId": "tc-err-1", + "success": false, + "isUserRequested": true, + "error": { + "message": "File not found", + "code": "ENOENT" + }, + "toolTelemetry": { + "duration": 50, + "retries": 0 + }, + "parentToolCallId": "ptc-complete" + } + } + """; + + var event = (ToolExecutionCompleteEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("tc-err-1", data.getToolCallId()); + assertFalse(data.isSuccess()); + assertTrue(data.getIsUserRequested()); + assertEquals("ptc-complete", data.getParentToolCallId()); + + assertNotNull(data.getError()); + assertEquals("File not found", data.getError().getMessage()); + assertEquals("ENOENT", data.getError().getCode()); + + assertNotNull(data.getToolTelemetry()); + assertEquals(2, data.getToolTelemetry().size()); + } + + @Test + void testToolExecutionCompleteEventWithResult() throws Exception { + String json = """ + { + "type": "tool.execution_complete", + "data": { + "toolCallId": "tc-res-1", + "success": true, + "result": { + "content": "file contents", + "detailedContent": "full detailed contents" + } + } + } + """; + + var event = (ToolExecutionCompleteEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertTrue(data.isSuccess()); + assertNotNull(data.getResult()); + assertEquals("file contents", data.getResult().getContent()); + assertEquals("full detailed contents", data.getResult().getDetailedContent()); + assertNull(data.getError()); + } + + @Test + void testToolExecutionPartialResultEventAllFields() throws Exception { + String json = """ + { + "type": "tool.execution_partial_result", + "data": { + "toolCallId": "tc-partial-1", + "partialOutput": "partial output data" + } + } + """; + + var event = (ToolExecutionPartialResultEvent) parseJson(json); + assertNotNull(event); + assertEquals("tc-partial-1", event.getData().getToolCallId()); + assertEquals("partial output data", event.getData().getPartialOutput()); + } + + @Test + void testToolExecutionProgressEventAllFields() throws Exception { + String json = """ + { + "type": "tool.execution_progress", + "data": { + "toolCallId": "tc-prog-1", + "progressMessage": "50% done" + } + } + """; + + var event = (ToolExecutionProgressEvent) parseJson(json); + assertNotNull(event); + assertEquals("tc-prog-1", event.getData().getToolCallId()); + assertEquals("50% done", event.getData().getProgressMessage()); + } + + @Test + void testToolUserRequestedEventAllFields() throws Exception { + String json = """ + { + "type": "tool.user_requested", + "data": { + "toolCallId": "tc-ur-1", + "toolName": "search_files", + "arguments": {"query": "TODO"} + } + } + """; + + var event = (ToolUserRequestedEvent) parseJson(json); + assertNotNull(event); + assertEquals("tc-ur-1", event.getData().getToolCallId()); + assertEquals("search_files", event.getData().getToolName()); + assertNotNull(event.getData().getArguments()); + } + + // ========================================================================= + // User events - rich field assertions + // ========================================================================= + + @Test + void testUserMessageEventAllFieldsWithAttachments() throws Exception { + String json = """ + { + "type": "user.message", + "data": { + "content": "Please review this file", + "transformedContent": "Transformed: Please review this file", + "source": "editor", + "attachments": [ + { + "type": "file", + "path": "/src/Main.java", + "filePath": "/full/src/Main.java", + "displayName": "Main.java", + "text": "public class Main {}", + "selection": { + "start": { "line": 1, "character": 0 }, + "end": { "line": 5, "character": 10 } + } + } + ] + } + } + """; + + var event = (UserMessageEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("Please review this file", data.getContent()); + assertEquals("Transformed: Please review this file", data.getTransformedContent()); + assertEquals("editor", data.getSource()); + + assertNotNull(data.getAttachments()); + assertEquals(1, data.getAttachments().size()); + + var att = data.getAttachments().get(0); + assertEquals("file", att.getType()); + assertEquals("/src/Main.java", att.getPath()); + assertEquals("/full/src/Main.java", att.getFilePath()); + assertEquals("Main.java", att.getDisplayName()); + assertEquals("public class Main {}", att.getText()); + + assertNotNull(att.getSelection()); + assertNotNull(att.getSelection().getStart()); + assertNotNull(att.getSelection().getEnd()); + assertEquals(1, att.getSelection().getStart().getLine()); + assertEquals(0, att.getSelection().getStart().getCharacter()); + assertEquals(5, att.getSelection().getEnd().getLine()); + assertEquals(10, att.getSelection().getEnd().getCharacter()); + } + + @Test + void testUserMessageEventNoAttachments() throws Exception { + String json = """ + { + "type": "user.message", + "data": { + "content": "Simple message" + } + } + """; + + var event = (UserMessageEvent) parseJson(json); + assertNotNull(event); + assertEquals("Simple message", event.getData().getContent()); + assertNull(event.getData().getAttachments()); + } + + // ========================================================================= + // Subagent events - rich field assertions + // ========================================================================= + + @Test + void testSubagentStartedEventAllFields() throws Exception { + String json = """ + { + "type": "subagent.started", + "data": { + "toolCallId": "tc-sub-1", + "agentName": "test-agent", + "agentDisplayName": "Test Agent", + "agentDescription": "A test subagent" + } + } + """; + + var event = (SubagentStartedEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("tc-sub-1", data.getToolCallId()); + assertEquals("test-agent", data.getAgentName()); + assertEquals("Test Agent", data.getAgentDisplayName()); + assertEquals("A test subagent", data.getAgentDescription()); + } + + @Test + void testSubagentCompletedEventAllFields() throws Exception { + String json = """ + { + "type": "subagent.completed", + "data": { + "toolCallId": "tc-sub-2", + "agentName": "reviewer" + } + } + """; + + var event = (SubagentCompletedEvent) parseJson(json); + assertNotNull(event); + assertEquals("tc-sub-2", event.getData().getToolCallId()); + assertEquals("reviewer", event.getData().getAgentName()); + } + + @Test + void testSubagentFailedEventAllFields() throws Exception { + String json = """ + { + "type": "subagent.failed", + "data": { + "toolCallId": "tc-sub-3", + "agentName": "broken-agent", + "error": "Connection timeout" + } + } + """; + + var event = (SubagentFailedEvent) parseJson(json); + assertNotNull(event); + assertEquals("tc-sub-3", event.getData().getToolCallId()); + assertEquals("broken-agent", event.getData().getAgentName()); + assertEquals("Connection timeout", event.getData().getError()); + } + + @Test + void testSubagentSelectedEventAllFields() throws Exception { + String json = """ + { + "type": "subagent.selected", + "data": { + "agentName": "best-agent", + "agentDisplayName": "Best Agent", + "tools": ["read", "write", "search"] + } + } + """; + + var event = (SubagentSelectedEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("best-agent", data.getAgentName()); + assertEquals("Best Agent", data.getAgentDisplayName()); + assertNotNull(data.getTools()); + assertEquals(3, data.getTools().length); + assertEquals("read", data.getTools()[0]); + assertEquals("write", data.getTools()[1]); + assertEquals("search", data.getTools()[2]); + } + + // ========================================================================= + // Hook events - rich field assertions + // ========================================================================= + + @Test + void testHookStartEventAllFields() throws Exception { + String json = """ + { + "type": "hook.start", + "data": { + "hookInvocationId": "hook-full-1", + "hookType": "postToolUse", + "input": {"toolName": "write_file", "result": "ok"} + } + } + """; + + var event = (HookStartEvent) parseJson(json); + assertNotNull(event); + assertEquals("hook-full-1", event.getData().getHookInvocationId()); + assertEquals("postToolUse", event.getData().getHookType()); + assertNotNull(event.getData().getInput()); + } + + @Test + void testHookEndEventWithError() throws Exception { + String json = """ + { + "type": "hook.end", + "data": { + "hookInvocationId": "hook-err-1", + "hookType": "preToolUse", + "output": null, + "success": false, + "error": { + "message": "Hook validation failed", + "stack": "at HookValidator.validate(line 10)" + } + } + } + """; + + var event = (HookEndEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("hook-err-1", data.getHookInvocationId()); + assertEquals("preToolUse", data.getHookType()); + assertFalse(data.isSuccess()); + assertNotNull(data.getError()); + assertEquals("Hook validation failed", data.getError().getMessage()); + assertEquals("at HookValidator.validate(line 10)", data.getError().getStack()); + } + + @Test + void testHookEndEventSuccess() throws Exception { + String json = """ + { + "type": "hook.end", + "data": { + "hookInvocationId": "hook-ok-1", + "hookType": "preToolUse", + "output": "approved", + "success": true + } + } + """; + + var event = (HookEndEvent) parseJson(json); + assertNotNull(event); + assertTrue(event.getData().isSuccess()); + assertNull(event.getData().getError()); + } + + // ========================================================================= + // Other events - rich field assertions + // ========================================================================= + + @Test + void testAbortEventAllFields() throws Exception { + String json = """ + { + "type": "abort", + "data": { + "reason": "user_cancelled" + } + } + """; + + var event = (AbortEvent) parseJson(json); + assertNotNull(event); + assertEquals("user_cancelled", event.getData().getReason()); + } + + @Test + void testSystemMessageEventAllFields() throws Exception { + String json = """ + { + "type": "system.message", + "data": { + "content": "System notification", + "type": "warning", + "metadata": { + "severity": "high", + "source": "rate-limiter" + } + } + } + """; + + var event = (SystemMessageEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("System notification", data.getContent()); + assertEquals("warning", data.getType()); + assertNotNull(data.getMetadata()); + assertEquals(2, data.getMetadata().size()); + } + + @Test + void testSessionInfoEventAllFields() throws Exception { + String json = """ + { + "type": "session.info", + "data": { + "infoType": "model_selection", + "message": "Using gpt-4-turbo for this task" + } + } + """; + + var event = (SessionInfoEvent) parseJson(json); + assertNotNull(event); + assertEquals("model_selection", event.getData().getInfoType()); + assertEquals("Using gpt-4-turbo for this task", event.getData().getMessage()); + } + + // ========================================================================= + // Null / missing data scenarios + // ========================================================================= + + @Test + void testParseEventWithNullData() throws Exception { + String json = """ + { + "type": "session.idle", + "data": null + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionIdleEvent.class, event); + } + + @Test + void testParseEventWithMissingData() throws Exception { + String json = """ + { + "type": "session.idle" + } + """; + + AbstractSessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionIdleEvent.class, event); + } + + @Test + void testParseNullJsonNode() throws Exception { + Logger parserLogger = Logger.getLogger(SessionEventParser.class.getName()); + Level originalLevel = parserLogger.getLevel(); + parserLogger.setLevel(Level.OFF); + + try { + AbstractSessionEvent event = SessionEventParser.parse((JsonNode) null); + assertNull(event, "Null JsonNode should return null"); + } finally { + parserLogger.setLevel(originalLevel); + } + } + + // ========================================================================= + // Additional data assertion tests + // ========================================================================= + + @Test + void testParseJsonNodeAssistantMessageWithFields() throws Exception { + String json = """ + { + "type": "assistant.message", + "id": "550e8400-e29b-41d4-a716-446655440000", + "ephemeral": true, + "data": { + "messageId": "msg-jn-1", + "content": "Hello from JsonNode", + "toolRequests": [ + { "toolCallId": "tc-jn", "name": "grep", "arguments": {} } + ] + } + } + """; + + var event = (AssistantMessageEvent) parseJson(json); + assertNotNull(event); + assertEquals(UUID.fromString("550e8400-e29b-41d4-a716-446655440000"), event.getId()); + assertTrue(event.getEphemeral()); + assertEquals("msg-jn-1", event.getData().getMessageId()); + assertEquals("Hello from JsonNode", event.getData().getContent()); + assertEquals(1, event.getData().getToolRequests().size()); + assertEquals("tc-jn", event.getData().getToolRequests().get(0).getToolCallId()); + } + + @Test + void testParseJsonNodeToolExecutionCompleteWithNestedTypes() throws Exception { + String json = """ + { + "type": "tool.execution_complete", + "data": { + "toolCallId": "tc-jn-comp", + "success": false, + "error": { + "message": "Permission denied", + "code": "EPERM" + } + } + } + """; + + var event = (ToolExecutionCompleteEvent) parseJson(json); + assertNotNull(event); + assertFalse(event.getData().isSuccess()); + assertEquals("Permission denied", event.getData().getError().getMessage()); + assertEquals("EPERM", event.getData().getError().getCode()); + } + + @Test + void testParseJsonNodeSessionShutdownWithCodeChanges() throws Exception { + String json = """ + { + "type": "session.shutdown", + "data": { + "shutdownType": "routine", + "totalPremiumRequests": 3, + "totalApiDurationMs": 999.9, + "codeChanges": { + "linesAdded": 100, + "linesRemoved": 50, + "filesModified": ["x.java"] + }, + "currentModel": "claude-4" + } + } + """; + + var event = (SessionShutdownEvent) parseJson(json); + assertNotNull(event); + assertEquals(SessionShutdownEvent.ShutdownType.ROUTINE, event.getData().getShutdownType()); + assertEquals(100.0, event.getData().getCodeChanges().getLinesAdded()); + assertEquals(1, event.getData().getCodeChanges().getFilesModified().size()); + } + + @Test + void testParseJsonNodeUserMessageWithAttachment() throws Exception { + String json = """ + { + "type": "user.message", + "data": { + "content": "Check this", + "attachments": [ + { + "type": "code", + "displayName": "snippet.py", + "text": "print('hello')", + "selection": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 14 } + } + } + ] + } + } + """; + + var event = (UserMessageEvent) parseJson(json); + assertNotNull(event); + assertEquals(1, event.getData().getAttachments().size()); + var att = event.getData().getAttachments().get(0); + assertEquals("code", att.getType()); + assertEquals("snippet.py", att.getDisplayName()); + assertEquals(0, att.getSelection().getStart().getLine()); + assertEquals(14, att.getSelection().getEnd().getCharacter()); } } From 29808978c1244071592680bd7dd0d00098174297 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 05:08:14 +0000 Subject: [PATCH 050/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index aa16ccc25..9a69863bd 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -6,13 +6,13 @@ - + coverage coverage - 69.7% - 69.7% + 78.9% + 78.9% From f5e9662621b693677554570f2abf62f6b74d10a7 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 08:34:44 -0500 Subject: [PATCH 051/147] Refactor data access methods in tests to use new naming conventions - Updated test assertions in MetadataApiTest, PermissionsTest, SessionEventHandlingTest, SessionEventParserTest, SessionEventsE2ETest, SkillsTest, and ToolsTest to reflect changes in data access methods from `getX()` to `x()`. - Ensured consistency across all test files by replacing deprecated method calls with the new accessors. --- .../com/github/copilot/sdk/CopilotClient.java | 2 +- .../github/copilot/sdk/CopilotSession.java | 8 +- .../github/copilot/sdk/events/AbortEvent.java | 13 +- .../sdk/events/AbstractSessionEvent.java | 2 +- .../sdk/events/AssistantIntentEvent.java | 13 +- .../events/AssistantMessageDeltaEvent.java | 49 +- .../sdk/events/AssistantMessageEvent.java | 234 +-------- .../events/AssistantReasoningDeltaEvent.java | 25 +- .../sdk/events/AssistantReasoningEvent.java | 25 +- .../sdk/events/AssistantTurnEndEvent.java | 13 +- .../sdk/events/AssistantTurnStartEvent.java | 13 +- .../sdk/events/AssistantUsageEvent.java | 143 +----- .../copilot/sdk/events/HookEndEvent.java | 83 +--- .../copilot/sdk/events/HookStartEvent.java | 36 +- .../events/PendingMessagesModifiedEvent.java | 3 +- .../SessionCompactionCompleteEvent.java | 179 +------ .../events/SessionCompactionStartEvent.java | 3 +- .../copilot/sdk/events/SessionErrorEvent.java | 59 +-- .../sdk/events/SessionHandoffEvent.java | 107 +---- .../copilot/sdk/events/SessionIdleEvent.java | 3 +- .../copilot/sdk/events/SessionInfoEvent.java | 24 +- .../sdk/events/SessionModelChangeEvent.java | 25 +- .../sdk/events/SessionResumeEvent.java | 25 +- .../sdk/events/SessionShutdownEvent.java | 134 +----- .../events/SessionSnapshotRewindEvent.java | 25 +- .../copilot/sdk/events/SessionStartEvent.java | 70 +-- .../sdk/events/SessionTruncationEvent.java | 97 +--- .../sdk/events/SessionUsageInfoEvent.java | 37 +- .../copilot/sdk/events/SkillInvokedEvent.java | 47 +- .../sdk/events/SubagentCompletedEvent.java | 25 +- .../sdk/events/SubagentFailedEvent.java | 36 +- .../sdk/events/SubagentSelectedEvent.java | 36 +- .../sdk/events/SubagentStartedEvent.java | 48 +- .../sdk/events/SystemMessageEvent.java | 36 +- .../events/ToolExecutionCompleteEvent.java | 134 +----- .../ToolExecutionPartialResultEvent.java | 25 +- .../events/ToolExecutionProgressEvent.java | 30 +- .../sdk/events/ToolExecutionStartEvent.java | 71 +-- .../sdk/events/ToolUserRequestedEvent.java | 36 +- .../copilot/sdk/events/UserMessageEvent.java | 199 +------- .../copilot/sdk/events/package-info.java | 8 +- .../com/github/copilot/sdk/package-info.java | 2 +- .../github/copilot/sdk/CompactionTest.java | 8 +- .../copilot/sdk/CopilotSessionTest.java | 62 +-- .../github/copilot/sdk/ErrorHandlingTest.java | 2 +- .../github/copilot/sdk/McpAndAgentsTest.java | 20 +- .../github/copilot/sdk/MetadataApiTest.java | 4 +- .../github/copilot/sdk/PermissionsTest.java | 6 +- .../copilot/sdk/SessionEventHandlingTest.java | 25 +- .../copilot/sdk/SessionEventParserTest.java | 444 +++++++++--------- .../copilot/sdk/SessionEventsE2ETest.java | 2 +- .../com/github/copilot/sdk/SkillsTest.java | 9 +- .../com/github/copilot/sdk/ToolsTest.java | 12 +- 53 files changed, 457 insertions(+), 2320 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 377f6b64a..ad2d76191 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -48,7 +48,7 @@ * var session = client.createSession(new SessionConfig().setModel("gpt-5")).get(); * * session.on(AssistantMessageEvent.class, msg -> { - * System.out.println(msg.getData().getContent()); + * System.out.println(msg.getData().content()); * }); * * session.send(new MessageOptions().setPrompt("Hello!")).get(); diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index ab2f49a29..78bf668ab 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -63,7 +63,7 @@ * * // Register type-safe event handlers * session.on(AssistantMessageEvent.class, msg -> { - * System.out.println(msg.getData().getContent()); + * System.out.println(msg.getData().content()); * }); * session.on(SessionIdleEvent.class, idle -> { * System.out.println("Session is idle"); @@ -338,7 +338,7 @@ public CompletableFuture sendAndWait(MessageOptions optio } else if (evt instanceof SessionIdleEvent) { future.complete(lastAssistantMessage.get()); } else if (evt instanceof SessionErrorEvent errorEvent) { - String message = errorEvent.getData() != null ? errorEvent.getData().getMessage() : "session error"; + String message = errorEvent.getData() != null ? errorEvent.getData().message() : "session error"; future.completeExceptionally(new RuntimeException("Session error: " + message)); } }; @@ -453,7 +453,7 @@ public Closeable on(Consumer handler) { *
{@code
      * // Handle assistant messages
      * session.on(AssistantMessageEvent.class, msg -> {
-     * 	System.out.println(msg.getData().getContent());
+     * 	System.out.println(msg.getData().content());
      * });
      *
      * // Handle session idle
@@ -463,7 +463,7 @@ public Closeable on(Consumer handler) {
      *
      * // Handle streaming deltas
      * session.on(AssistantMessageDeltaEvent.class, delta -> {
-     * 	System.out.print(delta.getData().getDeltaContent());
+     * 	System.out.print(delta.getData().deltaContent());
      * });
      * }
* diff --git a/src/main/java/com/github/copilot/sdk/events/AbortEvent.java b/src/main/java/com/github/copilot/sdk/events/AbortEvent.java index b1e5f0e88..d63185101 100644 --- a/src/main/java/com/github/copilot/sdk/events/AbortEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AbortEvent.java @@ -32,17 +32,6 @@ public void setData(AbortData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class AbortData { - - @JsonProperty("reason") - private String reason; - - public String getReason() { - return reason; - } - - public void setReason(String reason) { - this.reason = reason; - } + public record AbortData(@JsonProperty("reason") String reason) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java b/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java index 72e14e44a..693311395 100644 --- a/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java @@ -37,7 +37,7 @@ *
{@code
  * session.on(event -> {
  * 	if (event instanceof AssistantMessageEvent msg) {
- * 		System.out.println("Assistant: " + msg.getData().getContent());
+ * 		System.out.println("Assistant: " + msg.getData().content());
  * 	} else if (event instanceof SessionIdleEvent) {
  * 		System.out.println("Session is idle");
  * 	}
diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java
index 9b14beb65..e8c693139 100644
--- a/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java
@@ -32,17 +32,6 @@ public void setData(AssistantIntentData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class AssistantIntentData {
-
-        @JsonProperty("intent")
-        private String intent;
-
-        public String getIntent() {
-            return intent;
-        }
-
-        public void setIntent(String intent) {
-            this.intent = intent;
-        }
+    public record AssistantIntentData(@JsonProperty("intent") String intent) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java
index c7714b28b..901b05236 100644
--- a/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java
@@ -32,50 +32,9 @@ public void setData(AssistantMessageDeltaData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class AssistantMessageDeltaData {
-
-        @JsonProperty("messageId")
-        private String messageId;
-
-        @JsonProperty("deltaContent")
-        private String deltaContent;
-
-        @JsonProperty("totalResponseSizeBytes")
-        private Double totalResponseSizeBytes;
-
-        @JsonProperty("parentToolCallId")
-        private String parentToolCallId;
-
-        public String getMessageId() {
-            return messageId;
-        }
-
-        public void setMessageId(String messageId) {
-            this.messageId = messageId;
-        }
-
-        public String getDeltaContent() {
-            return deltaContent;
-        }
-
-        public void setDeltaContent(String deltaContent) {
-            this.deltaContent = deltaContent;
-        }
-
-        public Double getTotalResponseSizeBytes() {
-            return totalResponseSizeBytes;
-        }
-
-        public void setTotalResponseSizeBytes(Double totalResponseSizeBytes) {
-            this.totalResponseSizeBytes = totalResponseSizeBytes;
-        }
-
-        public String getParentToolCallId() {
-            return parentToolCallId;
-        }
-
-        public void setParentToolCallId(String parentToolCallId) {
-            this.parentToolCallId = parentToolCallId;
-        }
+    public record AssistantMessageDeltaData(@JsonProperty("messageId") String messageId,
+            @JsonProperty("deltaContent") String deltaContent,
+            @JsonProperty("totalResponseSizeBytes") Double totalResponseSizeBytes,
+            @JsonProperty("parentToolCallId") String parentToolCallId) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java
index b0be4a75e..22191a5f4 100644
--- a/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java
@@ -21,7 +21,7 @@
  * 
{@code
  * session.on(event -> {
  * 	if (event instanceof AssistantMessageEvent msg) {
- * 		String content = msg.getData().getContent();
+ * 		String content = msg.getData().content();
  * 		System.out.println("Assistant: " + content);
  * 	}
  * });
@@ -70,233 +70,25 @@ public void setData(AssistantMessageData data) {
      * Contains the assistant message content and metadata.
      */
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class AssistantMessageData {
-
-        @JsonProperty("messageId")
-        private String messageId;
-
-        @JsonProperty("content")
-        private String content;
-
-        @JsonProperty("toolRequests")
-        private List toolRequests;
-
-        @JsonProperty("parentToolCallId")
-        private String parentToolCallId;
-
-        @JsonProperty("reasoningOpaque")
-        private String reasoningOpaque;
-
-        @JsonProperty("reasoningText")
-        private String reasoningText;
-
-        @JsonProperty("encryptedContent")
-        private String encryptedContent;
-
-        /**
-         * Gets the unique message identifier.
-         *
-         * @return the message ID
-         */
-        public String getMessageId() {
-            return messageId;
-        }
-
-        /**
-         * Sets the message identifier.
-         *
-         * @param messageId
-         *            the message ID
-         */
-        public void setMessageId(String messageId) {
-            this.messageId = messageId;
-        }
-
-        /**
-         * Gets the text content of the assistant's message.
-         *
-         * @return the message content
-         */
-        public String getContent() {
-            return content;
-        }
-
-        /**
-         * Sets the message content.
-         *
-         * @param content
-         *            the message content
-         */
-        public void setContent(String content) {
-            this.content = content;
-        }
-
-        /**
-         * Gets the list of tool requests made by the assistant.
-         *
-         * @return the tool requests, or {@code null} if none
-         */
-        public List getToolRequests() {
+    public record AssistantMessageData(@JsonProperty("messageId") String messageId,
+            @JsonProperty("content") String content, @JsonProperty("toolRequests") List toolRequests,
+            @JsonProperty("parentToolCallId") String parentToolCallId,
+            @JsonProperty("reasoningOpaque") String reasoningOpaque,
+            @JsonProperty("reasoningText") String reasoningText,
+            @JsonProperty("encryptedContent") String encryptedContent) {
+
+        /** Returns a defensive copy of the tool requests list. */
+        @Override
+        public List toolRequests() {
             return toolRequests == null ? null : Collections.unmodifiableList(toolRequests);
         }
 
-        /**
-         * Sets the tool requests.
-         *
-         * @param toolRequests
-         *            the tool requests
-         */
-        public void setToolRequests(List toolRequests) {
-            this.toolRequests = toolRequests;
-        }
-
-        /**
-         * Gets the parent tool call ID if this message is in response to a tool.
-         *
-         * @return the parent tool call ID, or {@code null}
-         */
-        public String getParentToolCallId() {
-            return parentToolCallId;
-        }
-
-        /**
-         * Sets the parent tool call ID.
-         *
-         * @param parentToolCallId
-         *            the parent tool call ID
-         */
-        public void setParentToolCallId(String parentToolCallId) {
-            this.parentToolCallId = parentToolCallId;
-        }
-
-        /**
-         * Gets the opaque reasoning content (encrypted/encoded).
-         *
-         * @return the opaque reasoning content, or {@code null}
-         */
-        public String getReasoningOpaque() {
-            return reasoningOpaque;
-        }
-
-        /**
-         * Sets the opaque reasoning content.
-         *
-         * @param reasoningOpaque
-         *            the opaque reasoning content
-         */
-        public void setReasoningOpaque(String reasoningOpaque) {
-            this.reasoningOpaque = reasoningOpaque;
-        }
-
-        /**
-         * Gets the human-readable reasoning text.
-         *
-         * @return the reasoning text, or {@code null}
-         */
-        public String getReasoningText() {
-            return reasoningText;
-        }
-
-        /**
-         * Sets the reasoning text.
-         *
-         * @param reasoningText
-         *            the reasoning text
-         */
-        public void setReasoningText(String reasoningText) {
-            this.reasoningText = reasoningText;
-        }
-
-        /**
-         * Gets the encrypted content.
-         *
-         * @return the encrypted content, or {@code null}
-         */
-        public String getEncryptedContent() {
-            return encryptedContent;
-        }
-
-        /**
-         * Sets the encrypted content.
-         *
-         * @param encryptedContent
-         *            the encrypted content
-         */
-        public void setEncryptedContent(String encryptedContent) {
-            this.encryptedContent = encryptedContent;
-        }
-
         /**
          * Represents a request from the assistant to invoke a tool.
          */
         @JsonIgnoreProperties(ignoreUnknown = true)
-        public static class ToolRequest {
-
-            @JsonProperty("toolCallId")
-            private String toolCallId;
-
-            @JsonProperty("name")
-            private String name;
-
-            @JsonProperty("arguments")
-            private Object arguments;
-
-            /**
-             * Gets the unique tool call identifier.
-             *
-             * @return the tool call ID
-             */
-            public String getToolCallId() {
-                return toolCallId;
-            }
-
-            /**
-             * Sets the tool call identifier.
-             *
-             * @param toolCallId
-             *            the tool call ID
-             */
-            public void setToolCallId(String toolCallId) {
-                this.toolCallId = toolCallId;
-            }
-
-            /**
-             * Gets the name of the tool to invoke.
-             *
-             * @return the tool name
-             */
-            public String getName() {
-                return name;
-            }
-
-            /**
-             * Sets the tool name.
-             *
-             * @param name
-             *            the tool name
-             */
-            public void setName(String name) {
-                this.name = name;
-            }
-
-            /**
-             * Gets the arguments to pass to the tool.
-             *
-             * @return the tool arguments (typically a Map or JsonNode)
-             */
-            public Object getArguments() {
-                return arguments;
-            }
-
-            /**
-             * Sets the tool arguments.
-             *
-             * @param arguments
-             *            the tool arguments
-             */
-            public void setArguments(Object arguments) {
-                this.arguments = arguments;
-            }
+        public record ToolRequest(@JsonProperty("toolCallId") String toolCallId, @JsonProperty("name") String name,
+                @JsonProperty("arguments") Object arguments) {
         }
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java
index 2edb447cf..d661f5c32 100644
--- a/src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java
@@ -32,28 +32,7 @@ public void setData(AssistantReasoningDeltaData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class AssistantReasoningDeltaData {
-
-        @JsonProperty("reasoningId")
-        private String reasoningId;
-
-        @JsonProperty("deltaContent")
-        private String deltaContent;
-
-        public String getReasoningId() {
-            return reasoningId;
-        }
-
-        public void setReasoningId(String reasoningId) {
-            this.reasoningId = reasoningId;
-        }
-
-        public String getDeltaContent() {
-            return deltaContent;
-        }
-
-        public void setDeltaContent(String deltaContent) {
-            this.deltaContent = deltaContent;
-        }
+    public record AssistantReasoningDeltaData(@JsonProperty("reasoningId") String reasoningId,
+            @JsonProperty("deltaContent") String deltaContent) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java
index cab00a8df..7019ed71e 100644
--- a/src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java
@@ -32,28 +32,7 @@ public void setData(AssistantReasoningData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class AssistantReasoningData {
-
-        @JsonProperty("reasoningId")
-        private String reasoningId;
-
-        @JsonProperty("content")
-        private String content;
-
-        public String getReasoningId() {
-            return reasoningId;
-        }
-
-        public void setReasoningId(String reasoningId) {
-            this.reasoningId = reasoningId;
-        }
-
-        public String getContent() {
-            return content;
-        }
-
-        public void setContent(String content) {
-            this.content = content;
-        }
+    public record AssistantReasoningData(@JsonProperty("reasoningId") String reasoningId,
+            @JsonProperty("content") String content) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java
index 634f2aaeb..d847ddc6a 100644
--- a/src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java
@@ -32,17 +32,6 @@ public void setData(AssistantTurnEndData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class AssistantTurnEndData {
-
-        @JsonProperty("turnId")
-        private String turnId;
-
-        public String getTurnId() {
-            return turnId;
-        }
-
-        public void setTurnId(String turnId) {
-            this.turnId = turnId;
-        }
+    public record AssistantTurnEndData(@JsonProperty("turnId") String turnId) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java
index 1f0f6aec2..ab5e18653 100644
--- a/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java
@@ -32,17 +32,6 @@ public void setData(AssistantTurnStartData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class AssistantTurnStartData {
-
-        @JsonProperty("turnId")
-        private String turnId;
-
-        public String getTurnId() {
-            return turnId;
-        }
-
-        public void setTurnId(String turnId) {
-            this.turnId = turnId;
-        }
+    public record AssistantTurnStartData(@JsonProperty("turnId") String turnId) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java
index b84664c23..a3c38b03a 100644
--- a/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java
@@ -35,138 +35,19 @@ public void setData(AssistantUsageData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class AssistantUsageData {
-
-        @JsonProperty("model")
-        private String model;
-
-        @JsonProperty("inputTokens")
-        private Double inputTokens;
-
-        @JsonProperty("outputTokens")
-        private Double outputTokens;
-
-        @JsonProperty("cacheReadTokens")
-        private Double cacheReadTokens;
-
-        @JsonProperty("cacheWriteTokens")
-        private Double cacheWriteTokens;
-
-        @JsonProperty("cost")
-        private Double cost;
-
-        @JsonProperty("duration")
-        private Double duration;
-
-        @JsonProperty("initiator")
-        private String initiator;
-
-        @JsonProperty("apiCallId")
-        private String apiCallId;
-
-        @JsonProperty("providerCallId")
-        private String providerCallId;
-
-        @JsonProperty("parentToolCallId")
-        private String parentToolCallId;
-
-        @JsonProperty("quotaSnapshots")
-        private Map quotaSnapshots;
-
-        public String getModel() {
-            return model;
-        }
-
-        public void setModel(String model) {
-            this.model = model;
-        }
-
-        public Double getInputTokens() {
-            return inputTokens;
-        }
-
-        public void setInputTokens(Double inputTokens) {
-            this.inputTokens = inputTokens;
-        }
-
-        public Double getOutputTokens() {
-            return outputTokens;
-        }
-
-        public void setOutputTokens(Double outputTokens) {
-            this.outputTokens = outputTokens;
-        }
-
-        public Double getCacheReadTokens() {
-            return cacheReadTokens;
-        }
-
-        public void setCacheReadTokens(Double cacheReadTokens) {
-            this.cacheReadTokens = cacheReadTokens;
-        }
-
-        public Double getCacheWriteTokens() {
-            return cacheWriteTokens;
-        }
-
-        public void setCacheWriteTokens(Double cacheWriteTokens) {
-            this.cacheWriteTokens = cacheWriteTokens;
-        }
-
-        public Double getCost() {
-            return cost;
-        }
-
-        public void setCost(Double cost) {
-            this.cost = cost;
-        }
-
-        public Double getDuration() {
-            return duration;
-        }
-
-        public void setDuration(Double duration) {
-            this.duration = duration;
-        }
-
-        public String getInitiator() {
-            return initiator;
-        }
-
-        public void setInitiator(String initiator) {
-            this.initiator = initiator;
-        }
-
-        public String getApiCallId() {
-            return apiCallId;
-        }
-
-        public void setApiCallId(String apiCallId) {
-            this.apiCallId = apiCallId;
-        }
-
-        public String getProviderCallId() {
-            return providerCallId;
-        }
-
-        public void setProviderCallId(String providerCallId) {
-            this.providerCallId = providerCallId;
-        }
-
-        public String getParentToolCallId() {
-            return parentToolCallId;
-        }
-
-        public void setParentToolCallId(String parentToolCallId) {
-            this.parentToolCallId = parentToolCallId;
-        }
-
-        public Map getQuotaSnapshots() {
+    public record AssistantUsageData(@JsonProperty("model") String model,
+            @JsonProperty("inputTokens") Double inputTokens, @JsonProperty("outputTokens") Double outputTokens,
+            @JsonProperty("cacheReadTokens") Double cacheReadTokens,
+            @JsonProperty("cacheWriteTokens") Double cacheWriteTokens, @JsonProperty("cost") Double cost,
+            @JsonProperty("duration") Double duration, @JsonProperty("initiator") String initiator,
+            @JsonProperty("apiCallId") String apiCallId, @JsonProperty("providerCallId") String providerCallId,
+            @JsonProperty("parentToolCallId") String parentToolCallId,
+            @JsonProperty("quotaSnapshots") Map quotaSnapshots) {
+
+        /** Returns a defensive copy of the quota snapshots map. */
+        @Override
+        public Map quotaSnapshots() {
             return quotaSnapshots == null ? null : Collections.unmodifiableMap(quotaSnapshots);
         }
-
-        public void setQuotaSnapshots(Map quotaSnapshots) {
-            this.quotaSnapshots = quotaSnapshots;
-        }
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/HookEndEvent.java b/src/main/java/com/github/copilot/sdk/events/HookEndEvent.java
index 3de1723b5..9ace18171 100644
--- a/src/main/java/com/github/copilot/sdk/events/HookEndEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/HookEndEvent.java
@@ -32,87 +32,12 @@ public void setData(HookEndData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class HookEndData {
-
-        @JsonProperty("hookInvocationId")
-        private String hookInvocationId;
-
-        @JsonProperty("hookType")
-        private String hookType;
-
-        @JsonProperty("output")
-        private Object output;
-
-        @JsonProperty("success")
-        private boolean success;
-
-        @JsonProperty("error")
-        private HookError error;
-
-        public String getHookInvocationId() {
-            return hookInvocationId;
-        }
-
-        public void setHookInvocationId(String hookInvocationId) {
-            this.hookInvocationId = hookInvocationId;
-        }
-
-        public String getHookType() {
-            return hookType;
-        }
-
-        public void setHookType(String hookType) {
-            this.hookType = hookType;
-        }
-
-        public Object getOutput() {
-            return output;
-        }
-
-        public void setOutput(Object output) {
-            this.output = output;
-        }
-
-        public boolean isSuccess() {
-            return success;
-        }
-
-        public void setSuccess(boolean success) {
-            this.success = success;
-        }
-
-        public HookError getError() {
-            return error;
-        }
-
-        public void setError(HookError error) {
-            this.error = error;
-        }
+    public record HookEndData(@JsonProperty("hookInvocationId") String hookInvocationId,
+            @JsonProperty("hookType") String hookType, @JsonProperty("output") Object output,
+            @JsonProperty("success") boolean success, @JsonProperty("error") HookError error) {
 
         @JsonIgnoreProperties(ignoreUnknown = true)
-        public static class HookError {
-
-            @JsonProperty("message")
-            private String message;
-
-            @JsonProperty("stack")
-            private String stack;
-
-            public String getMessage() {
-                return message;
-            }
-
-            public void setMessage(String message) {
-                this.message = message;
-            }
-
-            public String getStack() {
-                return stack;
-            }
-
-            public void setStack(String stack) {
-                this.stack = stack;
-            }
+        public record HookError(@JsonProperty("message") String message, @JsonProperty("stack") String stack) {
         }
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/HookStartEvent.java b/src/main/java/com/github/copilot/sdk/events/HookStartEvent.java
index 263c06622..9c1f5c0ea 100644
--- a/src/main/java/com/github/copilot/sdk/events/HookStartEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/HookStartEvent.java
@@ -32,39 +32,7 @@ public void setData(HookStartData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class HookStartData {
-
-        @JsonProperty("hookInvocationId")
-        private String hookInvocationId;
-
-        @JsonProperty("hookType")
-        private String hookType;
-
-        @JsonProperty("input")
-        private Object input;
-
-        public String getHookInvocationId() {
-            return hookInvocationId;
-        }
-
-        public void setHookInvocationId(String hookInvocationId) {
-            this.hookInvocationId = hookInvocationId;
-        }
-
-        public String getHookType() {
-            return hookType;
-        }
-
-        public void setHookType(String hookType) {
-            this.hookType = hookType;
-        }
-
-        public Object getInput() {
-            return input;
-        }
-
-        public void setInput(Object input) {
-            this.input = input;
-        }
+    public record HookStartData(@JsonProperty("hookInvocationId") String hookInvocationId,
+            @JsonProperty("hookType") String hookType, @JsonProperty("input") Object input) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java b/src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java
index 49b9b62d8..6eb863c78 100644
--- a/src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java
@@ -32,7 +32,6 @@ public void setData(PendingMessagesModifiedData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class PendingMessagesModifiedData {
-        // Empty data
+    public record PendingMessagesModifiedData() {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java
index b47cc3eb5..57036e9d8 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java
@@ -32,178 +32,23 @@ public void setData(SessionCompactionCompleteData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionCompactionCompleteData {
-
-        @JsonProperty("success")
-        private boolean success;
-
-        @JsonProperty("error")
-        private String error;
-
-        @JsonProperty("preCompactionTokens")
-        private Double preCompactionTokens;
-
-        @JsonProperty("postCompactionTokens")
-        private Double postCompactionTokens;
-
-        @JsonProperty("preCompactionMessagesLength")
-        private Double preCompactionMessagesLength;
-
-        @JsonProperty("messagesRemoved")
-        private Double messagesRemoved;
-
-        @JsonProperty("tokensRemoved")
-        private Double tokensRemoved;
-
-        @JsonProperty("summaryContent")
-        private String summaryContent;
-
-        @JsonProperty("checkpointNumber")
-        private Double checkpointNumber;
-
-        @JsonProperty("checkpointPath")
-        private String checkpointPath;
-
-        @JsonProperty("compactionTokensUsed")
-        private CompactionTokensUsed compactionTokensUsed;
-
-        @JsonProperty("requestId")
-        private String requestId;
-
-        public boolean isSuccess() {
-            return success;
-        }
-
-        public void setSuccess(boolean success) {
-            this.success = success;
-        }
-
-        public String getError() {
-            return error;
-        }
-
-        public void setError(String error) {
-            this.error = error;
-        }
-
-        public Double getPreCompactionTokens() {
-            return preCompactionTokens;
-        }
-
-        public void setPreCompactionTokens(Double preCompactionTokens) {
-            this.preCompactionTokens = preCompactionTokens;
-        }
-
-        public Double getPostCompactionTokens() {
-            return postCompactionTokens;
-        }
-
-        public void setPostCompactionTokens(Double postCompactionTokens) {
-            this.postCompactionTokens = postCompactionTokens;
-        }
-
-        public Double getPreCompactionMessagesLength() {
-            return preCompactionMessagesLength;
-        }
-
-        public void setPreCompactionMessagesLength(Double preCompactionMessagesLength) {
-            this.preCompactionMessagesLength = preCompactionMessagesLength;
-        }
-
-        public Double getMessagesRemoved() {
-            return messagesRemoved;
-        }
-
-        public void setMessagesRemoved(Double messagesRemoved) {
-            this.messagesRemoved = messagesRemoved;
-        }
-
-        public Double getTokensRemoved() {
-            return tokensRemoved;
-        }
-
-        public void setTokensRemoved(Double tokensRemoved) {
-            this.tokensRemoved = tokensRemoved;
-        }
-
-        public String getSummaryContent() {
-            return summaryContent;
-        }
-
-        public void setSummaryContent(String summaryContent) {
-            this.summaryContent = summaryContent;
-        }
-
-        public Double getCheckpointNumber() {
-            return checkpointNumber;
-        }
-
-        public void setCheckpointNumber(Double checkpointNumber) {
-            this.checkpointNumber = checkpointNumber;
-        }
-
-        public String getCheckpointPath() {
-            return checkpointPath;
-        }
-
-        public void setCheckpointPath(String checkpointPath) {
-            this.checkpointPath = checkpointPath;
-        }
-
-        public CompactionTokensUsed getCompactionTokensUsed() {
-            return compactionTokensUsed;
-        }
-
-        public void setCompactionTokensUsed(CompactionTokensUsed compactionTokensUsed) {
-            this.compactionTokensUsed = compactionTokensUsed;
-        }
-
-        public String getRequestId() {
-            return requestId;
-        }
-
-        public void setRequestId(String requestId) {
-            this.requestId = requestId;
-        }
+    public record SessionCompactionCompleteData(@JsonProperty("success") boolean success,
+            @JsonProperty("error") String error, @JsonProperty("preCompactionTokens") Double preCompactionTokens,
+            @JsonProperty("postCompactionTokens") Double postCompactionTokens,
+            @JsonProperty("preCompactionMessagesLength") Double preCompactionMessagesLength,
+            @JsonProperty("messagesRemoved") Double messagesRemoved,
+            @JsonProperty("tokensRemoved") Double tokensRemoved, @JsonProperty("summaryContent") String summaryContent,
+            @JsonProperty("checkpointNumber") Double checkpointNumber,
+            @JsonProperty("checkpointPath") String checkpointPath,
+            @JsonProperty("compactionTokensUsed") CompactionTokensUsed compactionTokensUsed,
+            @JsonProperty("requestId") String requestId) {
     }
 
     /**
      * Token usage information for the compaction operation.
      */
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class CompactionTokensUsed {
-
-        @JsonProperty("input")
-        private double input;
-
-        @JsonProperty("output")
-        private double output;
-
-        @JsonProperty("cachedInput")
-        private double cachedInput;
-
-        public double getInput() {
-            return input;
-        }
-
-        public void setInput(double input) {
-            this.input = input;
-        }
-
-        public double getOutput() {
-            return output;
-        }
-
-        public void setOutput(double output) {
-            this.output = output;
-        }
-
-        public double getCachedInput() {
-            return cachedInput;
-        }
-
-        public void setCachedInput(double cachedInput) {
-            this.cachedInput = cachedInput;
-        }
+    public record CompactionTokensUsed(@JsonProperty("input") double input, @JsonProperty("output") double output,
+            @JsonProperty("cachedInput") double cachedInput) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java
index 15eec1021..ad29b37f8 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java
@@ -32,7 +32,6 @@ public void setData(SessionCompactionStartData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionCompactionStartData {
-        // Empty data
+    public record SessionCompactionStartData() {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java
index 54adbe789..ffa9e6d9e 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java
@@ -32,61 +32,8 @@ public void setData(SessionErrorData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionErrorData {
-
-        @JsonProperty("errorType")
-        private String errorType;
-
-        @JsonProperty("message")
-        private String message;
-
-        @JsonProperty("stack")
-        private String stack;
-
-        @JsonProperty("statusCode")
-        private Double statusCode;
-
-        @JsonProperty("providerCallId")
-        private String providerCallId;
-
-        public String getErrorType() {
-            return errorType;
-        }
-
-        public void setErrorType(String errorType) {
-            this.errorType = errorType;
-        }
-
-        public String getMessage() {
-            return message;
-        }
-
-        public void setMessage(String message) {
-            this.message = message;
-        }
-
-        public String getStack() {
-            return stack;
-        }
-
-        public void setStack(String stack) {
-            this.stack = stack;
-        }
-
-        public Double getStatusCode() {
-            return statusCode;
-        }
-
-        public void setStatusCode(Double statusCode) {
-            this.statusCode = statusCode;
-        }
-
-        public String getProviderCallId() {
-            return providerCallId;
-        }
-
-        public void setProviderCallId(String providerCallId) {
-            this.providerCallId = providerCallId;
-        }
+    public record SessionErrorData(@JsonProperty("errorType") String errorType, @JsonProperty("message") String message,
+            @JsonProperty("stack") String stack, @JsonProperty("statusCode") Double statusCode,
+            @JsonProperty("providerCallId") String providerCallId) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java
index 74c084158..08e8f8c42 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java
@@ -34,109 +34,14 @@ public void setData(SessionHandoffData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionHandoffData {
-
-        @JsonProperty("handoffTime")
-        private OffsetDateTime handoffTime;
-
-        @JsonProperty("sourceType")
-        private String sourceType;
-
-        @JsonProperty("repository")
-        private Repository repository;
-
-        @JsonProperty("context")
-        private String context;
-
-        @JsonProperty("summary")
-        private String summary;
-
-        @JsonProperty("remoteSessionId")
-        private String remoteSessionId;
-
-        public OffsetDateTime getHandoffTime() {
-            return handoffTime;
-        }
-
-        public void setHandoffTime(OffsetDateTime handoffTime) {
-            this.handoffTime = handoffTime;
-        }
-
-        public String getSourceType() {
-            return sourceType;
-        }
-
-        public void setSourceType(String sourceType) {
-            this.sourceType = sourceType;
-        }
-
-        public Repository getRepository() {
-            return repository;
-        }
-
-        public void setRepository(Repository repository) {
-            this.repository = repository;
-        }
-
-        public String getContext() {
-            return context;
-        }
-
-        public void setContext(String context) {
-            this.context = context;
-        }
-
-        public String getSummary() {
-            return summary;
-        }
-
-        public void setSummary(String summary) {
-            this.summary = summary;
-        }
-
-        public String getRemoteSessionId() {
-            return remoteSessionId;
-        }
-
-        public void setRemoteSessionId(String remoteSessionId) {
-            this.remoteSessionId = remoteSessionId;
-        }
+    public record SessionHandoffData(@JsonProperty("handoffTime") OffsetDateTime handoffTime,
+            @JsonProperty("sourceType") String sourceType, @JsonProperty("repository") Repository repository,
+            @JsonProperty("context") String context, @JsonProperty("summary") String summary,
+            @JsonProperty("remoteSessionId") String remoteSessionId) {
 
         @JsonIgnoreProperties(ignoreUnknown = true)
-        public static class Repository {
-
-            @JsonProperty("owner")
-            private String owner;
-
-            @JsonProperty("name")
-            private String name;
-
-            @JsonProperty("branch")
-            private String branch;
-
-            public String getOwner() {
-                return owner;
-            }
-
-            public void setOwner(String owner) {
-                this.owner = owner;
-            }
-
-            public String getName() {
-                return name;
-            }
-
-            public void setName(String name) {
-                this.name = name;
-            }
-
-            public String getBranch() {
-                return branch;
-            }
-
-            public void setBranch(String branch) {
-                this.branch = branch;
-            }
+        public record Repository(@JsonProperty("owner") String owner, @JsonProperty("name") String name,
+                @JsonProperty("branch") String branch) {
         }
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java
index 904c55df0..bb77b6ed0 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java
@@ -32,7 +32,6 @@ public void setData(SessionIdleData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionIdleData {
-        // Empty data
+    public record SessionIdleData() {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java
index 664be4bf4..dd1fe7ccb 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java
@@ -32,28 +32,6 @@ public void setData(SessionInfoData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionInfoData {
-
-        @JsonProperty("infoType")
-        private String infoType;
-
-        @JsonProperty("message")
-        private String message;
-
-        public String getInfoType() {
-            return infoType;
-        }
-
-        public void setInfoType(String infoType) {
-            this.infoType = infoType;
-        }
-
-        public String getMessage() {
-            return message;
-        }
-
-        public void setMessage(String message) {
-            this.message = message;
-        }
+    public record SessionInfoData(@JsonProperty("infoType") String infoType, @JsonProperty("message") String message) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java
index 7f0e21c6d..57d0b5499 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java
@@ -32,28 +32,7 @@ public void setData(SessionModelChangeData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionModelChangeData {
-
-        @JsonProperty("previousModel")
-        private String previousModel;
-
-        @JsonProperty("newModel")
-        private String newModel;
-
-        public String getPreviousModel() {
-            return previousModel;
-        }
-
-        public void setPreviousModel(String previousModel) {
-            this.previousModel = previousModel;
-        }
-
-        public String getNewModel() {
-            return newModel;
-        }
-
-        public void setNewModel(String newModel) {
-            this.newModel = newModel;
-        }
+    public record SessionModelChangeData(@JsonProperty("previousModel") String previousModel,
+            @JsonProperty("newModel") String newModel) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java
index 4d31dd9a8..bf305bc30 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java
@@ -34,28 +34,7 @@ public void setData(SessionResumeData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionResumeData {
-
-        @JsonProperty("resumeTime")
-        private OffsetDateTime resumeTime;
-
-        @JsonProperty("eventCount")
-        private double eventCount;
-
-        public OffsetDateTime getResumeTime() {
-            return resumeTime;
-        }
-
-        public void setResumeTime(OffsetDateTime resumeTime) {
-            this.resumeTime = resumeTime;
-        }
-
-        public double getEventCount() {
-            return eventCount;
-        }
-
-        public void setEventCount(double eventCount) {
-            this.eventCount = eventCount;
-        }
+    public record SessionResumeData(@JsonProperty("resumeTime") OffsetDateTime resumeTime,
+            @JsonProperty("eventCount") double eventCount) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionShutdownEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionShutdownEvent.java
index 4d8fc5d03..9a128500a 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionShutdownEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionShutdownEvent.java
@@ -41,135 +41,23 @@ public void setData(SessionShutdownData data) {
      * Data for the session shutdown event.
      */
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionShutdownData {
-
-        @JsonProperty("shutdownType")
-        private ShutdownType shutdownType;
-
-        @JsonProperty("errorReason")
-        private String errorReason;
-
-        @JsonProperty("totalPremiumRequests")
-        private double totalPremiumRequests;
-
-        @JsonProperty("totalApiDurationMs")
-        private double totalApiDurationMs;
-
-        @JsonProperty("sessionStartTime")
-        private double sessionStartTime;
-
-        @JsonProperty("codeChanges")
-        private CodeChanges codeChanges;
-
-        @JsonProperty("modelMetrics")
-        private Map modelMetrics;
-
-        @JsonProperty("currentModel")
-        private String currentModel;
-
-        public ShutdownType getShutdownType() {
-            return shutdownType;
-        }
-
-        public void setShutdownType(ShutdownType shutdownType) {
-            this.shutdownType = shutdownType;
-        }
-
-        public String getErrorReason() {
-            return errorReason;
-        }
-
-        public void setErrorReason(String errorReason) {
-            this.errorReason = errorReason;
-        }
-
-        public double getTotalPremiumRequests() {
-            return totalPremiumRequests;
-        }
-
-        public void setTotalPremiumRequests(double totalPremiumRequests) {
-            this.totalPremiumRequests = totalPremiumRequests;
-        }
-
-        public double getTotalApiDurationMs() {
-            return totalApiDurationMs;
-        }
-
-        public void setTotalApiDurationMs(double totalApiDurationMs) {
-            this.totalApiDurationMs = totalApiDurationMs;
-        }
-
-        public double getSessionStartTime() {
-            return sessionStartTime;
-        }
-
-        public void setSessionStartTime(double sessionStartTime) {
-            this.sessionStartTime = sessionStartTime;
-        }
-
-        public CodeChanges getCodeChanges() {
-            return codeChanges;
-        }
-
-        public void setCodeChanges(CodeChanges codeChanges) {
-            this.codeChanges = codeChanges;
-        }
-
-        public Map getModelMetrics() {
-            return modelMetrics;
-        }
-
-        public void setModelMetrics(Map modelMetrics) {
-            this.modelMetrics = modelMetrics;
-        }
-
-        public String getCurrentModel() {
-            return currentModel;
-        }
-
-        public void setCurrentModel(String currentModel) {
-            this.currentModel = currentModel;
-        }
+    public record SessionShutdownData(@JsonProperty("shutdownType") ShutdownType shutdownType,
+            @JsonProperty("errorReason") String errorReason,
+            @JsonProperty("totalPremiumRequests") double totalPremiumRequests,
+            @JsonProperty("totalApiDurationMs") double totalApiDurationMs,
+            @JsonProperty("sessionStartTime") double sessionStartTime,
+            @JsonProperty("codeChanges") CodeChanges codeChanges,
+            @JsonProperty("modelMetrics") Map modelMetrics,
+            @JsonProperty("currentModel") String currentModel) {
     }
 
     /**
      * Code changes made during the session.
      */
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class CodeChanges {
-
-        @JsonProperty("linesAdded")
-        private double linesAdded;
-
-        @JsonProperty("linesRemoved")
-        private double linesRemoved;
-
-        @JsonProperty("filesModified")
-        private List filesModified;
-
-        public double getLinesAdded() {
-            return linesAdded;
-        }
-
-        public void setLinesAdded(double linesAdded) {
-            this.linesAdded = linesAdded;
-        }
-
-        public double getLinesRemoved() {
-            return linesRemoved;
-        }
-
-        public void setLinesRemoved(double linesRemoved) {
-            this.linesRemoved = linesRemoved;
-        }
-
-        public List getFilesModified() {
-            return filesModified;
-        }
-
-        public void setFilesModified(List filesModified) {
-            this.filesModified = filesModified;
-        }
+    public record CodeChanges(@JsonProperty("linesAdded") double linesAdded,
+            @JsonProperty("linesRemoved") double linesRemoved,
+            @JsonProperty("filesModified") List filesModified) {
     }
 
     /**
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionSnapshotRewindEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionSnapshotRewindEvent.java
index 3345fa34f..ae7b0f3c5 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionSnapshotRewindEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionSnapshotRewindEvent.java
@@ -34,28 +34,7 @@ public void setData(SessionSnapshotRewindData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionSnapshotRewindData {
-
-        @JsonProperty("upToEventId")
-        private String upToEventId;
-
-        @JsonProperty("eventsRemoved")
-        private int eventsRemoved;
-
-        public String getUpToEventId() {
-            return upToEventId;
-        }
-
-        public void setUpToEventId(String upToEventId) {
-            this.upToEventId = upToEventId;
-        }
-
-        public int getEventsRemoved() {
-            return eventsRemoved;
-        }
-
-        public void setEventsRemoved(int eventsRemoved) {
-            this.eventsRemoved = eventsRemoved;
-        }
+    public record SessionSnapshotRewindData(@JsonProperty("upToEventId") String upToEventId,
+            @JsonProperty("eventsRemoved") int eventsRemoved) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java
index 6cb3b18d8..317b4a470 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java
@@ -34,72 +34,8 @@ public void setData(SessionStartData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionStartData {
-
-        @JsonProperty("sessionId")
-        private String sessionId;
-
-        @JsonProperty("version")
-        private double version;
-
-        @JsonProperty("producer")
-        private String producer;
-
-        @JsonProperty("copilotVersion")
-        private String copilotVersion;
-
-        @JsonProperty("startTime")
-        private OffsetDateTime startTime;
-
-        @JsonProperty("selectedModel")
-        private String selectedModel;
-
-        public String getSessionId() {
-            return sessionId;
-        }
-
-        public void setSessionId(String sessionId) {
-            this.sessionId = sessionId;
-        }
-
-        public double getVersion() {
-            return version;
-        }
-
-        public void setVersion(double version) {
-            this.version = version;
-        }
-
-        public String getProducer() {
-            return producer;
-        }
-
-        public void setProducer(String producer) {
-            this.producer = producer;
-        }
-
-        public String getCopilotVersion() {
-            return copilotVersion;
-        }
-
-        public void setCopilotVersion(String copilotVersion) {
-            this.copilotVersion = copilotVersion;
-        }
-
-        public OffsetDateTime getStartTime() {
-            return startTime;
-        }
-
-        public void setStartTime(OffsetDateTime startTime) {
-            this.startTime = startTime;
-        }
-
-        public String getSelectedModel() {
-            return selectedModel;
-        }
-
-        public void setSelectedModel(String selectedModel) {
-            this.selectedModel = selectedModel;
-        }
+    public record SessionStartData(@JsonProperty("sessionId") String sessionId, @JsonProperty("version") double version,
+            @JsonProperty("producer") String producer, @JsonProperty("copilotVersion") String copilotVersion,
+            @JsonProperty("startTime") OffsetDateTime startTime, @JsonProperty("selectedModel") String selectedModel) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java
index f5d74626b..04971cb97 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java
@@ -32,94 +32,13 @@ public void setData(SessionTruncationData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionTruncationData {
-
-        @JsonProperty("tokenLimit")
-        private double tokenLimit;
-
-        @JsonProperty("preTruncationTokensInMessages")
-        private double preTruncationTokensInMessages;
-
-        @JsonProperty("preTruncationMessagesLength")
-        private double preTruncationMessagesLength;
-
-        @JsonProperty("postTruncationTokensInMessages")
-        private double postTruncationTokensInMessages;
-
-        @JsonProperty("postTruncationMessagesLength")
-        private double postTruncationMessagesLength;
-
-        @JsonProperty("tokensRemovedDuringTruncation")
-        private double tokensRemovedDuringTruncation;
-
-        @JsonProperty("messagesRemovedDuringTruncation")
-        private double messagesRemovedDuringTruncation;
-
-        @JsonProperty("performedBy")
-        private String performedBy;
-
-        public double getTokenLimit() {
-            return tokenLimit;
-        }
-
-        public void setTokenLimit(double tokenLimit) {
-            this.tokenLimit = tokenLimit;
-        }
-
-        public double getPreTruncationTokensInMessages() {
-            return preTruncationTokensInMessages;
-        }
-
-        public void setPreTruncationTokensInMessages(double preTruncationTokensInMessages) {
-            this.preTruncationTokensInMessages = preTruncationTokensInMessages;
-        }
-
-        public double getPreTruncationMessagesLength() {
-            return preTruncationMessagesLength;
-        }
-
-        public void setPreTruncationMessagesLength(double preTruncationMessagesLength) {
-            this.preTruncationMessagesLength = preTruncationMessagesLength;
-        }
-
-        public double getPostTruncationTokensInMessages() {
-            return postTruncationTokensInMessages;
-        }
-
-        public void setPostTruncationTokensInMessages(double postTruncationTokensInMessages) {
-            this.postTruncationTokensInMessages = postTruncationTokensInMessages;
-        }
-
-        public double getPostTruncationMessagesLength() {
-            return postTruncationMessagesLength;
-        }
-
-        public void setPostTruncationMessagesLength(double postTruncationMessagesLength) {
-            this.postTruncationMessagesLength = postTruncationMessagesLength;
-        }
-
-        public double getTokensRemovedDuringTruncation() {
-            return tokensRemovedDuringTruncation;
-        }
-
-        public void setTokensRemovedDuringTruncation(double tokensRemovedDuringTruncation) {
-            this.tokensRemovedDuringTruncation = tokensRemovedDuringTruncation;
-        }
-
-        public double getMessagesRemovedDuringTruncation() {
-            return messagesRemovedDuringTruncation;
-        }
-
-        public void setMessagesRemovedDuringTruncation(double messagesRemovedDuringTruncation) {
-            this.messagesRemovedDuringTruncation = messagesRemovedDuringTruncation;
-        }
-
-        public String getPerformedBy() {
-            return performedBy;
-        }
-
-        public void setPerformedBy(String performedBy) {
-            this.performedBy = performedBy;
-        }
+    public record SessionTruncationData(@JsonProperty("tokenLimit") double tokenLimit,
+            @JsonProperty("preTruncationTokensInMessages") double preTruncationTokensInMessages,
+            @JsonProperty("preTruncationMessagesLength") double preTruncationMessagesLength,
+            @JsonProperty("postTruncationTokensInMessages") double postTruncationTokensInMessages,
+            @JsonProperty("postTruncationMessagesLength") double postTruncationMessagesLength,
+            @JsonProperty("tokensRemovedDuringTruncation") double tokensRemovedDuringTruncation,
+            @JsonProperty("messagesRemovedDuringTruncation") double messagesRemovedDuringTruncation,
+            @JsonProperty("performedBy") String performedBy) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java
index 808c855cb..1bf10d7de 100644
--- a/src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java
@@ -32,39 +32,8 @@ public void setData(SessionUsageInfoData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SessionUsageInfoData {
-
-        @JsonProperty("tokenLimit")
-        private double tokenLimit;
-
-        @JsonProperty("currentTokens")
-        private double currentTokens;
-
-        @JsonProperty("messagesLength")
-        private double messagesLength;
-
-        public double getTokenLimit() {
-            return tokenLimit;
-        }
-
-        public void setTokenLimit(double tokenLimit) {
-            this.tokenLimit = tokenLimit;
-        }
-
-        public double getCurrentTokens() {
-            return currentTokens;
-        }
-
-        public void setCurrentTokens(double currentTokens) {
-            this.currentTokens = currentTokens;
-        }
-
-        public double getMessagesLength() {
-            return messagesLength;
-        }
-
-        public void setMessagesLength(double messagesLength) {
-            this.messagesLength = messagesLength;
-        }
+    public record SessionUsageInfoData(@JsonProperty("tokenLimit") double tokenLimit,
+            @JsonProperty("currentTokens") double currentTokens,
+            @JsonProperty("messagesLength") double messagesLength) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SkillInvokedEvent.java b/src/main/java/com/github/copilot/sdk/events/SkillInvokedEvent.java
index 9617c2b68..febe01253 100644
--- a/src/main/java/com/github/copilot/sdk/events/SkillInvokedEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SkillInvokedEvent.java
@@ -39,50 +39,7 @@ public void setData(SkillInvokedData data) {
      * Data for the skill invoked event.
      */
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SkillInvokedData {
-
-        @JsonProperty("name")
-        private String name;
-
-        @JsonProperty("path")
-        private String path;
-
-        @JsonProperty("content")
-        private String content;
-
-        @JsonProperty("allowedTools")
-        private List allowedTools;
-
-        public String getName() {
-            return name;
-        }
-
-        public void setName(String name) {
-            this.name = name;
-        }
-
-        public String getPath() {
-            return path;
-        }
-
-        public void setPath(String path) {
-            this.path = path;
-        }
-
-        public String getContent() {
-            return content;
-        }
-
-        public void setContent(String content) {
-            this.content = content;
-        }
-
-        public List getAllowedTools() {
-            return allowedTools;
-        }
-
-        public void setAllowedTools(List allowedTools) {
-            this.allowedTools = allowedTools;
-        }
+    public record SkillInvokedData(@JsonProperty("name") String name, @JsonProperty("path") String path,
+            @JsonProperty("content") String content, @JsonProperty("allowedTools") List allowedTools) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java b/src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java
index cde0ce874..31bb9cc70 100644
--- a/src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java
@@ -32,28 +32,7 @@ public void setData(SubagentCompletedData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SubagentCompletedData {
-
-        @JsonProperty("toolCallId")
-        private String toolCallId;
-
-        @JsonProperty("agentName")
-        private String agentName;
-
-        public String getToolCallId() {
-            return toolCallId;
-        }
-
-        public void setToolCallId(String toolCallId) {
-            this.toolCallId = toolCallId;
-        }
-
-        public String getAgentName() {
-            return agentName;
-        }
-
-        public void setAgentName(String agentName) {
-            this.agentName = agentName;
-        }
+    public record SubagentCompletedData(@JsonProperty("toolCallId") String toolCallId,
+            @JsonProperty("agentName") String agentName) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java b/src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java
index 7dfaaae1f..d1b9426bd 100644
--- a/src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java
@@ -32,39 +32,7 @@ public void setData(SubagentFailedData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SubagentFailedData {
-
-        @JsonProperty("toolCallId")
-        private String toolCallId;
-
-        @JsonProperty("agentName")
-        private String agentName;
-
-        @JsonProperty("error")
-        private String error;
-
-        public String getToolCallId() {
-            return toolCallId;
-        }
-
-        public void setToolCallId(String toolCallId) {
-            this.toolCallId = toolCallId;
-        }
-
-        public String getAgentName() {
-            return agentName;
-        }
-
-        public void setAgentName(String agentName) {
-            this.agentName = agentName;
-        }
-
-        public String getError() {
-            return error;
-        }
-
-        public void setError(String error) {
-            this.error = error;
-        }
+    public record SubagentFailedData(@JsonProperty("toolCallId") String toolCallId,
+            @JsonProperty("agentName") String agentName, @JsonProperty("error") String error) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java b/src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java
index c5b9a58d8..6f8737110 100644
--- a/src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java
@@ -32,39 +32,13 @@ public void setData(SubagentSelectedData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SubagentSelectedData {
+    public record SubagentSelectedData(@JsonProperty("agentName") String agentName,
+            @JsonProperty("agentDisplayName") String agentDisplayName, @JsonProperty("tools") String[] tools) {
 
-        @JsonProperty("agentName")
-        private String agentName;
-
-        @JsonProperty("agentDisplayName")
-        private String agentDisplayName;
-
-        @JsonProperty("tools")
-        private String[] tools;
-
-        public String getAgentName() {
-            return agentName;
-        }
-
-        public void setAgentName(String agentName) {
-            this.agentName = agentName;
-        }
-
-        public String getAgentDisplayName() {
-            return agentDisplayName;
-        }
-
-        public void setAgentDisplayName(String agentDisplayName) {
-            this.agentDisplayName = agentDisplayName;
-        }
-
-        public String[] getTools() {
+        /** Returns a defensive copy of the tools array. */
+        @Override
+        public String[] tools() {
             return tools == null ? null : tools.clone();
         }
-
-        public void setTools(String[] tools) {
-            this.tools = tools;
-        }
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java b/src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java
index 95896aa7d..440ffc43e 100644
--- a/src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java
@@ -32,50 +32,8 @@ public void setData(SubagentStartedData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SubagentStartedData {
-
-        @JsonProperty("toolCallId")
-        private String toolCallId;
-
-        @JsonProperty("agentName")
-        private String agentName;
-
-        @JsonProperty("agentDisplayName")
-        private String agentDisplayName;
-
-        @JsonProperty("agentDescription")
-        private String agentDescription;
-
-        public String getToolCallId() {
-            return toolCallId;
-        }
-
-        public void setToolCallId(String toolCallId) {
-            this.toolCallId = toolCallId;
-        }
-
-        public String getAgentName() {
-            return agentName;
-        }
-
-        public void setAgentName(String agentName) {
-            this.agentName = agentName;
-        }
-
-        public String getAgentDisplayName() {
-            return agentDisplayName;
-        }
-
-        public void setAgentDisplayName(String agentDisplayName) {
-            this.agentDisplayName = agentDisplayName;
-        }
-
-        public String getAgentDescription() {
-            return agentDescription;
-        }
-
-        public void setAgentDescription(String agentDescription) {
-            this.agentDescription = agentDescription;
-        }
+    public record SubagentStartedData(@JsonProperty("toolCallId") String toolCallId,
+            @JsonProperty("agentName") String agentName, @JsonProperty("agentDisplayName") String agentDisplayName,
+            @JsonProperty("agentDescription") String agentDescription) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/SystemMessageEvent.java b/src/main/java/com/github/copilot/sdk/events/SystemMessageEvent.java
index 6d2e61e54..e895b3ef1 100644
--- a/src/main/java/com/github/copilot/sdk/events/SystemMessageEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/SystemMessageEvent.java
@@ -34,39 +34,7 @@ public void setData(SystemMessageData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class SystemMessageData {
-
-        @JsonProperty("content")
-        private String content;
-
-        @JsonProperty("type")
-        private String type;
-
-        @JsonProperty("metadata")
-        private Map metadata;
-
-        public String getContent() {
-            return content;
-        }
-
-        public void setContent(String content) {
-            this.content = content;
-        }
-
-        public String getType() {
-            return type;
-        }
-
-        public void setType(String type) {
-            this.type = type;
-        }
-
-        public Map getMetadata() {
-            return metadata;
-        }
-
-        public void setMetadata(Map metadata) {
-            this.metadata = metadata;
-        }
+    public record SystemMessageData(@JsonProperty("content") String content, @JsonProperty("type") String type,
+            @JsonProperty("metadata") Map metadata) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java b/src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java
index 9eb8301ef..d6cc1662f 100644
--- a/src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/ToolExecutionCompleteEvent.java
@@ -35,135 +35,25 @@ public void setData(ToolExecutionCompleteData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class ToolExecutionCompleteData {
-
-        @JsonProperty("toolCallId")
-        private String toolCallId;
-
-        @JsonProperty("success")
-        private boolean success;
-
-        @JsonProperty("isUserRequested")
-        private Boolean isUserRequested;
-
-        @JsonProperty("result")
-        private Result result;
-
-        @JsonProperty("error")
-        private Error error;
-
-        @JsonProperty("toolTelemetry")
-        private Map toolTelemetry;
-
-        @JsonProperty("parentToolCallId")
-        private String parentToolCallId;
-
-        public String getToolCallId() {
-            return toolCallId;
-        }
-
-        public void setToolCallId(String toolCallId) {
-            this.toolCallId = toolCallId;
-        }
-
-        public boolean isSuccess() {
-            return success;
-        }
-
-        public void setSuccess(boolean success) {
-            this.success = success;
-        }
-
-        public Boolean getIsUserRequested() {
-            return isUserRequested;
-        }
-
-        public void setIsUserRequested(Boolean isUserRequested) {
-            this.isUserRequested = isUserRequested;
-        }
-
-        public Result getResult() {
-            return result;
-        }
-
-        public void setResult(Result result) {
-            this.result = result;
-        }
-
-        public Error getError() {
-            return error;
-        }
-
-        public void setError(Error error) {
-            this.error = error;
-        }
-
-        public Map getToolTelemetry() {
+    public record ToolExecutionCompleteData(@JsonProperty("toolCallId") String toolCallId,
+            @JsonProperty("success") boolean success, @JsonProperty("isUserRequested") Boolean isUserRequested,
+            @JsonProperty("result") Result result, @JsonProperty("error") Error error,
+            @JsonProperty("toolTelemetry") Map toolTelemetry,
+            @JsonProperty("parentToolCallId") String parentToolCallId) {
+
+        /** Returns a defensive copy of the tool telemetry map. */
+        @Override
+        public Map toolTelemetry() {
             return toolTelemetry == null ? null : Collections.unmodifiableMap(toolTelemetry);
         }
 
-        public void setToolTelemetry(Map toolTelemetry) {
-            this.toolTelemetry = toolTelemetry;
-        }
-
-        public String getParentToolCallId() {
-            return parentToolCallId;
-        }
-
-        public void setParentToolCallId(String parentToolCallId) {
-            this.parentToolCallId = parentToolCallId;
-        }
-
         @JsonIgnoreProperties(ignoreUnknown = true)
-        public static class Result {
-
-            @JsonProperty("content")
-            private String content;
-
-            @JsonProperty("detailedContent")
-            private String detailedContent;
-
-            public String getContent() {
-                return content;
-            }
-
-            public void setContent(String content) {
-                this.content = content;
-            }
-
-            public String getDetailedContent() {
-                return detailedContent;
-            }
-
-            public void setDetailedContent(String detailedContent) {
-                this.detailedContent = detailedContent;
-            }
+        public record Result(@JsonProperty("content") String content,
+                @JsonProperty("detailedContent") String detailedContent) {
         }
 
         @JsonIgnoreProperties(ignoreUnknown = true)
-        public static class Error {
-
-            @JsonProperty("message")
-            private String message;
-
-            @JsonProperty("code")
-            private String code;
-
-            public String getMessage() {
-                return message;
-            }
-
-            public void setMessage(String message) {
-                this.message = message;
-            }
-
-            public String getCode() {
-                return code;
-            }
-
-            public void setCode(String code) {
-                this.code = code;
-            }
+        public record Error(@JsonProperty("message") String message, @JsonProperty("code") String code) {
         }
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java b/src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java
index ef38d8a7e..0fcf7f75b 100644
--- a/src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java
@@ -32,28 +32,7 @@ public void setData(ToolExecutionPartialResultData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class ToolExecutionPartialResultData {
-
-        @JsonProperty("toolCallId")
-        private String toolCallId;
-
-        @JsonProperty("partialOutput")
-        private String partialOutput;
-
-        public String getToolCallId() {
-            return toolCallId;
-        }
-
-        public void setToolCallId(String toolCallId) {
-            this.toolCallId = toolCallId;
-        }
-
-        public String getPartialOutput() {
-            return partialOutput;
-        }
-
-        public void setPartialOutput(String partialOutput) {
-            this.partialOutput = partialOutput;
-        }
+    public record ToolExecutionPartialResultData(@JsonProperty("toolCallId") String toolCallId,
+            @JsonProperty("partialOutput") String partialOutput) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/ToolExecutionProgressEvent.java b/src/main/java/com/github/copilot/sdk/events/ToolExecutionProgressEvent.java
index 8350c6634..380073123 100644
--- a/src/main/java/com/github/copilot/sdk/events/ToolExecutionProgressEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/ToolExecutionProgressEvent.java
@@ -31,36 +31,12 @@ public ToolExecutionProgressData getData() {
         return data;
     }
 
-    public ToolExecutionProgressEvent setData(ToolExecutionProgressData data) {
+    public void setData(ToolExecutionProgressData data) {
         this.data = data;
-        return this;
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class ToolExecutionProgressData {
-
-        @JsonProperty("toolCallId")
-        private String toolCallId;
-
-        @JsonProperty("progressMessage")
-        private String progressMessage;
-
-        public String getToolCallId() {
-            return toolCallId;
-        }
-
-        public ToolExecutionProgressData setToolCallId(String toolCallId) {
-            this.toolCallId = toolCallId;
-            return this;
-        }
-
-        public String getProgressMessage() {
-            return progressMessage;
-        }
-
-        public ToolExecutionProgressData setProgressMessage(String progressMessage) {
-            this.progressMessage = progressMessage;
-            return this;
-        }
+    public record ToolExecutionProgressData(@JsonProperty("toolCallId") String toolCallId,
+            @JsonProperty("progressMessage") String progressMessage) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/ToolExecutionStartEvent.java b/src/main/java/com/github/copilot/sdk/events/ToolExecutionStartEvent.java
index 33d1c79f5..850151825 100644
--- a/src/main/java/com/github/copilot/sdk/events/ToolExecutionStartEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/ToolExecutionStartEvent.java
@@ -32,72 +32,9 @@ public void setData(ToolExecutionStartData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class ToolExecutionStartData {
-
-        @JsonProperty("toolCallId")
-        private String toolCallId;
-
-        @JsonProperty("toolName")
-        private String toolName;
-
-        @JsonProperty("arguments")
-        private Object arguments;
-
-        @JsonProperty("mcpServerName")
-        private String mcpServerName;
-
-        @JsonProperty("mcpToolName")
-        private String mcpToolName;
-
-        @JsonProperty("parentToolCallId")
-        private String parentToolCallId;
-
-        public String getToolCallId() {
-            return toolCallId;
-        }
-
-        public void setToolCallId(String toolCallId) {
-            this.toolCallId = toolCallId;
-        }
-
-        public String getToolName() {
-            return toolName;
-        }
-
-        public void setToolName(String toolName) {
-            this.toolName = toolName;
-        }
-
-        public Object getArguments() {
-            return arguments;
-        }
-
-        public void setArguments(Object arguments) {
-            this.arguments = arguments;
-        }
-
-        public String getMcpServerName() {
-            return mcpServerName;
-        }
-
-        public void setMcpServerName(String mcpServerName) {
-            this.mcpServerName = mcpServerName;
-        }
-
-        public String getMcpToolName() {
-            return mcpToolName;
-        }
-
-        public void setMcpToolName(String mcpToolName) {
-            this.mcpToolName = mcpToolName;
-        }
-
-        public String getParentToolCallId() {
-            return parentToolCallId;
-        }
-
-        public void setParentToolCallId(String parentToolCallId) {
-            this.parentToolCallId = parentToolCallId;
-        }
+    public record ToolExecutionStartData(@JsonProperty("toolCallId") String toolCallId,
+            @JsonProperty("toolName") String toolName, @JsonProperty("arguments") Object arguments,
+            @JsonProperty("mcpServerName") String mcpServerName, @JsonProperty("mcpToolName") String mcpToolName,
+            @JsonProperty("parentToolCallId") String parentToolCallId) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/ToolUserRequestedEvent.java b/src/main/java/com/github/copilot/sdk/events/ToolUserRequestedEvent.java
index e270f80f9..0a11e4568 100644
--- a/src/main/java/com/github/copilot/sdk/events/ToolUserRequestedEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/ToolUserRequestedEvent.java
@@ -32,39 +32,7 @@ public void setData(ToolUserRequestedData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class ToolUserRequestedData {
-
-        @JsonProperty("toolCallId")
-        private String toolCallId;
-
-        @JsonProperty("toolName")
-        private String toolName;
-
-        @JsonProperty("arguments")
-        private Object arguments;
-
-        public String getToolCallId() {
-            return toolCallId;
-        }
-
-        public void setToolCallId(String toolCallId) {
-            this.toolCallId = toolCallId;
-        }
-
-        public String getToolName() {
-            return toolName;
-        }
-
-        public void setToolName(String toolName) {
-            this.toolName = toolName;
-        }
-
-        public Object getArguments() {
-            return arguments;
-        }
-
-        public void setArguments(Object arguments) {
-            this.arguments = arguments;
-        }
+    public record ToolUserRequestedData(@JsonProperty("toolCallId") String toolCallId,
+            @JsonProperty("toolName") String toolName, @JsonProperty("arguments") Object arguments) {
     }
 }
diff --git a/src/main/java/com/github/copilot/sdk/events/UserMessageEvent.java b/src/main/java/com/github/copilot/sdk/events/UserMessageEvent.java
index 6251604ee..d5e43f891 100644
--- a/src/main/java/com/github/copilot/sdk/events/UserMessageEvent.java
+++ b/src/main/java/com/github/copilot/sdk/events/UserMessageEvent.java
@@ -35,203 +35,26 @@ public void setData(UserMessageData data) {
     }
 
     @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class UserMessageData {
+    public record UserMessageData(@JsonProperty("content") String content,
+            @JsonProperty("transformedContent") String transformedContent,
+            @JsonProperty("attachments") List attachments, @JsonProperty("source") String source) {
 
-        @JsonProperty("content")
-        private String content;
-
-        @JsonProperty("transformedContent")
-        private String transformedContent;
-
-        @JsonProperty("attachments")
-        private List attachments;
-
-        @JsonProperty("source")
-        private String source;
-
-        public String getContent() {
-            return content;
-        }
-
-        public void setContent(String content) {
-            this.content = content;
-        }
-
-        public String getTransformedContent() {
-            return transformedContent;
-        }
-
-        public void setTransformedContent(String transformedContent) {
-            this.transformedContent = transformedContent;
-        }
-
-        public List getAttachments() {
+        /** Returns a defensive copy of the attachments list. */
+        @Override
+        public List attachments() {
             return attachments == null ? null : Collections.unmodifiableList(attachments);
         }
 
-        public void setAttachments(List attachments) {
-            this.attachments = attachments;
-        }
-
-        public String getSource() {
-            return source;
-        }
-
-        public void setSource(String source) {
-            this.source = source;
-        }
-
         @JsonIgnoreProperties(ignoreUnknown = true)
-        public static class Attachment {
-
-            @JsonProperty("type")
-            private String type;
-
-            @JsonProperty("path")
-            private String path;
-
-            @JsonProperty("filePath")
-            private String filePath;
-
-            @JsonProperty("displayName")
-            private String displayName;
-
-            @JsonProperty("text")
-            private String text;
-
-            @JsonProperty("selection")
-            private Selection selection;
-
-            public String getType() {
-                return type;
-            }
-
-            public void setType(String type) {
-                this.type = type;
-            }
-
-            public String getPath() {
-                return path;
-            }
-
-            public void setPath(String path) {
-                this.path = path;
-            }
-
-            /**
-             * Gets the file path (used for selection attachments).
-             *
-             * @return the file path
-             */
-            public String getFilePath() {
-                return filePath;
-            }
-
-            /**
-             * Sets the file path (used for selection attachments).
-             *
-             * @param filePath
-             *            the file path
-             */
-            public void setFilePath(String filePath) {
-                this.filePath = filePath;
-            }
-
-            public String getDisplayName() {
-                return displayName;
-            }
-
-            public void setDisplayName(String displayName) {
-                this.displayName = displayName;
-            }
-
-            /**
-             * Gets the text content (used for selection attachments).
-             *
-             * @return the selected text
-             */
-            public String getText() {
-                return text;
-            }
-
-            /**
-             * Sets the text content (used for selection attachments).
-             *
-             * @param text
-             *            the selected text
-             */
-            public void setText(String text) {
-                this.text = text;
-            }
-
-            /**
-             * Gets the selection range (used for selection attachments).
-             *
-             * @return the selection range
-             */
-            public Selection getSelection() {
-                return selection;
-            }
-
-            /**
-             * Sets the selection range (used for selection attachments).
-             *
-             * @param selection
-             *            the selection range
-             */
-            public void setSelection(Selection selection) {
-                this.selection = selection;
-            }
+        public record Attachment(@JsonProperty("type") String type, @JsonProperty("path") String path,
+                @JsonProperty("filePath") String filePath, @JsonProperty("displayName") String displayName,
+                @JsonProperty("text") String text, @JsonProperty("selection") Selection selection) {
 
             @JsonIgnoreProperties(ignoreUnknown = true)
-            public static class Selection {
-
-                @JsonProperty("start")
-                private Position start;
-
-                @JsonProperty("end")
-                private Position end;
-
-                public Position getStart() {
-                    return start;
-                }
-
-                public void setStart(Position start) {
-                    this.start = start;
-                }
-
-                public Position getEnd() {
-                    return end;
-                }
-
-                public void setEnd(Position end) {
-                    this.end = end;
-                }
+            public record Selection(@JsonProperty("start") Position start, @JsonProperty("end") Position end) {
 
                 @JsonIgnoreProperties(ignoreUnknown = true)
-                public static class Position {
-
-                    @JsonProperty("line")
-                    private int line;
-
-                    @JsonProperty("character")
-                    private int character;
-
-                    public int getLine() {
-                        return line;
-                    }
-
-                    public void setLine(int line) {
-                        this.line = line;
-                    }
-
-                    public int getCharacter() {
-                        return character;
-                    }
-
-                    public void setCharacter(int character) {
-                        this.character = character;
-                    }
+                public record Position(@JsonProperty("line") int line, @JsonProperty("character") int character) {
                 }
             }
         }
diff --git a/src/main/java/com/github/copilot/sdk/events/package-info.java b/src/main/java/com/github/copilot/sdk/events/package-info.java
index 3bbf130f5..6801543aa 100644
--- a/src/main/java/com/github/copilot/sdk/events/package-info.java
+++ b/src/main/java/com/github/copilot/sdk/events/package-info.java
@@ -70,16 +70,16 @@
  * session.on(evt -> {
  * 	if (evt instanceof AssistantMessageDeltaEvent delta) {
  * 		// Streaming response - print incrementally
- * 		System.out.print(delta.getData().getDeltaContent());
+ * 		System.out.print(delta.getData().deltaContent());
  * 	} else if (evt instanceof AssistantMessageEvent msg) {
  * 		// Complete response
- * 		System.out.println("\nFinal: " + msg.getData().getContent());
+ * 		System.out.println("\nFinal: " + msg.getData().content());
  * 	} else if (evt instanceof ToolExecutionStartEvent tool) {
- * 		System.out.println("Executing tool: " + tool.getData().getName());
+ * 		System.out.println("Executing tool: " + tool.getData().toolName());
  * 	} else if (evt instanceof SessionIdleEvent) {
  * 		System.out.println("Session is idle");
  * 	} else if (evt instanceof SessionErrorEvent err) {
- * 		System.err.println("Error: " + err.getData().getMessage());
+ * 		System.err.println("Error: " + err.getData().message());
  * 	}
  * });
  * }
diff --git a/src/main/java/com/github/copilot/sdk/package-info.java b/src/main/java/com/github/copilot/sdk/package-info.java index deb436677..fdf6ff5b6 100644 --- a/src/main/java/com/github/copilot/sdk/package-info.java +++ b/src/main/java/com/github/copilot/sdk/package-info.java @@ -34,7 +34,7 @@ * var session = client.createSession(new SessionConfig().setModel("gpt-4.1")).get(); * * session.on(AssistantMessageEvent.class, msg -> { - * System.out.println(msg.getData().getContent()); + * System.out.println(msg.getData().content()); * }); * * session.send(new MessageOptions().setPrompt("Hello, Copilot!")).get(); diff --git a/src/test/java/com/github/copilot/sdk/CompactionTest.java b/src/test/java/com/github/copilot/sdk/CompactionTest.java index fc95d53f3..33cdc56d1 100644 --- a/src/test/java/com/github/copilot/sdk/CompactionTest.java +++ b/src/test/java/com/github/copilot/sdk/CompactionTest.java @@ -103,17 +103,17 @@ void testShouldTriggerCompactionWithLowThresholdAndEmitEvents() throws Exception .map(e -> (SessionCompactionCompleteEvent) e).reduce((first, second) -> second).orElse(null); assertNotNull(lastCompactionComplete); - assertTrue(lastCompactionComplete.getData().isSuccess(), "Compaction should have succeeded"); + assertTrue(lastCompactionComplete.getData().success(), "Compaction should have succeeded"); // Verify the session still works after compaction AssistantMessageEvent answer = session .sendAndWait(new MessageOptions().setPrompt("What was the story about?")).get(60, TimeUnit.SECONDS); assertNotNull(answer); - assertNotNull(answer.getData().getContent()); + assertNotNull(answer.getData().content()); // Should remember it was about a dragon (context preserved via summary) - assertTrue(answer.getData().getContent().toLowerCase().contains("dragon"), - "Should remember the story was about a dragon: " + answer.getData().getContent()); + assertTrue(answer.getData().content().toLowerCase().contains("dragon"), + "Should remember the story was about a dragon: " + answer.getData().content()); session.close(); } diff --git a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java index 781fb9a9a..233be5236 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -110,16 +110,16 @@ void testShouldHaveStatefulConversation() throws Exception { .get(90, TimeUnit.SECONDS); assertNotNull(response1); - assertTrue(response1.getData().getContent().contains("2"), - "Response should contain 2: " + response1.getData().getContent()); + assertTrue(response1.getData().content().contains("2"), + "Response should contain 2: " + response1.getData().content()); AssistantMessageEvent response2 = session .sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?"), 60000) .get(90, TimeUnit.SECONDS); assertNotNull(response2); - assertTrue(response2.getData().getContent().contains("4"), - "Response should contain 4: " + response2.getData().getContent()); + assertTrue(response2.getData().content().contains("4"), + "Response should contain 4: " + response2.getData().content()); session.close(); } @@ -162,8 +162,8 @@ void testShouldReceiveSessionEvents() throws Exception { .map(e -> (AssistantMessageEvent) e).findFirst().orElse(null); assertNotNull(assistantMsg); - assertTrue(assistantMsg.getData().getContent().contains("300"), - "Response should contain 300: " + assistantMsg.getData().getContent()); + assertTrue(assistantMsg.getData().content().contains("300"), + "Response should contain 300: " + assistantMsg.getData().content()); session.close(); } @@ -207,8 +207,8 @@ void testSendReturnsImmediatelyWhileEventsStreamInBackground() throws Exception assertTrue(events.contains("session.idle")); assertTrue(events.contains("assistant.message")); assertNotNull(lastMessage.get()); - assertTrue(lastMessage.get().getData().getContent().contains("done"), - "Response should contain done: " + lastMessage.get().getData().getContent()); + assertTrue(lastMessage.get().getData().content().contains("done"), + "Response should contain done: " + lastMessage.get().getData().content()); session.close(); } @@ -236,8 +236,8 @@ void testSendAndWaitBlocksUntilSessionIdleAndReturnsFinalAssistantMessage() thro assertNotNull(response); assertEquals("assistant.message", response.getType()); - assertTrue(response.getData().getContent().contains("4"), - "Response should contain 4: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("4"), + "Response should contain 4: " + response.getData().content()); assertTrue(events.contains("session.idle")); assertTrue(events.contains("assistant.message")); @@ -262,8 +262,8 @@ void testShouldResumeSessionUsingTheSameClient() throws Exception { AssistantMessageEvent answer = session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); assertNotNull(answer); - assertTrue(answer.getData().getContent().contains("2"), - "Response should contain 2: " + answer.getData().getContent()); + assertTrue(answer.getData().content().contains("2"), + "Response should contain 2: " + answer.getData().content()); // Resume using the same client CopilotSession session2 = client.resumeSession(sessionId).get(); @@ -273,7 +273,7 @@ void testShouldResumeSessionUsingTheSameClient() throws Exception { // Verify resumed session has the previous messages List messages = session2.getMessages().get(60, TimeUnit.SECONDS); boolean hasAssistantMessage = messages.stream().filter(m -> m instanceof AssistantMessageEvent) - .map(m -> (AssistantMessageEvent) m).anyMatch(m -> m.getData().getContent().contains("2")); + .map(m -> (AssistantMessageEvent) m).anyMatch(m -> m.getData().content().contains("2")); assertTrue(hasAssistantMessage, "Should find previous assistant message containing 2"); session2.close(); @@ -299,8 +299,8 @@ void testShouldResumeSessionUsingNewClient() throws Exception { AssistantMessageEvent answer = session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); assertNotNull(answer); - assertTrue(answer.getData().getContent().contains("2"), - "Response should contain 2: " + answer.getData().getContent()); + assertTrue(answer.getData().content().contains("2"), + "Response should contain 2: " + answer.getData().content()); // Resume using a new client (keeping client1 alive) try (CopilotClient client2 = ctx.createClient()) { @@ -343,10 +343,10 @@ void testShouldCreateSessionWithAppendedSystemMessageConfig() throws Exception { .sendAndWait(new MessageOptions().setPrompt("What is your full name?")).get(60, TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("GitHub"), - "Response should contain GitHub: " + response.getData().getContent()); - assertTrue(response.getData().getContent().contains("Have a nice day!"), - "Response should end with 'Have a nice day!': " + response.getData().getContent()); + assertTrue(response.getData().content().contains("GitHub"), + "Response should contain GitHub: " + response.getData().content()); + assertTrue(response.getData().content().contains("Have a nice day!"), + "Response should end with 'Have a nice day!': " + response.getData().content()); session.close(); } } @@ -374,8 +374,8 @@ void testShouldCreateSessionWithReplacedSystemMessageConfig() throws Exception { .sendAndWait(new MessageOptions().setPrompt("What is your full name?")).get(60, TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("Testy McTestface"), - "Response should contain 'Testy McTestface': " + response.getData().getContent()); + assertTrue(response.getData().content().contains("Testy McTestface"), + "Response should contain 'Testy McTestface': " + response.getData().content()); session.close(); } } @@ -467,8 +467,8 @@ void testShouldAbortSession() throws Exception { AssistantMessageEvent answer = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, TimeUnit.SECONDS); assertNotNull(answer); - assertTrue(answer.getData().getContent().contains("4"), - "Response should contain 4: " + answer.getData().getContent()); + assertTrue(answer.getData().content().contains("4"), + "Response should contain 4: " + answer.getData().content()); session.close(); } @@ -518,8 +518,8 @@ void testShouldCreateSessionWithExcludedTools() throws Exception { TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("2"), - "Response should contain 2: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("2"), + "Response should contain 2: " + response.getData().content()); session.close(); } } @@ -567,8 +567,8 @@ void testShouldCreateSessionWithCustomConfigDir() throws Exception { TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("2"), - "Response should contain 2: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("2"), + "Response should contain 2: " + response.getData().content()); session.close(); } @@ -727,8 +727,8 @@ void testShouldCreateSessionWithCustomTool() throws Exception { .get(60, TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("54321"), - "Response should contain 54321: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("54321"), + "Response should contain 54321: " + response.getData().content()); session.close(); } @@ -755,8 +755,8 @@ void testShouldPassStreamingOptionToSessionCreation() throws Exception { TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("2"), - "Response should contain 2: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("2"), + "Response should contain 2: " + response.getData().content()); session.close(); } diff --git a/src/test/java/com/github/copilot/sdk/ErrorHandlingTest.java b/src/test/java/com/github/copilot/sdk/ErrorHandlingTest.java index 8b9df4039..e111d8854 100644 --- a/src/test/java/com/github/copilot/sdk/ErrorHandlingTest.java +++ b/src/test/java/com/github/copilot/sdk/ErrorHandlingTest.java @@ -149,7 +149,7 @@ void testShouldHandlePermissionHandlerErrorsGracefully_deniesPermission() throws assertNotNull(response, "Should receive a response despite handler error"); // The response should indicate failure/inability - String content = response.getData().getContent().toLowerCase(); + String content = response.getData().content().toLowerCase(); assertTrue( content.contains("fail") || content.contains("cannot") || content.contains("unable") || content.contains("permission") || content.contains("denied"), diff --git a/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java b/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java index 275cc48d6..faa3deafa 100644 --- a/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java +++ b/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java @@ -80,8 +80,8 @@ void testShouldAcceptMcpServerConfigurationOnSessionCreate() throws Exception { TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("4"), - "Response should contain 4: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("4"), + "Response should contain 4: " + response.getData().content()); session.close(); } @@ -116,8 +116,8 @@ void testShouldAcceptMcpServerConfigurationOnSessionResume() throws Exception { .get(60, TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("6"), - "Response should contain 6: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("6"), + "Response should contain 6: " + response.getData().content()); session2.close(); } @@ -173,8 +173,8 @@ void testShouldAcceptCustomAgentConfigurationOnSessionCreate() throws Exception TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("10"), - "Response should contain 10: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("10"), + "Response should contain 10: " + response.getData().content()); session.close(); } @@ -210,8 +210,8 @@ void testShouldAcceptCustomAgentConfigurationOnSessionResume() throws Exception .get(60, TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("12"), - "Response should contain 12: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("12"), + "Response should contain 12: " + response.getData().content()); session2.close(); } @@ -321,8 +321,8 @@ void testShouldAcceptBothMcpServersAndCustomAgents() throws Exception { TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("14"), - "Response should contain 14: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("14"), + "Response should contain 14: " + response.getData().content()); session.close(); } diff --git a/src/test/java/com/github/copilot/sdk/MetadataApiTest.java b/src/test/java/com/github/copilot/sdk/MetadataApiTest.java index 857e5ab35..b3eb8fcb7 100644 --- a/src/test/java/com/github/copilot/sdk/MetadataApiTest.java +++ b/src/test/java/com/github/copilot/sdk/MetadataApiTest.java @@ -102,8 +102,8 @@ void testToolExecutionProgressEventParsing() throws Exception { ToolExecutionProgressEvent progressEvent = (ToolExecutionProgressEvent) event; assertEquals("tool.execution_progress", progressEvent.getType()); assertNotNull(progressEvent.getData()); - assertEquals("call-123", progressEvent.getData().getToolCallId()); - assertEquals("Processing file 1 of 10...", progressEvent.getData().getProgressMessage()); + assertEquals("call-123", progressEvent.getData().toolCallId()); + assertEquals("Processing file 1 of 10...", progressEvent.getData().progressMessage()); } @Test diff --git a/src/test/java/com/github/copilot/sdk/PermissionsTest.java b/src/test/java/com/github/copilot/sdk/PermissionsTest.java index ba68de0e2..d07b7c500 100644 --- a/src/test/java/com/github/copilot/sdk/PermissionsTest.java +++ b/src/test/java/com/github/copilot/sdk/PermissionsTest.java @@ -141,8 +141,8 @@ void testWithoutPermissionHandler(TestInfo testInfo) throws Exception { TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("4"), - "Response should contain 4: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("4"), + "Response should contain 4: " + response.getData().content()); session.close(); } @@ -279,7 +279,7 @@ void testShouldHandlePermissionHandlerErrorsGracefully(TestInfo testInfo) throws // Should handle the error and deny permission assertNotNull(response); - String content = response.getData().getContent().toLowerCase(); + String content = response.getData().content().toLowerCase(); assertTrue(content.contains("fail") || content.contains("cannot") || content.contains("unable") || content.contains("permission"), "Response should indicate failure: " + content); diff --git a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java index 3743bf275..cc36a767f 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java @@ -81,8 +81,8 @@ void testTypedEventHandler() { // Should only have the two assistant messages assertEquals(2, receivedMessages.size()); - assertEquals("First message", receivedMessages.get(0).getData().getContent()); - assertEquals("Second message", receivedMessages.get(1).getData().getContent()); + assertEquals("First message", receivedMessages.get(0).getData().content()); + assertEquals("Second message", receivedMessages.get(1).getData().content()); } @Test @@ -154,7 +154,7 @@ void testMixedHandlers() { session.on(event -> allEvents.add(event.getType())); // Typed handler captures only messages - session.on(AssistantMessageEvent.class, msg -> messageEvents.add(msg.getData().getContent())); + session.on(AssistantMessageEvent.class, msg -> messageEvents.add(msg.getData().content())); dispatchEvent(createSessionStartEvent()); dispatchEvent(createAssistantMessageEvent("Hello")); @@ -171,15 +171,14 @@ void testHandlerReceivesCorrectEventData() { var capturedSessionId = new AtomicReference(); session.on(AssistantMessageEvent.class, msg -> { - capturedContent.set(msg.getData().getContent()); + capturedContent.set(msg.getData().content()); }); session.on(SessionStartEvent.class, start -> { - capturedSessionId.set(start.getData().getSessionId()); + capturedSessionId.set(start.getData().sessionId()); }); - SessionStartEvent startEvent = createSessionStartEvent(); - startEvent.getData().setSessionId("my-session-123"); + SessionStartEvent startEvent = createSessionStartEvent("my-session-123"); dispatchEvent(startEvent); AssistantMessageEvent msgEvent = createAssistantMessageEvent("Test content"); @@ -210,7 +209,7 @@ void testHandlerExceptionDoesNotBreakOtherHandlers() { // Second handler should still receive events session.on(AssistantMessageEvent.class, msg -> { - handler2Events.add(msg.getData().getContent()); + handler2Events.add(msg.getData().content()); }); // This should not throw - exceptions are caught @@ -850,17 +849,19 @@ private void dispatchEvent(AbstractSessionEvent event) { // Factory methods for creating test events private SessionStartEvent createSessionStartEvent() { + return createSessionStartEvent("test-session"); + } + + private SessionStartEvent createSessionStartEvent(String sessionId) { var event = new SessionStartEvent(); - var data = new SessionStartEvent.SessionStartData(); - data.setSessionId("test-session"); + var data = new SessionStartEvent.SessionStartData(sessionId, 0, null, null, null, null); event.setData(data); return event; } private AssistantMessageEvent createAssistantMessageEvent(String content) { var event = new AssistantMessageEvent(); - var data = new AssistantMessageEvent.AssistantMessageData(); - data.setContent(content); + var data = new AssistantMessageEvent.AssistantMessageData(null, content, null, null, null, null, null); event.setData(data); return event; } diff --git a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java index 0926eb811..c29e6a403 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java @@ -59,7 +59,7 @@ void testParseSessionStartEvent() throws Exception { assertEquals("session.start", event.getType()); var startEvent = (SessionStartEvent) event; - assertEquals("sess-123", startEvent.getData().getSessionId()); + assertEquals("sess-123", startEvent.getData().sessionId()); } @Test @@ -98,9 +98,9 @@ void testParseSessionErrorEvent() throws Exception { assertEquals("session.error", event.getType()); var errorEvent = (SessionErrorEvent) event; - assertEquals("RateLimitError", errorEvent.getData().getErrorType()); - assertEquals("Rate limit exceeded", errorEvent.getData().getMessage()); - assertNotNull(errorEvent.getData().getStack()); + assertEquals("RateLimitError", errorEvent.getData().errorType()); + assertEquals("Rate limit exceeded", errorEvent.getData().message()); + assertNotNull(errorEvent.getData().stack()); } @Test @@ -136,8 +136,8 @@ void testParseSessionInfoEvent() throws Exception { assertEquals("session.info", event.getType()); var infoEvent = (SessionInfoEvent) event; - assertEquals("status", infoEvent.getData().getInfoType()); - assertEquals("Processing request", infoEvent.getData().getMessage()); + assertEquals("status", infoEvent.getData().infoType()); + assertEquals("Processing request", infoEvent.getData().message()); } @Test @@ -316,7 +316,7 @@ void testParseAssistantTurnStartEvent() throws Exception { assertEquals("assistant.turn_start", event.getType()); var turnEvent = (AssistantTurnStartEvent) event; - assertEquals("turn-123", turnEvent.getData().getTurnId()); + assertEquals("turn-123", turnEvent.getData().turnId()); } @Test @@ -354,8 +354,8 @@ void testParseAssistantReasoningEvent() throws Exception { assertEquals("assistant.reasoning", event.getType()); var reasoningEvent = (AssistantReasoningEvent) event; - assertEquals("reason-123", reasoningEvent.getData().getReasoningId()); - assertEquals("Analyzing the code structure...", reasoningEvent.getData().getContent()); + assertEquals("reason-123", reasoningEvent.getData().reasoningId()); + assertEquals("Analyzing the code structure...", reasoningEvent.getData().content()); } @Test @@ -394,7 +394,7 @@ void testParseAssistantMessageEvent() throws Exception { assertEquals("assistant.message", event.getType()); var msgEvent = (AssistantMessageEvent) event; - assertEquals("Here is the code you requested.", msgEvent.getData().getContent()); + assertEquals("Here is the code you requested.", msgEvent.getData().content()); } @Test @@ -549,7 +549,7 @@ void testParseToolExecutionCompleteEvent() throws Exception { assertEquals("tool.execution_complete", event.getType()); var completeEvent = (ToolExecutionCompleteEvent) event; - assertTrue(completeEvent.getData().isSuccess()); + assertTrue(completeEvent.getData().success()); } // ========================================================================= @@ -576,8 +576,8 @@ void testParseSubagentStartedEvent() throws Exception { assertEquals("subagent.started", event.getType()); var startedEvent = (SubagentStartedEvent) event; - assertEquals("code-review", startedEvent.getData().getAgentName()); - assertEquals("Code Review Agent", startedEvent.getData().getAgentDisplayName()); + assertEquals("code-review", startedEvent.getData().agentName()); + assertEquals("Code Review Agent", startedEvent.getData().agentDisplayName()); } @Test @@ -656,8 +656,8 @@ void testParseHookStartEvent() throws Exception { assertEquals("hook.start", event.getType()); var hookEvent = (HookStartEvent) event; - assertEquals("hook-123", hookEvent.getData().getHookInvocationId()); - assertEquals("preToolUse", hookEvent.getData().getHookType()); + assertEquals("hook-123", hookEvent.getData().hookInvocationId()); + assertEquals("preToolUse", hookEvent.getData().hookType()); } @Test @@ -743,11 +743,11 @@ void testParseSessionShutdownEvent() throws Exception { assertEquals("session.shutdown", event.getType()); var shutdownEvent = (SessionShutdownEvent) event; - assertEquals(SessionShutdownEvent.ShutdownType.ROUTINE, shutdownEvent.getData().getShutdownType()); - assertEquals(5, shutdownEvent.getData().getTotalPremiumRequests()); - assertEquals("gpt-4", shutdownEvent.getData().getCurrentModel()); - assertNotNull(shutdownEvent.getData().getCodeChanges()); - assertEquals(10, shutdownEvent.getData().getCodeChanges().getLinesAdded()); + assertEquals(SessionShutdownEvent.ShutdownType.ROUTINE, shutdownEvent.getData().shutdownType()); + assertEquals(5, shutdownEvent.getData().totalPremiumRequests()); + assertEquals("gpt-4", shutdownEvent.getData().currentModel()); + assertNotNull(shutdownEvent.getData().codeChanges()); + assertEquals(10, shutdownEvent.getData().codeChanges().linesAdded()); } @Test @@ -770,11 +770,11 @@ void testParseSkillInvokedEvent() throws Exception { assertEquals("skill.invoked", event.getType()); var skillEvent = (SkillInvokedEvent) event; - assertEquals("code-review", skillEvent.getData().getName()); - assertEquals("/path/to/skill", skillEvent.getData().getPath()); - assertEquals("Skill instructions here", skillEvent.getData().getContent()); - assertNotNull(skillEvent.getData().getAllowedTools()); - assertEquals(3, skillEvent.getData().getAllowedTools().size()); + assertEquals("code-review", skillEvent.getData().name()); + assertEquals("/path/to/skill", skillEvent.getData().path()); + assertEquals("Skill instructions here", skillEvent.getData().content()); + assertNotNull(skillEvent.getData().allowedTools()); + assertEquals(3, skillEvent.getData().allowedTools().size()); } // ========================================================================= @@ -973,7 +973,7 @@ void testParseBaseFieldsAllTogether() throws Exception { assertFalse(event.getEphemeral()); assertNotNull(event.getTimestamp()); assertInstanceOf(AssistantMessageEvent.class, event); - assertEquals("Hello", ((AssistantMessageEvent) event).getData().getContent()); + assertEquals("Hello", ((AssistantMessageEvent) event).getData().content()); } @Test @@ -1016,12 +1016,12 @@ void testSessionStartEventAllFields() throws Exception { var event = (SessionStartEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("sess-full", data.getSessionId()); - assertEquals(2.0, data.getVersion()); - assertEquals("copilot-cli", data.getProducer()); - assertEquals("1.2.3", data.getCopilotVersion()); - assertNotNull(data.getStartTime()); - assertEquals("gpt-4-turbo", data.getSelectedModel()); + assertEquals("sess-full", data.sessionId()); + assertEquals(2.0, data.version()); + assertEquals("copilot-cli", data.producer()); + assertEquals("1.2.3", data.copilotVersion()); + assertNotNull(data.startTime()); + assertEquals("gpt-4-turbo", data.selectedModel()); } @Test @@ -1039,8 +1039,8 @@ void testSessionResumeEventAllFields() throws Exception { var event = (SessionResumeEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertNotNull(data.getResumeTime()); - assertEquals(42.0, data.getEventCount()); + assertNotNull(data.resumeTime()); + assertEquals(42.0, data.eventCount()); } @Test @@ -1061,11 +1061,11 @@ void testSessionErrorEventAllFields() throws Exception { var event = (SessionErrorEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("InternalError", data.getErrorType()); - assertEquals("Something went wrong", data.getMessage()); - assertEquals("at line 42", data.getStack()); - assertEquals(500, data.getStatusCode()); - assertEquals("prov-err-1", data.getProviderCallId()); + assertEquals("InternalError", data.errorType()); + assertEquals("Something went wrong", data.message()); + assertEquals("at line 42", data.stack()); + assertEquals(500, data.statusCode()); + assertEquals("prov-err-1", data.providerCallId()); } @Test @@ -1082,8 +1082,8 @@ void testSessionModelChangeEventAllFields() throws Exception { var event = (SessionModelChangeEvent) parseJson(json); assertNotNull(event); - assertEquals("gpt-4", event.getData().getPreviousModel()); - assertEquals("gpt-4o", event.getData().getNewModel()); + assertEquals("gpt-4", event.getData().previousModel()); + assertEquals("gpt-4o", event.getData().newModel()); } @Test @@ -1109,15 +1109,15 @@ void testSessionHandoffEventAllFields() throws Exception { var event = (SessionHandoffEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertNotNull(data.getHandoffTime()); - assertEquals("cli", data.getSourceType()); - assertEquals("additional context", data.getContext()); - assertEquals("handoff summary", data.getSummary()); - assertEquals("remote-sess-1", data.getRemoteSessionId()); - assertNotNull(data.getRepository()); - assertEquals("my-org", data.getRepository().getOwner()); - assertEquals("my-repo", data.getRepository().getName()); - assertEquals("main", data.getRepository().getBranch()); + assertNotNull(data.handoffTime()); + assertEquals("cli", data.sourceType()); + assertEquals("additional context", data.context()); + assertEquals("handoff summary", data.summary()); + assertEquals("remote-sess-1", data.remoteSessionId()); + assertNotNull(data.repository()); + assertEquals("my-org", data.repository().owner()); + assertEquals("my-repo", data.repository().name()); + assertEquals("main", data.repository().branch()); } @Test @@ -1141,14 +1141,14 @@ void testSessionTruncationEventAllFields() throws Exception { var event = (SessionTruncationEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals(128000.0, data.getTokenLimit()); - assertEquals(150000.0, data.getPreTruncationTokensInMessages()); - assertEquals(100.0, data.getPreTruncationMessagesLength()); - assertEquals(120000.0, data.getPostTruncationTokensInMessages()); - assertEquals(80.0, data.getPostTruncationMessagesLength()); - assertEquals(30000.0, data.getTokensRemovedDuringTruncation()); - assertEquals(20.0, data.getMessagesRemovedDuringTruncation()); - assertEquals("system", data.getPerformedBy()); + assertEquals(128000.0, data.tokenLimit()); + assertEquals(150000.0, data.preTruncationTokensInMessages()); + assertEquals(100.0, data.preTruncationMessagesLength()); + assertEquals(120000.0, data.postTruncationTokensInMessages()); + assertEquals(80.0, data.postTruncationMessagesLength()); + assertEquals(30000.0, data.tokensRemovedDuringTruncation()); + assertEquals(20.0, data.messagesRemovedDuringTruncation()); + assertEquals("system", data.performedBy()); } @Test @@ -1167,9 +1167,9 @@ void testSessionUsageInfoEventAllFields() throws Exception { var event = (SessionUsageInfoEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals(128000.0, data.getTokenLimit()); - assertEquals(50000.0, data.getCurrentTokens()); - assertEquals(25.0, data.getMessagesLength()); + assertEquals(128000.0, data.tokenLimit()); + assertEquals(50000.0, data.currentTokens()); + assertEquals(25.0, data.messagesLength()); } @Test @@ -1201,23 +1201,23 @@ void testSessionCompactionCompleteEventAllFields() throws Exception { var event = (SessionCompactionCompleteEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertTrue(data.isSuccess()); - assertNull(data.getError()); - assertEquals(150000.0, data.getPreCompactionTokens()); - assertEquals(60000.0, data.getPostCompactionTokens()); - assertEquals(100.0, data.getPreCompactionMessagesLength()); - assertEquals(50.0, data.getMessagesRemoved()); - assertEquals(90000.0, data.getTokensRemoved()); - assertEquals("Compacted conversation", data.getSummaryContent()); - assertEquals(3.0, data.getCheckpointNumber()); - assertEquals("/checkpoints/3", data.getCheckpointPath()); - assertEquals("req-compact-1", data.getRequestId()); - - var tokens = data.getCompactionTokensUsed(); + assertTrue(data.success()); + assertNull(data.error()); + assertEquals(150000.0, data.preCompactionTokens()); + assertEquals(60000.0, data.postCompactionTokens()); + assertEquals(100.0, data.preCompactionMessagesLength()); + assertEquals(50.0, data.messagesRemoved()); + assertEquals(90000.0, data.tokensRemoved()); + assertEquals("Compacted conversation", data.summaryContent()); + assertEquals(3.0, data.checkpointNumber()); + assertEquals("/checkpoints/3", data.checkpointPath()); + assertEquals("req-compact-1", data.requestId()); + + var tokens = data.compactionTokensUsed(); assertNotNull(tokens); - assertEquals(1000.0, tokens.getInput()); - assertEquals(500.0, tokens.getOutput()); - assertEquals(200.0, tokens.getCachedInput()); + assertEquals(1000.0, tokens.input()); + assertEquals(500.0, tokens.output()); + assertEquals(200.0, tokens.cachedInput()); } @Test @@ -1247,21 +1247,21 @@ void testSessionShutdownEventAllFields() throws Exception { var event = (SessionShutdownEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals(SessionShutdownEvent.ShutdownType.ERROR, data.getShutdownType()); - assertEquals("OOM", data.getErrorReason()); - assertEquals(10.0, data.getTotalPremiumRequests()); - assertEquals(5000.5, data.getTotalApiDurationMs()); - assertEquals(1700000000000.0, data.getSessionStartTime()); - assertEquals("gpt-4-turbo", data.getCurrentModel()); - assertNotNull(data.getModelMetrics()); - - var changes = data.getCodeChanges(); + assertEquals(SessionShutdownEvent.ShutdownType.ERROR, data.shutdownType()); + assertEquals("OOM", data.errorReason()); + assertEquals(10.0, data.totalPremiumRequests()); + assertEquals(5000.5, data.totalApiDurationMs()); + assertEquals(1700000000000.0, data.sessionStartTime()); + assertEquals("gpt-4-turbo", data.currentModel()); + assertNotNull(data.modelMetrics()); + + var changes = data.codeChanges(); assertNotNull(changes); - assertEquals(50.0, changes.getLinesAdded()); - assertEquals(20.0, changes.getLinesRemoved()); - assertNotNull(changes.getFilesModified()); - assertEquals(3, changes.getFilesModified().size()); - assertEquals("a.java", changes.getFilesModified().get(0)); + assertEquals(50.0, changes.linesAdded()); + assertEquals(20.0, changes.linesRemoved()); + assertNotNull(changes.filesModified()); + assertEquals(3, changes.filesModified().size()); + assertEquals("a.java", changes.filesModified().get(0)); } // ========================================================================= @@ -1299,20 +1299,20 @@ void testAssistantMessageEventAllFields() throws Exception { var event = (AssistantMessageEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("msg-rich", data.getMessageId()); - assertEquals("Full response", data.getContent()); - assertEquals("parent-tc", data.getParentToolCallId()); - assertEquals("opaque-data", data.getReasoningOpaque()); - assertEquals("My reasoning", data.getReasoningText()); - assertEquals("enc123", data.getEncryptedContent()); + assertEquals("msg-rich", data.messageId()); + assertEquals("Full response", data.content()); + assertEquals("parent-tc", data.parentToolCallId()); + assertEquals("opaque-data", data.reasoningOpaque()); + assertEquals("My reasoning", data.reasoningText()); + assertEquals("enc123", data.encryptedContent()); - assertNotNull(data.getToolRequests()); - assertEquals(2, data.getToolRequests().size()); - assertEquals("tc-1", data.getToolRequests().get(0).getToolCallId()); - assertEquals("read_file", data.getToolRequests().get(0).getName()); - assertNotNull(data.getToolRequests().get(0).getArguments()); - assertEquals("tc-2", data.getToolRequests().get(1).getToolCallId()); - assertEquals("write_file", data.getToolRequests().get(1).getName()); + assertNotNull(data.toolRequests()); + assertEquals(2, data.toolRequests().size()); + assertEquals("tc-1", data.toolRequests().get(0).toolCallId()); + assertEquals("read_file", data.toolRequests().get(0).name()); + assertNotNull(data.toolRequests().get(0).arguments()); + assertEquals("tc-2", data.toolRequests().get(1).toolCallId()); + assertEquals("write_file", data.toolRequests().get(1).name()); } @Test @@ -1332,10 +1332,10 @@ void testAssistantMessageDeltaEventAllFields() throws Exception { var event = (AssistantMessageDeltaEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("msg-delta-1", data.getMessageId()); - assertEquals("partial text", data.getDeltaContent()); - assertEquals(4096.0, data.getTotalResponseSizeBytes()); - assertEquals("ptc-1", data.getParentToolCallId()); + assertEquals("msg-delta-1", data.messageId()); + assertEquals("partial text", data.deltaContent()); + assertEquals(4096.0, data.totalResponseSizeBytes()); + assertEquals("ptc-1", data.parentToolCallId()); } @Test @@ -1366,19 +1366,19 @@ void testAssistantUsageEventAllFields() throws Exception { var event = (AssistantUsageEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("gpt-4-turbo", data.getModel()); - assertEquals(500.0, data.getInputTokens()); - assertEquals(200.0, data.getOutputTokens()); - assertEquals(50.0, data.getCacheReadTokens()); - assertEquals(150.0, data.getCacheWriteTokens()); - assertEquals(0.05, data.getCost()); - assertEquals(1234.5, data.getDuration()); - assertEquals("user", data.getInitiator()); - assertEquals("api-1", data.getApiCallId()); - assertEquals("prov-1", data.getProviderCallId()); - assertEquals("ptc-usage", data.getParentToolCallId()); - assertNotNull(data.getQuotaSnapshots()); - assertEquals(2, data.getQuotaSnapshots().size()); + assertEquals("gpt-4-turbo", data.model()); + assertEquals(500.0, data.inputTokens()); + assertEquals(200.0, data.outputTokens()); + assertEquals(50.0, data.cacheReadTokens()); + assertEquals(150.0, data.cacheWriteTokens()); + assertEquals(0.05, data.cost()); + assertEquals(1234.5, data.duration()); + assertEquals("user", data.initiator()); + assertEquals("api-1", data.apiCallId()); + assertEquals("prov-1", data.providerCallId()); + assertEquals("ptc-usage", data.parentToolCallId()); + assertNotNull(data.quotaSnapshots()); + assertEquals(2, data.quotaSnapshots().size()); } @Test @@ -1395,8 +1395,8 @@ void testAssistantReasoningDeltaEventAllFields() throws Exception { var event = (AssistantReasoningDeltaEvent) parseJson(json); assertNotNull(event); - assertEquals("r-delta-1", event.getData().getReasoningId()); - assertEquals("thinking about...", event.getData().getDeltaContent()); + assertEquals("r-delta-1", event.getData().reasoningId()); + assertEquals("thinking about...", event.getData().deltaContent()); } @Test @@ -1412,7 +1412,7 @@ void testAssistantIntentEventAllFields() throws Exception { var event = (AssistantIntentEvent) parseJson(json); assertNotNull(event); - assertEquals("refactor_code", event.getData().getIntent()); + assertEquals("refactor_code", event.getData().intent()); } @Test @@ -1428,7 +1428,7 @@ void testAssistantTurnEndEventAllFields() throws Exception { var event = (AssistantTurnEndEvent) parseJson(json); assertNotNull(event); - assertEquals("turn-end-1", event.getData().getTurnId()); + assertEquals("turn-end-1", event.getData().turnId()); } // ========================================================================= @@ -1454,12 +1454,12 @@ void testToolExecutionStartEventAllFields() throws Exception { var event = (ToolExecutionStartEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("tc-start-1", data.getToolCallId()); - assertEquals("mcp_read_file", data.getToolName()); - assertNotNull(data.getArguments()); - assertEquals("filesystem", data.getMcpServerName()); - assertEquals("read_file", data.getMcpToolName()); - assertEquals("ptc-exec", data.getParentToolCallId()); + assertEquals("tc-start-1", data.toolCallId()); + assertEquals("mcp_read_file", data.toolName()); + assertNotNull(data.arguments()); + assertEquals("filesystem", data.mcpServerName()); + assertEquals("read_file", data.mcpToolName()); + assertEquals("ptc-exec", data.parentToolCallId()); } @Test @@ -1487,17 +1487,17 @@ void testToolExecutionCompleteEventWithError() throws Exception { var event = (ToolExecutionCompleteEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("tc-err-1", data.getToolCallId()); - assertFalse(data.isSuccess()); - assertTrue(data.getIsUserRequested()); - assertEquals("ptc-complete", data.getParentToolCallId()); + assertEquals("tc-err-1", data.toolCallId()); + assertFalse(data.success()); + assertTrue(data.isUserRequested()); + assertEquals("ptc-complete", data.parentToolCallId()); - assertNotNull(data.getError()); - assertEquals("File not found", data.getError().getMessage()); - assertEquals("ENOENT", data.getError().getCode()); + assertNotNull(data.error()); + assertEquals("File not found", data.error().message()); + assertEquals("ENOENT", data.error().code()); - assertNotNull(data.getToolTelemetry()); - assertEquals(2, data.getToolTelemetry().size()); + assertNotNull(data.toolTelemetry()); + assertEquals(2, data.toolTelemetry().size()); } @Test @@ -1519,11 +1519,11 @@ void testToolExecutionCompleteEventWithResult() throws Exception { var event = (ToolExecutionCompleteEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertTrue(data.isSuccess()); - assertNotNull(data.getResult()); - assertEquals("file contents", data.getResult().getContent()); - assertEquals("full detailed contents", data.getResult().getDetailedContent()); - assertNull(data.getError()); + assertTrue(data.success()); + assertNotNull(data.result()); + assertEquals("file contents", data.result().content()); + assertEquals("full detailed contents", data.result().detailedContent()); + assertNull(data.error()); } @Test @@ -1540,8 +1540,8 @@ void testToolExecutionPartialResultEventAllFields() throws Exception { var event = (ToolExecutionPartialResultEvent) parseJson(json); assertNotNull(event); - assertEquals("tc-partial-1", event.getData().getToolCallId()); - assertEquals("partial output data", event.getData().getPartialOutput()); + assertEquals("tc-partial-1", event.getData().toolCallId()); + assertEquals("partial output data", event.getData().partialOutput()); } @Test @@ -1558,8 +1558,8 @@ void testToolExecutionProgressEventAllFields() throws Exception { var event = (ToolExecutionProgressEvent) parseJson(json); assertNotNull(event); - assertEquals("tc-prog-1", event.getData().getToolCallId()); - assertEquals("50% done", event.getData().getProgressMessage()); + assertEquals("tc-prog-1", event.getData().toolCallId()); + assertEquals("50% done", event.getData().progressMessage()); } @Test @@ -1577,9 +1577,9 @@ void testToolUserRequestedEventAllFields() throws Exception { var event = (ToolUserRequestedEvent) parseJson(json); assertNotNull(event); - assertEquals("tc-ur-1", event.getData().getToolCallId()); - assertEquals("search_files", event.getData().getToolName()); - assertNotNull(event.getData().getArguments()); + assertEquals("tc-ur-1", event.getData().toolCallId()); + assertEquals("search_files", event.getData().toolName()); + assertNotNull(event.getData().arguments()); } // ========================================================================= @@ -1615,27 +1615,27 @@ void testUserMessageEventAllFieldsWithAttachments() throws Exception { var event = (UserMessageEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("Please review this file", data.getContent()); - assertEquals("Transformed: Please review this file", data.getTransformedContent()); - assertEquals("editor", data.getSource()); + assertEquals("Please review this file", data.content()); + assertEquals("Transformed: Please review this file", data.transformedContent()); + assertEquals("editor", data.source()); - assertNotNull(data.getAttachments()); - assertEquals(1, data.getAttachments().size()); + assertNotNull(data.attachments()); + assertEquals(1, data.attachments().size()); - var att = data.getAttachments().get(0); - assertEquals("file", att.getType()); - assertEquals("/src/Main.java", att.getPath()); - assertEquals("/full/src/Main.java", att.getFilePath()); - assertEquals("Main.java", att.getDisplayName()); - assertEquals("public class Main {}", att.getText()); + var att = data.attachments().get(0); + assertEquals("file", att.type()); + assertEquals("/src/Main.java", att.path()); + assertEquals("/full/src/Main.java", att.filePath()); + assertEquals("Main.java", att.displayName()); + assertEquals("public class Main {}", att.text()); - assertNotNull(att.getSelection()); - assertNotNull(att.getSelection().getStart()); - assertNotNull(att.getSelection().getEnd()); - assertEquals(1, att.getSelection().getStart().getLine()); - assertEquals(0, att.getSelection().getStart().getCharacter()); - assertEquals(5, att.getSelection().getEnd().getLine()); - assertEquals(10, att.getSelection().getEnd().getCharacter()); + assertNotNull(att.selection()); + assertNotNull(att.selection().start()); + assertNotNull(att.selection().end()); + assertEquals(1, att.selection().start().line()); + assertEquals(0, att.selection().start().character()); + assertEquals(5, att.selection().end().line()); + assertEquals(10, att.selection().end().character()); } @Test @@ -1651,8 +1651,8 @@ void testUserMessageEventNoAttachments() throws Exception { var event = (UserMessageEvent) parseJson(json); assertNotNull(event); - assertEquals("Simple message", event.getData().getContent()); - assertNull(event.getData().getAttachments()); + assertEquals("Simple message", event.getData().content()); + assertNull(event.getData().attachments()); } // ========================================================================= @@ -1676,10 +1676,10 @@ void testSubagentStartedEventAllFields() throws Exception { var event = (SubagentStartedEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("tc-sub-1", data.getToolCallId()); - assertEquals("test-agent", data.getAgentName()); - assertEquals("Test Agent", data.getAgentDisplayName()); - assertEquals("A test subagent", data.getAgentDescription()); + assertEquals("tc-sub-1", data.toolCallId()); + assertEquals("test-agent", data.agentName()); + assertEquals("Test Agent", data.agentDisplayName()); + assertEquals("A test subagent", data.agentDescription()); } @Test @@ -1696,8 +1696,8 @@ void testSubagentCompletedEventAllFields() throws Exception { var event = (SubagentCompletedEvent) parseJson(json); assertNotNull(event); - assertEquals("tc-sub-2", event.getData().getToolCallId()); - assertEquals("reviewer", event.getData().getAgentName()); + assertEquals("tc-sub-2", event.getData().toolCallId()); + assertEquals("reviewer", event.getData().agentName()); } @Test @@ -1715,9 +1715,9 @@ void testSubagentFailedEventAllFields() throws Exception { var event = (SubagentFailedEvent) parseJson(json); assertNotNull(event); - assertEquals("tc-sub-3", event.getData().getToolCallId()); - assertEquals("broken-agent", event.getData().getAgentName()); - assertEquals("Connection timeout", event.getData().getError()); + assertEquals("tc-sub-3", event.getData().toolCallId()); + assertEquals("broken-agent", event.getData().agentName()); + assertEquals("Connection timeout", event.getData().error()); } @Test @@ -1736,13 +1736,13 @@ void testSubagentSelectedEventAllFields() throws Exception { var event = (SubagentSelectedEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("best-agent", data.getAgentName()); - assertEquals("Best Agent", data.getAgentDisplayName()); - assertNotNull(data.getTools()); - assertEquals(3, data.getTools().length); - assertEquals("read", data.getTools()[0]); - assertEquals("write", data.getTools()[1]); - assertEquals("search", data.getTools()[2]); + assertEquals("best-agent", data.agentName()); + assertEquals("Best Agent", data.agentDisplayName()); + assertNotNull(data.tools()); + assertEquals(3, data.tools().length); + assertEquals("read", data.tools()[0]); + assertEquals("write", data.tools()[1]); + assertEquals("search", data.tools()[2]); } // ========================================================================= @@ -1764,9 +1764,9 @@ void testHookStartEventAllFields() throws Exception { var event = (HookStartEvent) parseJson(json); assertNotNull(event); - assertEquals("hook-full-1", event.getData().getHookInvocationId()); - assertEquals("postToolUse", event.getData().getHookType()); - assertNotNull(event.getData().getInput()); + assertEquals("hook-full-1", event.getData().hookInvocationId()); + assertEquals("postToolUse", event.getData().hookType()); + assertNotNull(event.getData().input()); } @Test @@ -1790,12 +1790,12 @@ void testHookEndEventWithError() throws Exception { var event = (HookEndEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("hook-err-1", data.getHookInvocationId()); - assertEquals("preToolUse", data.getHookType()); - assertFalse(data.isSuccess()); - assertNotNull(data.getError()); - assertEquals("Hook validation failed", data.getError().getMessage()); - assertEquals("at HookValidator.validate(line 10)", data.getError().getStack()); + assertEquals("hook-err-1", data.hookInvocationId()); + assertEquals("preToolUse", data.hookType()); + assertFalse(data.success()); + assertNotNull(data.error()); + assertEquals("Hook validation failed", data.error().message()); + assertEquals("at HookValidator.validate(line 10)", data.error().stack()); } @Test @@ -1814,8 +1814,8 @@ void testHookEndEventSuccess() throws Exception { var event = (HookEndEvent) parseJson(json); assertNotNull(event); - assertTrue(event.getData().isSuccess()); - assertNull(event.getData().getError()); + assertTrue(event.getData().success()); + assertNull(event.getData().error()); } // ========================================================================= @@ -1835,7 +1835,7 @@ void testAbortEventAllFields() throws Exception { var event = (AbortEvent) parseJson(json); assertNotNull(event); - assertEquals("user_cancelled", event.getData().getReason()); + assertEquals("user_cancelled", event.getData().reason()); } @Test @@ -1857,10 +1857,10 @@ void testSystemMessageEventAllFields() throws Exception { var event = (SystemMessageEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("System notification", data.getContent()); - assertEquals("warning", data.getType()); - assertNotNull(data.getMetadata()); - assertEquals(2, data.getMetadata().size()); + assertEquals("System notification", data.content()); + assertEquals("warning", data.type()); + assertNotNull(data.metadata()); + assertEquals(2, data.metadata().size()); } @Test @@ -1877,8 +1877,8 @@ void testSessionInfoEventAllFields() throws Exception { var event = (SessionInfoEvent) parseJson(json); assertNotNull(event); - assertEquals("model_selection", event.getData().getInfoType()); - assertEquals("Using gpt-4-turbo for this task", event.getData().getMessage()); + assertEquals("model_selection", event.getData().infoType()); + assertEquals("Using gpt-4-turbo for this task", event.getData().message()); } // ========================================================================= @@ -1951,10 +1951,10 @@ void testParseJsonNodeAssistantMessageWithFields() throws Exception { assertNotNull(event); assertEquals(UUID.fromString("550e8400-e29b-41d4-a716-446655440000"), event.getId()); assertTrue(event.getEphemeral()); - assertEquals("msg-jn-1", event.getData().getMessageId()); - assertEquals("Hello from JsonNode", event.getData().getContent()); - assertEquals(1, event.getData().getToolRequests().size()); - assertEquals("tc-jn", event.getData().getToolRequests().get(0).getToolCallId()); + assertEquals("msg-jn-1", event.getData().messageId()); + assertEquals("Hello from JsonNode", event.getData().content()); + assertEquals(1, event.getData().toolRequests().size()); + assertEquals("tc-jn", event.getData().toolRequests().get(0).toolCallId()); } @Test @@ -1975,9 +1975,9 @@ void testParseJsonNodeToolExecutionCompleteWithNestedTypes() throws Exception { var event = (ToolExecutionCompleteEvent) parseJson(json); assertNotNull(event); - assertFalse(event.getData().isSuccess()); - assertEquals("Permission denied", event.getData().getError().getMessage()); - assertEquals("EPERM", event.getData().getError().getCode()); + assertFalse(event.getData().success()); + assertEquals("Permission denied", event.getData().error().message()); + assertEquals("EPERM", event.getData().error().code()); } @Test @@ -2001,9 +2001,9 @@ void testParseJsonNodeSessionShutdownWithCodeChanges() throws Exception { var event = (SessionShutdownEvent) parseJson(json); assertNotNull(event); - assertEquals(SessionShutdownEvent.ShutdownType.ROUTINE, event.getData().getShutdownType()); - assertEquals(100.0, event.getData().getCodeChanges().getLinesAdded()); - assertEquals(1, event.getData().getCodeChanges().getFilesModified().size()); + assertEquals(SessionShutdownEvent.ShutdownType.ROUTINE, event.getData().shutdownType()); + assertEquals(100.0, event.getData().codeChanges().linesAdded()); + assertEquals(1, event.getData().codeChanges().filesModified().size()); } @Test @@ -2030,11 +2030,11 @@ void testParseJsonNodeUserMessageWithAttachment() throws Exception { var event = (UserMessageEvent) parseJson(json); assertNotNull(event); - assertEquals(1, event.getData().getAttachments().size()); - var att = event.getData().getAttachments().get(0); - assertEquals("code", att.getType()); - assertEquals("snippet.py", att.getDisplayName()); - assertEquals(0, att.getSelection().getStart().getLine()); - assertEquals(14, att.getSelection().getEnd().getCharacter()); + assertEquals(1, event.getData().attachments().size()); + var att = event.getData().attachments().get(0); + assertEquals("code", att.type()); + assertEquals("snippet.py", att.displayName()); + assertEquals(0, att.selection().start().line()); + assertEquals(14, att.selection().end().character()); } } diff --git a/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java b/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java index 1dfb4e236..5af1b445b 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java @@ -150,7 +150,7 @@ void testInvokesBuiltInTools_toolExecutionCompleteEvent() throws Exception { assertFalse(toolCompletes.isEmpty(), "Should receive tool.execution_complete event"); // Verify tool execution completed successfully - assertTrue(toolCompletes.stream().anyMatch(e -> e.getData().isSuccess()), + assertTrue(toolCompletes.stream().anyMatch(e -> e.getData().success()), "At least one tool execution should be successful"); } } diff --git a/src/test/java/com/github/copilot/sdk/SkillsTest.java b/src/test/java/com/github/copilot/sdk/SkillsTest.java index aecf3db10..9367145d7 100644 --- a/src/test/java/com/github/copilot/sdk/SkillsTest.java +++ b/src/test/java/com/github/copilot/sdk/SkillsTest.java @@ -118,8 +118,8 @@ void testShouldLoadAndApplySkillFromSkillDirectories() throws Exception { .get(60, TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains(SKILL_MARKER), - "Response should contain skill marker '" + SKILL_MARKER + "': " + response.getData().getContent()); + assertTrue(response.getData().content().contains(SKILL_MARKER), + "Response should contain skill marker '" + SKILL_MARKER + "': " + response.getData().content()); session.close(); } @@ -150,9 +150,8 @@ void testShouldNotApplySkillWhenDisabledViaDisabledSkills() throws Exception { .get(60, TimeUnit.SECONDS); assertNotNull(response); - assertFalse(response.getData().getContent().contains(SKILL_MARKER), - "Response should NOT contain skill marker when skill is disabled: " - + response.getData().getContent()); + assertFalse(response.getData().content().contains(SKILL_MARKER), + "Response should NOT contain skill marker when skill is disabled: " + response.getData().content()); session.close(); } diff --git a/src/test/java/com/github/copilot/sdk/ToolsTest.java b/src/test/java/com/github/copilot/sdk/ToolsTest.java index bee3e3037..bc392d9f0 100644 --- a/src/test/java/com/github/copilot/sdk/ToolsTest.java +++ b/src/test/java/com/github/copilot/sdk/ToolsTest.java @@ -70,8 +70,8 @@ void testInvokesBuiltInTools(TestInfo testInfo) throws Exception { .get(60, TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("ELIZA"), - "Response should contain ELIZA: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("ELIZA"), + "Response should contain ELIZA: " + response.getData().content()); session.close(); } @@ -112,8 +112,8 @@ void testInvokesCustomTool(TestInfo testInfo) throws Exception { .get(60, TimeUnit.SECONDS); assertNotNull(response); - assertTrue(response.getData().getContent().contains("HELLO"), - "Response should contain HELLO: " + response.getData().getContent()); + assertTrue(response.getData().content().contains("HELLO"), + "Response should contain HELLO: " + response.getData().content()); session.close(); } @@ -150,7 +150,7 @@ void testHandlesToolCallingErrors(TestInfo testInfo) throws Exception { assertNotNull(response); // The error message should NOT be exposed to the assistant - String content = response.getData().getContent().toLowerCase(); + String content = response.getData().content().toLowerCase(); assertFalse(content.contains("melbourne"), "Error details should not be exposed in response: " + content); assertTrue(content.contains("unknown") || content.contains("unable") || content.contains("cannot"), "Response should indicate inability to get location: " + content); @@ -211,7 +211,7 @@ void testCanReceiveAndReturnComplexTypes(TestInfo testInfo) throws Exception { .get(60, TimeUnit.SECONDS); assertNotNull(response); - String content = response.getData().getContent(); + String content = response.getData().content(); assertTrue(content.contains("Passos"), "Response should contain Passos: " + content); assertTrue(content.contains("San Lorenzo"), "Response should contain San Lorenzo: " + content); From ef556f75ecde612ec56db6128c1a67a9e3e2fe10 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 10:59:27 -0500 Subject: [PATCH 052/147] refactor: convert SubagentSelectedData to a record for improved data handling --- .../sdk/events/SubagentSelectedEvent.java | 36 +++---------------- .../copilot/sdk/SessionEventParserTest.java | 14 ++++---- 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java b/src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java index c5b9a58d8..6f8737110 100644 --- a/src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SubagentSelectedEvent.java @@ -32,39 +32,13 @@ public void setData(SubagentSelectedData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SubagentSelectedData { + public record SubagentSelectedData(@JsonProperty("agentName") String agentName, + @JsonProperty("agentDisplayName") String agentDisplayName, @JsonProperty("tools") String[] tools) { - @JsonProperty("agentName") - private String agentName; - - @JsonProperty("agentDisplayName") - private String agentDisplayName; - - @JsonProperty("tools") - private String[] tools; - - public String getAgentName() { - return agentName; - } - - public void setAgentName(String agentName) { - this.agentName = agentName; - } - - public String getAgentDisplayName() { - return agentDisplayName; - } - - public void setAgentDisplayName(String agentDisplayName) { - this.agentDisplayName = agentDisplayName; - } - - public String[] getTools() { + /** Returns a defensive copy of the tools array. */ + @Override + public String[] tools() { return tools == null ? null : tools.clone(); } - - public void setTools(String[] tools) { - this.tools = tools; - } } } diff --git a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java index 0926eb811..a3fb4605f 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java @@ -1736,13 +1736,13 @@ void testSubagentSelectedEventAllFields() throws Exception { var event = (SubagentSelectedEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("best-agent", data.getAgentName()); - assertEquals("Best Agent", data.getAgentDisplayName()); - assertNotNull(data.getTools()); - assertEquals(3, data.getTools().length); - assertEquals("read", data.getTools()[0]); - assertEquals("write", data.getTools()[1]); - assertEquals("search", data.getTools()[2]); + assertEquals("best-agent", data.agentName()); + assertEquals("Best Agent", data.agentDisplayName()); + assertNotNull(data.tools()); + assertEquals(3, data.tools().length); + assertEquals("read", data.tools()[0]); + assertEquals("write", data.tools()[1]); + assertEquals("search", data.tools()[2]); } // ========================================================================= From e405117826a32894c55d7303eb8f24f823282c40 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 11:52:58 -0500 Subject: [PATCH 053/147] Update Java badge to include logo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5f743abad..2072ab2bb 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Site](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/deploy-site.yml/badge.svg)](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/deploy-site.yml) [![Coverage](.github/badges/jacoco.svg)](https://copilot-community-sdk.github.io/copilot-sdk-java/snapshot/jacoco/index.html) [![Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://copilot-community-sdk.github.io/copilot-sdk-java/) -[![Java 17+](https://img.shields.io/badge/Java-17%2B-blue)](https://openjdk.org/) +[![Java 17+](https://img.shields.io/badge/Java-17%2B-blue?logo=openjdk&logoColor=white)](https://openjdk.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) #### Latest release @@ -179,4 +179,4 @@ MIT — see [LICENSE](LICENSE) for details. [![Star History Chart](https://api.star-history.com/svg?repos=copilot-community-sdk/copilot-sdk-java&type=Date)](https://www.star-history.com/#copilot-community-sdk/copilot-sdk-java&Date) -⭐ Drop a star if you find this useful! \ No newline at end of file +⭐ Drop a star if you find this useful! From 77e7ee87da4f3780382ab8bc576947673de09788 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 13:40:33 -0500 Subject: [PATCH 054/147] fix: remove unnecessary code block syntax from SKILL.md files --- .claude/skills/commit-as-pull-request/SKILL.md | 2 -- .claude/skills/documentation-coverage/SKILL.md | 7 +++++++ .github/skills/commit-as-pull-request/SKILL.md | 2 -- .github/skills/documentation-coverage/SKILL.md | 2 -- 4 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 .claude/skills/documentation-coverage/SKILL.md diff --git a/.claude/skills/commit-as-pull-request/SKILL.md b/.claude/skills/commit-as-pull-request/SKILL.md index ba65e73cb..23aef5714 100644 --- a/.claude/skills/commit-as-pull-request/SKILL.md +++ b/.claude/skills/commit-as-pull-request/SKILL.md @@ -1,4 +1,3 @@ -```skill --- name: commit-as-pull-request description: Commit current changes as a pull request — creates a branch, pushes, opens a PR, squash-merges, and syncs local main. @@ -6,4 +5,3 @@ license: MIT --- Follow instructions in the [commit-as-pull-request prompt](../../../.github/prompts/commit-as-pull-request.prompt.md) to take the current uncommitted changes, create a feature branch, push it, open a pull request, squash-merge it into main, and sync the local repository. -``` diff --git a/.claude/skills/documentation-coverage/SKILL.md b/.claude/skills/documentation-coverage/SKILL.md new file mode 100644 index 000000000..562f4e29c --- /dev/null +++ b/.claude/skills/documentation-coverage/SKILL.md @@ -0,0 +1,7 @@ +--- +name: documentation-coverage +description: Assess whether the documentation in src/site/markdown/ adequately covers the Java SDK functionality. +license: MIT +--- + +Follow instructions in the [documentation-coverage prompt](../../../.github/prompts/documentation-coverage.prompt.md) to analyze gaps between the SDK's implemented features and its documentation. diff --git a/.github/skills/commit-as-pull-request/SKILL.md b/.github/skills/commit-as-pull-request/SKILL.md index f29f09a41..ebc614d7d 100644 --- a/.github/skills/commit-as-pull-request/SKILL.md +++ b/.github/skills/commit-as-pull-request/SKILL.md @@ -1,4 +1,3 @@ -```skill --- name: commit-as-pull-request description: Commit current changes as a pull request — creates a branch, pushes, opens a PR, squash-merges, and syncs local main. @@ -6,4 +5,3 @@ license: MIT --- Follow instructions in the [commit-as-pull-request prompt](../../prompts/commit-as-pull-request.prompt.md) to take the current uncommitted changes, create a feature branch, push it, open a pull request, squash-merge it into main, and sync the local repository. -``` diff --git a/.github/skills/documentation-coverage/SKILL.md b/.github/skills/documentation-coverage/SKILL.md index 34eb72c02..7a02f2f8c 100644 --- a/.github/skills/documentation-coverage/SKILL.md +++ b/.github/skills/documentation-coverage/SKILL.md @@ -1,4 +1,3 @@ -```skill --- name: documentation-coverage description: Assess whether the documentation in src/site/markdown/ adequately covers the Java SDK functionality. @@ -6,4 +5,3 @@ license: MIT --- Follow instructions in the [documentation-coverage prompt](../../prompts/documentation-coverage.prompt.md) to analyze gaps between the SDK's implemented features and its documentation. -``` From 1e25b01673b734397de4bd8bc93c1d3b6d6e59b1 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 13:44:24 -0500 Subject: [PATCH 055/147] Convert 25 simple inner Data classes to Java records (#118) Replace mutable inner Data classes with Java records across the events package. Records provide immutable, concise DTOs with automatic equals, hashCode, and toString. Jackson @JsonProperty annotations on constructor parameters preserve JSON deserialization compatibility. Updated all accessor calls in production code, tests, and documentation to use record-style accessors (e.g., message() instead of getMessage()). --- .../github/copilot/sdk/CopilotSession.java | 4 +- .../github/copilot/sdk/events/AbortEvent.java | 13 +- .../sdk/events/AssistantIntentEvent.java | 13 +- .../events/AssistantMessageDeltaEvent.java | 49 +---- .../events/AssistantReasoningDeltaEvent.java | 25 +-- .../sdk/events/AssistantReasoningEvent.java | 25 +-- .../sdk/events/AssistantTurnEndEvent.java | 13 +- .../sdk/events/AssistantTurnStartEvent.java | 13 +- .../events/PendingMessagesModifiedEvent.java | 3 +- .../SessionCompactionCompleteEvent.java | 179 ++---------------- .../events/SessionCompactionStartEvent.java | 3 +- .../copilot/sdk/events/SessionErrorEvent.java | 59 +----- .../sdk/events/SessionHandoffEvent.java | 107 +---------- .../copilot/sdk/events/SessionIdleEvent.java | 3 +- .../copilot/sdk/events/SessionInfoEvent.java | 24 +-- .../sdk/events/SessionModelChangeEvent.java | 25 +-- .../sdk/events/SessionResumeEvent.java | 25 +-- .../events/SessionSnapshotRewindEvent.java | 25 +-- .../copilot/sdk/events/SessionStartEvent.java | 70 +------ .../sdk/events/SessionTruncationEvent.java | 97 +--------- .../sdk/events/SessionUsageInfoEvent.java | 37 +--- .../sdk/events/SubagentCompletedEvent.java | 25 +-- .../sdk/events/SubagentFailedEvent.java | 36 +--- .../sdk/events/SubagentStartedEvent.java | 48 +---- .../ToolExecutionPartialResultEvent.java | 25 +-- .../events/ToolExecutionProgressEvent.java | 27 +-- .../copilot/sdk/events/package-info.java | 4 +- src/site/markdown/documentation.md | 6 +- src/site/markdown/getting-started.md | 6 +- .../github/copilot/sdk/CompactionTest.java | 2 +- .../github/copilot/sdk/MetadataApiTest.java | 4 +- .../copilot/sdk/SessionEventHandlingTest.java | 7 +- .../copilot/sdk/SessionEventParserTest.java | 172 ++++++++--------- 33 files changed, 170 insertions(+), 1004 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index ab2f49a29..dd158bbac 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -338,7 +338,7 @@ public CompletableFuture sendAndWait(MessageOptions optio } else if (evt instanceof SessionIdleEvent) { future.complete(lastAssistantMessage.get()); } else if (evt instanceof SessionErrorEvent errorEvent) { - String message = errorEvent.getData() != null ? errorEvent.getData().getMessage() : "session error"; + String message = errorEvent.getData() != null ? errorEvent.getData().message() : "session error"; future.completeExceptionally(new RuntimeException("Session error: " + message)); } }; @@ -463,7 +463,7 @@ public Closeable on(Consumer handler) { * * // Handle streaming deltas * session.on(AssistantMessageDeltaEvent.class, delta -> { - * System.out.print(delta.getData().getDeltaContent()); + * System.out.print(delta.getData().deltaContent()); * }); * }
* diff --git a/src/main/java/com/github/copilot/sdk/events/AbortEvent.java b/src/main/java/com/github/copilot/sdk/events/AbortEvent.java index b1e5f0e88..d63185101 100644 --- a/src/main/java/com/github/copilot/sdk/events/AbortEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AbortEvent.java @@ -32,17 +32,6 @@ public void setData(AbortData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class AbortData { - - @JsonProperty("reason") - private String reason; - - public String getReason() { - return reason; - } - - public void setReason(String reason) { - this.reason = reason; - } + public record AbortData(@JsonProperty("reason") String reason) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java index 9b14beb65..e8c693139 100644 --- a/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AssistantIntentEvent.java @@ -32,17 +32,6 @@ public void setData(AssistantIntentData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class AssistantIntentData { - - @JsonProperty("intent") - private String intent; - - public String getIntent() { - return intent; - } - - public void setIntent(String intent) { - this.intent = intent; - } + public record AssistantIntentData(@JsonProperty("intent") String intent) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java index c7714b28b..901b05236 100644 --- a/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java @@ -32,50 +32,9 @@ public void setData(AssistantMessageDeltaData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class AssistantMessageDeltaData { - - @JsonProperty("messageId") - private String messageId; - - @JsonProperty("deltaContent") - private String deltaContent; - - @JsonProperty("totalResponseSizeBytes") - private Double totalResponseSizeBytes; - - @JsonProperty("parentToolCallId") - private String parentToolCallId; - - public String getMessageId() { - return messageId; - } - - public void setMessageId(String messageId) { - this.messageId = messageId; - } - - public String getDeltaContent() { - return deltaContent; - } - - public void setDeltaContent(String deltaContent) { - this.deltaContent = deltaContent; - } - - public Double getTotalResponseSizeBytes() { - return totalResponseSizeBytes; - } - - public void setTotalResponseSizeBytes(Double totalResponseSizeBytes) { - this.totalResponseSizeBytes = totalResponseSizeBytes; - } - - public String getParentToolCallId() { - return parentToolCallId; - } - - public void setParentToolCallId(String parentToolCallId) { - this.parentToolCallId = parentToolCallId; - } + public record AssistantMessageDeltaData(@JsonProperty("messageId") String messageId, + @JsonProperty("deltaContent") String deltaContent, + @JsonProperty("totalResponseSizeBytes") Double totalResponseSizeBytes, + @JsonProperty("parentToolCallId") String parentToolCallId) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java index 2edb447cf..d661f5c32 100644 --- a/src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AssistantReasoningDeltaEvent.java @@ -32,28 +32,7 @@ public void setData(AssistantReasoningDeltaData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class AssistantReasoningDeltaData { - - @JsonProperty("reasoningId") - private String reasoningId; - - @JsonProperty("deltaContent") - private String deltaContent; - - public String getReasoningId() { - return reasoningId; - } - - public void setReasoningId(String reasoningId) { - this.reasoningId = reasoningId; - } - - public String getDeltaContent() { - return deltaContent; - } - - public void setDeltaContent(String deltaContent) { - this.deltaContent = deltaContent; - } + public record AssistantReasoningDeltaData(@JsonProperty("reasoningId") String reasoningId, + @JsonProperty("deltaContent") String deltaContent) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java index cab00a8df..7019ed71e 100644 --- a/src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AssistantReasoningEvent.java @@ -32,28 +32,7 @@ public void setData(AssistantReasoningData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class AssistantReasoningData { - - @JsonProperty("reasoningId") - private String reasoningId; - - @JsonProperty("content") - private String content; - - public String getReasoningId() { - return reasoningId; - } - - public void setReasoningId(String reasoningId) { - this.reasoningId = reasoningId; - } - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } + public record AssistantReasoningData(@JsonProperty("reasoningId") String reasoningId, + @JsonProperty("content") String content) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java index 634f2aaeb..d847ddc6a 100644 --- a/src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AssistantTurnEndEvent.java @@ -32,17 +32,6 @@ public void setData(AssistantTurnEndData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class AssistantTurnEndData { - - @JsonProperty("turnId") - private String turnId; - - public String getTurnId() { - return turnId; - } - - public void setTurnId(String turnId) { - this.turnId = turnId; - } + public record AssistantTurnEndData(@JsonProperty("turnId") String turnId) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java index 1f0f6aec2..ab5e18653 100644 --- a/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AssistantTurnStartEvent.java @@ -32,17 +32,6 @@ public void setData(AssistantTurnStartData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class AssistantTurnStartData { - - @JsonProperty("turnId") - private String turnId; - - public String getTurnId() { - return turnId; - } - - public void setTurnId(String turnId) { - this.turnId = turnId; - } + public record AssistantTurnStartData(@JsonProperty("turnId") String turnId) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java b/src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java index 49b9b62d8..6eb863c78 100644 --- a/src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/PendingMessagesModifiedEvent.java @@ -32,7 +32,6 @@ public void setData(PendingMessagesModifiedData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class PendingMessagesModifiedData { - // Empty data + public record PendingMessagesModifiedData() { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java index b47cc3eb5..57036e9d8 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionCompactionCompleteEvent.java @@ -32,178 +32,23 @@ public void setData(SessionCompactionCompleteData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SessionCompactionCompleteData { - - @JsonProperty("success") - private boolean success; - - @JsonProperty("error") - private String error; - - @JsonProperty("preCompactionTokens") - private Double preCompactionTokens; - - @JsonProperty("postCompactionTokens") - private Double postCompactionTokens; - - @JsonProperty("preCompactionMessagesLength") - private Double preCompactionMessagesLength; - - @JsonProperty("messagesRemoved") - private Double messagesRemoved; - - @JsonProperty("tokensRemoved") - private Double tokensRemoved; - - @JsonProperty("summaryContent") - private String summaryContent; - - @JsonProperty("checkpointNumber") - private Double checkpointNumber; - - @JsonProperty("checkpointPath") - private String checkpointPath; - - @JsonProperty("compactionTokensUsed") - private CompactionTokensUsed compactionTokensUsed; - - @JsonProperty("requestId") - private String requestId; - - public boolean isSuccess() { - return success; - } - - public void setSuccess(boolean success) { - this.success = success; - } - - public String getError() { - return error; - } - - public void setError(String error) { - this.error = error; - } - - public Double getPreCompactionTokens() { - return preCompactionTokens; - } - - public void setPreCompactionTokens(Double preCompactionTokens) { - this.preCompactionTokens = preCompactionTokens; - } - - public Double getPostCompactionTokens() { - return postCompactionTokens; - } - - public void setPostCompactionTokens(Double postCompactionTokens) { - this.postCompactionTokens = postCompactionTokens; - } - - public Double getPreCompactionMessagesLength() { - return preCompactionMessagesLength; - } - - public void setPreCompactionMessagesLength(Double preCompactionMessagesLength) { - this.preCompactionMessagesLength = preCompactionMessagesLength; - } - - public Double getMessagesRemoved() { - return messagesRemoved; - } - - public void setMessagesRemoved(Double messagesRemoved) { - this.messagesRemoved = messagesRemoved; - } - - public Double getTokensRemoved() { - return tokensRemoved; - } - - public void setTokensRemoved(Double tokensRemoved) { - this.tokensRemoved = tokensRemoved; - } - - public String getSummaryContent() { - return summaryContent; - } - - public void setSummaryContent(String summaryContent) { - this.summaryContent = summaryContent; - } - - public Double getCheckpointNumber() { - return checkpointNumber; - } - - public void setCheckpointNumber(Double checkpointNumber) { - this.checkpointNumber = checkpointNumber; - } - - public String getCheckpointPath() { - return checkpointPath; - } - - public void setCheckpointPath(String checkpointPath) { - this.checkpointPath = checkpointPath; - } - - public CompactionTokensUsed getCompactionTokensUsed() { - return compactionTokensUsed; - } - - public void setCompactionTokensUsed(CompactionTokensUsed compactionTokensUsed) { - this.compactionTokensUsed = compactionTokensUsed; - } - - public String getRequestId() { - return requestId; - } - - public void setRequestId(String requestId) { - this.requestId = requestId; - } + public record SessionCompactionCompleteData(@JsonProperty("success") boolean success, + @JsonProperty("error") String error, @JsonProperty("preCompactionTokens") Double preCompactionTokens, + @JsonProperty("postCompactionTokens") Double postCompactionTokens, + @JsonProperty("preCompactionMessagesLength") Double preCompactionMessagesLength, + @JsonProperty("messagesRemoved") Double messagesRemoved, + @JsonProperty("tokensRemoved") Double tokensRemoved, @JsonProperty("summaryContent") String summaryContent, + @JsonProperty("checkpointNumber") Double checkpointNumber, + @JsonProperty("checkpointPath") String checkpointPath, + @JsonProperty("compactionTokensUsed") CompactionTokensUsed compactionTokensUsed, + @JsonProperty("requestId") String requestId) { } /** * Token usage information for the compaction operation. */ @JsonIgnoreProperties(ignoreUnknown = true) - public static class CompactionTokensUsed { - - @JsonProperty("input") - private double input; - - @JsonProperty("output") - private double output; - - @JsonProperty("cachedInput") - private double cachedInput; - - public double getInput() { - return input; - } - - public void setInput(double input) { - this.input = input; - } - - public double getOutput() { - return output; - } - - public void setOutput(double output) { - this.output = output; - } - - public double getCachedInput() { - return cachedInput; - } - - public void setCachedInput(double cachedInput) { - this.cachedInput = cachedInput; - } + public record CompactionTokensUsed(@JsonProperty("input") double input, @JsonProperty("output") double output, + @JsonProperty("cachedInput") double cachedInput) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java index 15eec1021..ad29b37f8 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionCompactionStartEvent.java @@ -32,7 +32,6 @@ public void setData(SessionCompactionStartData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SessionCompactionStartData { - // Empty data + public record SessionCompactionStartData() { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java index 54adbe789..ffa9e6d9e 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionErrorEvent.java @@ -32,61 +32,8 @@ public void setData(SessionErrorData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SessionErrorData { - - @JsonProperty("errorType") - private String errorType; - - @JsonProperty("message") - private String message; - - @JsonProperty("stack") - private String stack; - - @JsonProperty("statusCode") - private Double statusCode; - - @JsonProperty("providerCallId") - private String providerCallId; - - public String getErrorType() { - return errorType; - } - - public void setErrorType(String errorType) { - this.errorType = errorType; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getStack() { - return stack; - } - - public void setStack(String stack) { - this.stack = stack; - } - - public Double getStatusCode() { - return statusCode; - } - - public void setStatusCode(Double statusCode) { - this.statusCode = statusCode; - } - - public String getProviderCallId() { - return providerCallId; - } - - public void setProviderCallId(String providerCallId) { - this.providerCallId = providerCallId; - } + public record SessionErrorData(@JsonProperty("errorType") String errorType, @JsonProperty("message") String message, + @JsonProperty("stack") String stack, @JsonProperty("statusCode") Double statusCode, + @JsonProperty("providerCallId") String providerCallId) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java index 74c084158..08e8f8c42 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionHandoffEvent.java @@ -34,109 +34,14 @@ public void setData(SessionHandoffData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SessionHandoffData { - - @JsonProperty("handoffTime") - private OffsetDateTime handoffTime; - - @JsonProperty("sourceType") - private String sourceType; - - @JsonProperty("repository") - private Repository repository; - - @JsonProperty("context") - private String context; - - @JsonProperty("summary") - private String summary; - - @JsonProperty("remoteSessionId") - private String remoteSessionId; - - public OffsetDateTime getHandoffTime() { - return handoffTime; - } - - public void setHandoffTime(OffsetDateTime handoffTime) { - this.handoffTime = handoffTime; - } - - public String getSourceType() { - return sourceType; - } - - public void setSourceType(String sourceType) { - this.sourceType = sourceType; - } - - public Repository getRepository() { - return repository; - } - - public void setRepository(Repository repository) { - this.repository = repository; - } - - public String getContext() { - return context; - } - - public void setContext(String context) { - this.context = context; - } - - public String getSummary() { - return summary; - } - - public void setSummary(String summary) { - this.summary = summary; - } - - public String getRemoteSessionId() { - return remoteSessionId; - } - - public void setRemoteSessionId(String remoteSessionId) { - this.remoteSessionId = remoteSessionId; - } + public record SessionHandoffData(@JsonProperty("handoffTime") OffsetDateTime handoffTime, + @JsonProperty("sourceType") String sourceType, @JsonProperty("repository") Repository repository, + @JsonProperty("context") String context, @JsonProperty("summary") String summary, + @JsonProperty("remoteSessionId") String remoteSessionId) { @JsonIgnoreProperties(ignoreUnknown = true) - public static class Repository { - - @JsonProperty("owner") - private String owner; - - @JsonProperty("name") - private String name; - - @JsonProperty("branch") - private String branch; - - public String getOwner() { - return owner; - } - - public void setOwner(String owner) { - this.owner = owner; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getBranch() { - return branch; - } - - public void setBranch(String branch) { - this.branch = branch; - } + public record Repository(@JsonProperty("owner") String owner, @JsonProperty("name") String name, + @JsonProperty("branch") String branch) { } } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java index 904c55df0..bb77b6ed0 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionIdleEvent.java @@ -32,7 +32,6 @@ public void setData(SessionIdleData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SessionIdleData { - // Empty data + public record SessionIdleData() { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java index 664be4bf4..dd1fe7ccb 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionInfoEvent.java @@ -32,28 +32,6 @@ public void setData(SessionInfoData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SessionInfoData { - - @JsonProperty("infoType") - private String infoType; - - @JsonProperty("message") - private String message; - - public String getInfoType() { - return infoType; - } - - public void setInfoType(String infoType) { - this.infoType = infoType; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } + public record SessionInfoData(@JsonProperty("infoType") String infoType, @JsonProperty("message") String message) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java index 7f0e21c6d..57d0b5499 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionModelChangeEvent.java @@ -32,28 +32,7 @@ public void setData(SessionModelChangeData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SessionModelChangeData { - - @JsonProperty("previousModel") - private String previousModel; - - @JsonProperty("newModel") - private String newModel; - - public String getPreviousModel() { - return previousModel; - } - - public void setPreviousModel(String previousModel) { - this.previousModel = previousModel; - } - - public String getNewModel() { - return newModel; - } - - public void setNewModel(String newModel) { - this.newModel = newModel; - } + public record SessionModelChangeData(@JsonProperty("previousModel") String previousModel, + @JsonProperty("newModel") String newModel) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java index 4d31dd9a8..bf305bc30 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionResumeEvent.java @@ -34,28 +34,7 @@ public void setData(SessionResumeData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SessionResumeData { - - @JsonProperty("resumeTime") - private OffsetDateTime resumeTime; - - @JsonProperty("eventCount") - private double eventCount; - - public OffsetDateTime getResumeTime() { - return resumeTime; - } - - public void setResumeTime(OffsetDateTime resumeTime) { - this.resumeTime = resumeTime; - } - - public double getEventCount() { - return eventCount; - } - - public void setEventCount(double eventCount) { - this.eventCount = eventCount; - } + public record SessionResumeData(@JsonProperty("resumeTime") OffsetDateTime resumeTime, + @JsonProperty("eventCount") double eventCount) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionSnapshotRewindEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionSnapshotRewindEvent.java index 3345fa34f..ae7b0f3c5 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionSnapshotRewindEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionSnapshotRewindEvent.java @@ -34,28 +34,7 @@ public void setData(SessionSnapshotRewindData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SessionSnapshotRewindData { - - @JsonProperty("upToEventId") - private String upToEventId; - - @JsonProperty("eventsRemoved") - private int eventsRemoved; - - public String getUpToEventId() { - return upToEventId; - } - - public void setUpToEventId(String upToEventId) { - this.upToEventId = upToEventId; - } - - public int getEventsRemoved() { - return eventsRemoved; - } - - public void setEventsRemoved(int eventsRemoved) { - this.eventsRemoved = eventsRemoved; - } + public record SessionSnapshotRewindData(@JsonProperty("upToEventId") String upToEventId, + @JsonProperty("eventsRemoved") int eventsRemoved) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java index 6cb3b18d8..317b4a470 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionStartEvent.java @@ -34,72 +34,8 @@ public void setData(SessionStartData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SessionStartData { - - @JsonProperty("sessionId") - private String sessionId; - - @JsonProperty("version") - private double version; - - @JsonProperty("producer") - private String producer; - - @JsonProperty("copilotVersion") - private String copilotVersion; - - @JsonProperty("startTime") - private OffsetDateTime startTime; - - @JsonProperty("selectedModel") - private String selectedModel; - - public String getSessionId() { - return sessionId; - } - - public void setSessionId(String sessionId) { - this.sessionId = sessionId; - } - - public double getVersion() { - return version; - } - - public void setVersion(double version) { - this.version = version; - } - - public String getProducer() { - return producer; - } - - public void setProducer(String producer) { - this.producer = producer; - } - - public String getCopilotVersion() { - return copilotVersion; - } - - public void setCopilotVersion(String copilotVersion) { - this.copilotVersion = copilotVersion; - } - - public OffsetDateTime getStartTime() { - return startTime; - } - - public void setStartTime(OffsetDateTime startTime) { - this.startTime = startTime; - } - - public String getSelectedModel() { - return selectedModel; - } - - public void setSelectedModel(String selectedModel) { - this.selectedModel = selectedModel; - } + public record SessionStartData(@JsonProperty("sessionId") String sessionId, @JsonProperty("version") double version, + @JsonProperty("producer") String producer, @JsonProperty("copilotVersion") String copilotVersion, + @JsonProperty("startTime") OffsetDateTime startTime, @JsonProperty("selectedModel") String selectedModel) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java index f5d74626b..04971cb97 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionTruncationEvent.java @@ -32,94 +32,13 @@ public void setData(SessionTruncationData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SessionTruncationData { - - @JsonProperty("tokenLimit") - private double tokenLimit; - - @JsonProperty("preTruncationTokensInMessages") - private double preTruncationTokensInMessages; - - @JsonProperty("preTruncationMessagesLength") - private double preTruncationMessagesLength; - - @JsonProperty("postTruncationTokensInMessages") - private double postTruncationTokensInMessages; - - @JsonProperty("postTruncationMessagesLength") - private double postTruncationMessagesLength; - - @JsonProperty("tokensRemovedDuringTruncation") - private double tokensRemovedDuringTruncation; - - @JsonProperty("messagesRemovedDuringTruncation") - private double messagesRemovedDuringTruncation; - - @JsonProperty("performedBy") - private String performedBy; - - public double getTokenLimit() { - return tokenLimit; - } - - public void setTokenLimit(double tokenLimit) { - this.tokenLimit = tokenLimit; - } - - public double getPreTruncationTokensInMessages() { - return preTruncationTokensInMessages; - } - - public void setPreTruncationTokensInMessages(double preTruncationTokensInMessages) { - this.preTruncationTokensInMessages = preTruncationTokensInMessages; - } - - public double getPreTruncationMessagesLength() { - return preTruncationMessagesLength; - } - - public void setPreTruncationMessagesLength(double preTruncationMessagesLength) { - this.preTruncationMessagesLength = preTruncationMessagesLength; - } - - public double getPostTruncationTokensInMessages() { - return postTruncationTokensInMessages; - } - - public void setPostTruncationTokensInMessages(double postTruncationTokensInMessages) { - this.postTruncationTokensInMessages = postTruncationTokensInMessages; - } - - public double getPostTruncationMessagesLength() { - return postTruncationMessagesLength; - } - - public void setPostTruncationMessagesLength(double postTruncationMessagesLength) { - this.postTruncationMessagesLength = postTruncationMessagesLength; - } - - public double getTokensRemovedDuringTruncation() { - return tokensRemovedDuringTruncation; - } - - public void setTokensRemovedDuringTruncation(double tokensRemovedDuringTruncation) { - this.tokensRemovedDuringTruncation = tokensRemovedDuringTruncation; - } - - public double getMessagesRemovedDuringTruncation() { - return messagesRemovedDuringTruncation; - } - - public void setMessagesRemovedDuringTruncation(double messagesRemovedDuringTruncation) { - this.messagesRemovedDuringTruncation = messagesRemovedDuringTruncation; - } - - public String getPerformedBy() { - return performedBy; - } - - public void setPerformedBy(String performedBy) { - this.performedBy = performedBy; - } + public record SessionTruncationData(@JsonProperty("tokenLimit") double tokenLimit, + @JsonProperty("preTruncationTokensInMessages") double preTruncationTokensInMessages, + @JsonProperty("preTruncationMessagesLength") double preTruncationMessagesLength, + @JsonProperty("postTruncationTokensInMessages") double postTruncationTokensInMessages, + @JsonProperty("postTruncationMessagesLength") double postTruncationMessagesLength, + @JsonProperty("tokensRemovedDuringTruncation") double tokensRemovedDuringTruncation, + @JsonProperty("messagesRemovedDuringTruncation") double messagesRemovedDuringTruncation, + @JsonProperty("performedBy") String performedBy) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java b/src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java index 808c855cb..1bf10d7de 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionUsageInfoEvent.java @@ -32,39 +32,8 @@ public void setData(SessionUsageInfoData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SessionUsageInfoData { - - @JsonProperty("tokenLimit") - private double tokenLimit; - - @JsonProperty("currentTokens") - private double currentTokens; - - @JsonProperty("messagesLength") - private double messagesLength; - - public double getTokenLimit() { - return tokenLimit; - } - - public void setTokenLimit(double tokenLimit) { - this.tokenLimit = tokenLimit; - } - - public double getCurrentTokens() { - return currentTokens; - } - - public void setCurrentTokens(double currentTokens) { - this.currentTokens = currentTokens; - } - - public double getMessagesLength() { - return messagesLength; - } - - public void setMessagesLength(double messagesLength) { - this.messagesLength = messagesLength; - } + public record SessionUsageInfoData(@JsonProperty("tokenLimit") double tokenLimit, + @JsonProperty("currentTokens") double currentTokens, + @JsonProperty("messagesLength") double messagesLength) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java b/src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java index cde0ce874..31bb9cc70 100644 --- a/src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SubagentCompletedEvent.java @@ -32,28 +32,7 @@ public void setData(SubagentCompletedData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SubagentCompletedData { - - @JsonProperty("toolCallId") - private String toolCallId; - - @JsonProperty("agentName") - private String agentName; - - public String getToolCallId() { - return toolCallId; - } - - public void setToolCallId(String toolCallId) { - this.toolCallId = toolCallId; - } - - public String getAgentName() { - return agentName; - } - - public void setAgentName(String agentName) { - this.agentName = agentName; - } + public record SubagentCompletedData(@JsonProperty("toolCallId") String toolCallId, + @JsonProperty("agentName") String agentName) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java b/src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java index 7dfaaae1f..d1b9426bd 100644 --- a/src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SubagentFailedEvent.java @@ -32,39 +32,7 @@ public void setData(SubagentFailedData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SubagentFailedData { - - @JsonProperty("toolCallId") - private String toolCallId; - - @JsonProperty("agentName") - private String agentName; - - @JsonProperty("error") - private String error; - - public String getToolCallId() { - return toolCallId; - } - - public void setToolCallId(String toolCallId) { - this.toolCallId = toolCallId; - } - - public String getAgentName() { - return agentName; - } - - public void setAgentName(String agentName) { - this.agentName = agentName; - } - - public String getError() { - return error; - } - - public void setError(String error) { - this.error = error; - } + public record SubagentFailedData(@JsonProperty("toolCallId") String toolCallId, + @JsonProperty("agentName") String agentName, @JsonProperty("error") String error) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java b/src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java index 95896aa7d..440ffc43e 100644 --- a/src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/SubagentStartedEvent.java @@ -32,50 +32,8 @@ public void setData(SubagentStartedData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class SubagentStartedData { - - @JsonProperty("toolCallId") - private String toolCallId; - - @JsonProperty("agentName") - private String agentName; - - @JsonProperty("agentDisplayName") - private String agentDisplayName; - - @JsonProperty("agentDescription") - private String agentDescription; - - public String getToolCallId() { - return toolCallId; - } - - public void setToolCallId(String toolCallId) { - this.toolCallId = toolCallId; - } - - public String getAgentName() { - return agentName; - } - - public void setAgentName(String agentName) { - this.agentName = agentName; - } - - public String getAgentDisplayName() { - return agentDisplayName; - } - - public void setAgentDisplayName(String agentDisplayName) { - this.agentDisplayName = agentDisplayName; - } - - public String getAgentDescription() { - return agentDescription; - } - - public void setAgentDescription(String agentDescription) { - this.agentDescription = agentDescription; - } + public record SubagentStartedData(@JsonProperty("toolCallId") String toolCallId, + @JsonProperty("agentName") String agentName, @JsonProperty("agentDisplayName") String agentDisplayName, + @JsonProperty("agentDescription") String agentDescription) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java b/src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java index ef38d8a7e..0fcf7f75b 100644 --- a/src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/ToolExecutionPartialResultEvent.java @@ -32,28 +32,7 @@ public void setData(ToolExecutionPartialResultData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class ToolExecutionPartialResultData { - - @JsonProperty("toolCallId") - private String toolCallId; - - @JsonProperty("partialOutput") - private String partialOutput; - - public String getToolCallId() { - return toolCallId; - } - - public void setToolCallId(String toolCallId) { - this.toolCallId = toolCallId; - } - - public String getPartialOutput() { - return partialOutput; - } - - public void setPartialOutput(String partialOutput) { - this.partialOutput = partialOutput; - } + public record ToolExecutionPartialResultData(@JsonProperty("toolCallId") String toolCallId, + @JsonProperty("partialOutput") String partialOutput) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/ToolExecutionProgressEvent.java b/src/main/java/com/github/copilot/sdk/events/ToolExecutionProgressEvent.java index 8350c6634..9fd9baada 100644 --- a/src/main/java/com/github/copilot/sdk/events/ToolExecutionProgressEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/ToolExecutionProgressEvent.java @@ -37,30 +37,7 @@ public ToolExecutionProgressEvent setData(ToolExecutionProgressData data) { } @JsonIgnoreProperties(ignoreUnknown = true) - public static class ToolExecutionProgressData { - - @JsonProperty("toolCallId") - private String toolCallId; - - @JsonProperty("progressMessage") - private String progressMessage; - - public String getToolCallId() { - return toolCallId; - } - - public ToolExecutionProgressData setToolCallId(String toolCallId) { - this.toolCallId = toolCallId; - return this; - } - - public String getProgressMessage() { - return progressMessage; - } - - public ToolExecutionProgressData setProgressMessage(String progressMessage) { - this.progressMessage = progressMessage; - return this; - } + public record ToolExecutionProgressData(@JsonProperty("toolCallId") String toolCallId, + @JsonProperty("progressMessage") String progressMessage) { } } diff --git a/src/main/java/com/github/copilot/sdk/events/package-info.java b/src/main/java/com/github/copilot/sdk/events/package-info.java index 3bbf130f5..b9fed7638 100644 --- a/src/main/java/com/github/copilot/sdk/events/package-info.java +++ b/src/main/java/com/github/copilot/sdk/events/package-info.java @@ -70,7 +70,7 @@ * session.on(evt -> { * if (evt instanceof AssistantMessageDeltaEvent delta) { * // Streaming response - print incrementally - * System.out.print(delta.getData().getDeltaContent()); + * System.out.print(delta.getData().deltaContent()); * } else if (evt instanceof AssistantMessageEvent msg) { * // Complete response * System.out.println("\nFinal: " + msg.getData().getContent()); @@ -79,7 +79,7 @@ * } else if (evt instanceof SessionIdleEvent) { * System.out.println("Session is idle"); * } else if (evt instanceof SessionErrorEvent err) { - * System.err.println("Error: " + err.getData().getMessage()); + * System.err.println("Error: " + err.getData().message()); * } * }); * } diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index b1b25be73..8d474690f 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -72,7 +72,7 @@ session.on(AssistantMessageEvent.class, msg -> { }); session.on(SessionErrorEvent.class, err -> { - System.err.println("Error: " + err.getData().getMessage()); + System.err.println("Error: " + err.getData().message()); }); session.on(SessionIdleEvent.class, idle -> { @@ -91,7 +91,7 @@ session.on(event -> { case AssistantMessageEvent msg -> System.out.println("Response: " + msg.getData().getContent()); case SessionErrorEvent err -> - System.err.println("Error: " + err.getData().getMessage()); + System.err.println("Error: " + err.getData().message()); case SessionIdleEvent idle -> done.complete(null); default -> { } @@ -202,7 +202,7 @@ var done = new CompletableFuture(); session.on(AssistantMessageDeltaEvent.class, delta -> { // Print each chunk as it arrives - System.out.print(delta.getData().getDeltaContent()); + System.out.print(delta.getData().deltaContent()); }); session.on(SessionIdleEvent.class, idle -> { diff --git a/src/site/markdown/getting-started.md b/src/site/markdown/getting-started.md index d7205f765..d1a14a323 100644 --- a/src/site/markdown/getting-started.md +++ b/src/site/markdown/getting-started.md @@ -128,7 +128,7 @@ public class StreamingExample { // Listen for response chunks session.on(AssistantMessageDeltaEvent.class, delta -> { - System.out.print(delta.getData().getDeltaContent()); + System.out.print(delta.getData().deltaContent()); }); session.on(SessionIdleEvent.class, idle -> { System.out.println(); // New line when done @@ -204,7 +204,7 @@ public class ToolExample { var done = new CompletableFuture(); session.on(AssistantMessageDeltaEvent.class, delta -> { - System.out.print(delta.getData().getDeltaContent()); + System.out.print(delta.getData().deltaContent()); }); session.on(SessionIdleEvent.class, idle -> { System.out.println(); @@ -283,7 +283,7 @@ public class WeatherAssistant { // Register listener once, outside the loop session.on(AssistantMessageDeltaEvent.class, delta -> { - System.out.print(delta.getData().getDeltaContent()); + System.out.print(delta.getData().deltaContent()); }); session.on(SessionIdleEvent.class, idle -> { System.out.println(); diff --git a/src/test/java/com/github/copilot/sdk/CompactionTest.java b/src/test/java/com/github/copilot/sdk/CompactionTest.java index fc95d53f3..504be7d52 100644 --- a/src/test/java/com/github/copilot/sdk/CompactionTest.java +++ b/src/test/java/com/github/copilot/sdk/CompactionTest.java @@ -103,7 +103,7 @@ void testShouldTriggerCompactionWithLowThresholdAndEmitEvents() throws Exception .map(e -> (SessionCompactionCompleteEvent) e).reduce((first, second) -> second).orElse(null); assertNotNull(lastCompactionComplete); - assertTrue(lastCompactionComplete.getData().isSuccess(), "Compaction should have succeeded"); + assertTrue(lastCompactionComplete.getData().success(), "Compaction should have succeeded"); // Verify the session still works after compaction AssistantMessageEvent answer = session diff --git a/src/test/java/com/github/copilot/sdk/MetadataApiTest.java b/src/test/java/com/github/copilot/sdk/MetadataApiTest.java index 857e5ab35..b3eb8fcb7 100644 --- a/src/test/java/com/github/copilot/sdk/MetadataApiTest.java +++ b/src/test/java/com/github/copilot/sdk/MetadataApiTest.java @@ -102,8 +102,8 @@ void testToolExecutionProgressEventParsing() throws Exception { ToolExecutionProgressEvent progressEvent = (ToolExecutionProgressEvent) event; assertEquals("tool.execution_progress", progressEvent.getType()); assertNotNull(progressEvent.getData()); - assertEquals("call-123", progressEvent.getData().getToolCallId()); - assertEquals("Processing file 1 of 10...", progressEvent.getData().getProgressMessage()); + assertEquals("call-123", progressEvent.getData().toolCallId()); + assertEquals("Processing file 1 of 10...", progressEvent.getData().progressMessage()); } @Test diff --git a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java index 3743bf275..6640034f8 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java @@ -175,11 +175,11 @@ void testHandlerReceivesCorrectEventData() { }); session.on(SessionStartEvent.class, start -> { - capturedSessionId.set(start.getData().getSessionId()); + capturedSessionId.set(start.getData().sessionId()); }); SessionStartEvent startEvent = createSessionStartEvent(); - startEvent.getData().setSessionId("my-session-123"); + startEvent.setData(new SessionStartEvent.SessionStartData("my-session-123", 0, null, null, null, null)); dispatchEvent(startEvent); AssistantMessageEvent msgEvent = createAssistantMessageEvent("Test content"); @@ -851,8 +851,7 @@ private void dispatchEvent(AbstractSessionEvent event) { // Factory methods for creating test events private SessionStartEvent createSessionStartEvent() { var event = new SessionStartEvent(); - var data = new SessionStartEvent.SessionStartData(); - data.setSessionId("test-session"); + var data = new SessionStartEvent.SessionStartData("test-session", 0, null, null, null, null); event.setData(data); return event; } diff --git a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java index a3fb4605f..f6c871690 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java @@ -59,7 +59,7 @@ void testParseSessionStartEvent() throws Exception { assertEquals("session.start", event.getType()); var startEvent = (SessionStartEvent) event; - assertEquals("sess-123", startEvent.getData().getSessionId()); + assertEquals("sess-123", startEvent.getData().sessionId()); } @Test @@ -98,9 +98,9 @@ void testParseSessionErrorEvent() throws Exception { assertEquals("session.error", event.getType()); var errorEvent = (SessionErrorEvent) event; - assertEquals("RateLimitError", errorEvent.getData().getErrorType()); - assertEquals("Rate limit exceeded", errorEvent.getData().getMessage()); - assertNotNull(errorEvent.getData().getStack()); + assertEquals("RateLimitError", errorEvent.getData().errorType()); + assertEquals("Rate limit exceeded", errorEvent.getData().message()); + assertNotNull(errorEvent.getData().stack()); } @Test @@ -136,8 +136,8 @@ void testParseSessionInfoEvent() throws Exception { assertEquals("session.info", event.getType()); var infoEvent = (SessionInfoEvent) event; - assertEquals("status", infoEvent.getData().getInfoType()); - assertEquals("Processing request", infoEvent.getData().getMessage()); + assertEquals("status", infoEvent.getData().infoType()); + assertEquals("Processing request", infoEvent.getData().message()); } @Test @@ -316,7 +316,7 @@ void testParseAssistantTurnStartEvent() throws Exception { assertEquals("assistant.turn_start", event.getType()); var turnEvent = (AssistantTurnStartEvent) event; - assertEquals("turn-123", turnEvent.getData().getTurnId()); + assertEquals("turn-123", turnEvent.getData().turnId()); } @Test @@ -354,8 +354,8 @@ void testParseAssistantReasoningEvent() throws Exception { assertEquals("assistant.reasoning", event.getType()); var reasoningEvent = (AssistantReasoningEvent) event; - assertEquals("reason-123", reasoningEvent.getData().getReasoningId()); - assertEquals("Analyzing the code structure...", reasoningEvent.getData().getContent()); + assertEquals("reason-123", reasoningEvent.getData().reasoningId()); + assertEquals("Analyzing the code structure...", reasoningEvent.getData().content()); } @Test @@ -576,8 +576,8 @@ void testParseSubagentStartedEvent() throws Exception { assertEquals("subagent.started", event.getType()); var startedEvent = (SubagentStartedEvent) event; - assertEquals("code-review", startedEvent.getData().getAgentName()); - assertEquals("Code Review Agent", startedEvent.getData().getAgentDisplayName()); + assertEquals("code-review", startedEvent.getData().agentName()); + assertEquals("Code Review Agent", startedEvent.getData().agentDisplayName()); } @Test @@ -1016,12 +1016,12 @@ void testSessionStartEventAllFields() throws Exception { var event = (SessionStartEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("sess-full", data.getSessionId()); - assertEquals(2.0, data.getVersion()); - assertEquals("copilot-cli", data.getProducer()); - assertEquals("1.2.3", data.getCopilotVersion()); - assertNotNull(data.getStartTime()); - assertEquals("gpt-4-turbo", data.getSelectedModel()); + assertEquals("sess-full", data.sessionId()); + assertEquals(2.0, data.version()); + assertEquals("copilot-cli", data.producer()); + assertEquals("1.2.3", data.copilotVersion()); + assertNotNull(data.startTime()); + assertEquals("gpt-4-turbo", data.selectedModel()); } @Test @@ -1039,8 +1039,8 @@ void testSessionResumeEventAllFields() throws Exception { var event = (SessionResumeEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertNotNull(data.getResumeTime()); - assertEquals(42.0, data.getEventCount()); + assertNotNull(data.resumeTime()); + assertEquals(42.0, data.eventCount()); } @Test @@ -1061,11 +1061,11 @@ void testSessionErrorEventAllFields() throws Exception { var event = (SessionErrorEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("InternalError", data.getErrorType()); - assertEquals("Something went wrong", data.getMessage()); - assertEquals("at line 42", data.getStack()); - assertEquals(500, data.getStatusCode()); - assertEquals("prov-err-1", data.getProviderCallId()); + assertEquals("InternalError", data.errorType()); + assertEquals("Something went wrong", data.message()); + assertEquals("at line 42", data.stack()); + assertEquals(500, data.statusCode()); + assertEquals("prov-err-1", data.providerCallId()); } @Test @@ -1082,8 +1082,8 @@ void testSessionModelChangeEventAllFields() throws Exception { var event = (SessionModelChangeEvent) parseJson(json); assertNotNull(event); - assertEquals("gpt-4", event.getData().getPreviousModel()); - assertEquals("gpt-4o", event.getData().getNewModel()); + assertEquals("gpt-4", event.getData().previousModel()); + assertEquals("gpt-4o", event.getData().newModel()); } @Test @@ -1109,15 +1109,15 @@ void testSessionHandoffEventAllFields() throws Exception { var event = (SessionHandoffEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertNotNull(data.getHandoffTime()); - assertEquals("cli", data.getSourceType()); - assertEquals("additional context", data.getContext()); - assertEquals("handoff summary", data.getSummary()); - assertEquals("remote-sess-1", data.getRemoteSessionId()); - assertNotNull(data.getRepository()); - assertEquals("my-org", data.getRepository().getOwner()); - assertEquals("my-repo", data.getRepository().getName()); - assertEquals("main", data.getRepository().getBranch()); + assertNotNull(data.handoffTime()); + assertEquals("cli", data.sourceType()); + assertEquals("additional context", data.context()); + assertEquals("handoff summary", data.summary()); + assertEquals("remote-sess-1", data.remoteSessionId()); + assertNotNull(data.repository()); + assertEquals("my-org", data.repository().owner()); + assertEquals("my-repo", data.repository().name()); + assertEquals("main", data.repository().branch()); } @Test @@ -1141,14 +1141,14 @@ void testSessionTruncationEventAllFields() throws Exception { var event = (SessionTruncationEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals(128000.0, data.getTokenLimit()); - assertEquals(150000.0, data.getPreTruncationTokensInMessages()); - assertEquals(100.0, data.getPreTruncationMessagesLength()); - assertEquals(120000.0, data.getPostTruncationTokensInMessages()); - assertEquals(80.0, data.getPostTruncationMessagesLength()); - assertEquals(30000.0, data.getTokensRemovedDuringTruncation()); - assertEquals(20.0, data.getMessagesRemovedDuringTruncation()); - assertEquals("system", data.getPerformedBy()); + assertEquals(128000.0, data.tokenLimit()); + assertEquals(150000.0, data.preTruncationTokensInMessages()); + assertEquals(100.0, data.preTruncationMessagesLength()); + assertEquals(120000.0, data.postTruncationTokensInMessages()); + assertEquals(80.0, data.postTruncationMessagesLength()); + assertEquals(30000.0, data.tokensRemovedDuringTruncation()); + assertEquals(20.0, data.messagesRemovedDuringTruncation()); + assertEquals("system", data.performedBy()); } @Test @@ -1167,9 +1167,9 @@ void testSessionUsageInfoEventAllFields() throws Exception { var event = (SessionUsageInfoEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals(128000.0, data.getTokenLimit()); - assertEquals(50000.0, data.getCurrentTokens()); - assertEquals(25.0, data.getMessagesLength()); + assertEquals(128000.0, data.tokenLimit()); + assertEquals(50000.0, data.currentTokens()); + assertEquals(25.0, data.messagesLength()); } @Test @@ -1201,23 +1201,23 @@ void testSessionCompactionCompleteEventAllFields() throws Exception { var event = (SessionCompactionCompleteEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertTrue(data.isSuccess()); - assertNull(data.getError()); - assertEquals(150000.0, data.getPreCompactionTokens()); - assertEquals(60000.0, data.getPostCompactionTokens()); - assertEquals(100.0, data.getPreCompactionMessagesLength()); - assertEquals(50.0, data.getMessagesRemoved()); - assertEquals(90000.0, data.getTokensRemoved()); - assertEquals("Compacted conversation", data.getSummaryContent()); - assertEquals(3.0, data.getCheckpointNumber()); - assertEquals("/checkpoints/3", data.getCheckpointPath()); - assertEquals("req-compact-1", data.getRequestId()); - - var tokens = data.getCompactionTokensUsed(); + assertTrue(data.success()); + assertNull(data.error()); + assertEquals(150000.0, data.preCompactionTokens()); + assertEquals(60000.0, data.postCompactionTokens()); + assertEquals(100.0, data.preCompactionMessagesLength()); + assertEquals(50.0, data.messagesRemoved()); + assertEquals(90000.0, data.tokensRemoved()); + assertEquals("Compacted conversation", data.summaryContent()); + assertEquals(3.0, data.checkpointNumber()); + assertEquals("/checkpoints/3", data.checkpointPath()); + assertEquals("req-compact-1", data.requestId()); + + var tokens = data.compactionTokensUsed(); assertNotNull(tokens); - assertEquals(1000.0, tokens.getInput()); - assertEquals(500.0, tokens.getOutput()); - assertEquals(200.0, tokens.getCachedInput()); + assertEquals(1000.0, tokens.input()); + assertEquals(500.0, tokens.output()); + assertEquals(200.0, tokens.cachedInput()); } @Test @@ -1332,10 +1332,10 @@ void testAssistantMessageDeltaEventAllFields() throws Exception { var event = (AssistantMessageDeltaEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("msg-delta-1", data.getMessageId()); - assertEquals("partial text", data.getDeltaContent()); - assertEquals(4096.0, data.getTotalResponseSizeBytes()); - assertEquals("ptc-1", data.getParentToolCallId()); + assertEquals("msg-delta-1", data.messageId()); + assertEquals("partial text", data.deltaContent()); + assertEquals(4096.0, data.totalResponseSizeBytes()); + assertEquals("ptc-1", data.parentToolCallId()); } @Test @@ -1395,8 +1395,8 @@ void testAssistantReasoningDeltaEventAllFields() throws Exception { var event = (AssistantReasoningDeltaEvent) parseJson(json); assertNotNull(event); - assertEquals("r-delta-1", event.getData().getReasoningId()); - assertEquals("thinking about...", event.getData().getDeltaContent()); + assertEquals("r-delta-1", event.getData().reasoningId()); + assertEquals("thinking about...", event.getData().deltaContent()); } @Test @@ -1412,7 +1412,7 @@ void testAssistantIntentEventAllFields() throws Exception { var event = (AssistantIntentEvent) parseJson(json); assertNotNull(event); - assertEquals("refactor_code", event.getData().getIntent()); + assertEquals("refactor_code", event.getData().intent()); } @Test @@ -1428,7 +1428,7 @@ void testAssistantTurnEndEventAllFields() throws Exception { var event = (AssistantTurnEndEvent) parseJson(json); assertNotNull(event); - assertEquals("turn-end-1", event.getData().getTurnId()); + assertEquals("turn-end-1", event.getData().turnId()); } // ========================================================================= @@ -1540,8 +1540,8 @@ void testToolExecutionPartialResultEventAllFields() throws Exception { var event = (ToolExecutionPartialResultEvent) parseJson(json); assertNotNull(event); - assertEquals("tc-partial-1", event.getData().getToolCallId()); - assertEquals("partial output data", event.getData().getPartialOutput()); + assertEquals("tc-partial-1", event.getData().toolCallId()); + assertEquals("partial output data", event.getData().partialOutput()); } @Test @@ -1558,8 +1558,8 @@ void testToolExecutionProgressEventAllFields() throws Exception { var event = (ToolExecutionProgressEvent) parseJson(json); assertNotNull(event); - assertEquals("tc-prog-1", event.getData().getToolCallId()); - assertEquals("50% done", event.getData().getProgressMessage()); + assertEquals("tc-prog-1", event.getData().toolCallId()); + assertEquals("50% done", event.getData().progressMessage()); } @Test @@ -1676,10 +1676,10 @@ void testSubagentStartedEventAllFields() throws Exception { var event = (SubagentStartedEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals("tc-sub-1", data.getToolCallId()); - assertEquals("test-agent", data.getAgentName()); - assertEquals("Test Agent", data.getAgentDisplayName()); - assertEquals("A test subagent", data.getAgentDescription()); + assertEquals("tc-sub-1", data.toolCallId()); + assertEquals("test-agent", data.agentName()); + assertEquals("Test Agent", data.agentDisplayName()); + assertEquals("A test subagent", data.agentDescription()); } @Test @@ -1696,8 +1696,8 @@ void testSubagentCompletedEventAllFields() throws Exception { var event = (SubagentCompletedEvent) parseJson(json); assertNotNull(event); - assertEquals("tc-sub-2", event.getData().getToolCallId()); - assertEquals("reviewer", event.getData().getAgentName()); + assertEquals("tc-sub-2", event.getData().toolCallId()); + assertEquals("reviewer", event.getData().agentName()); } @Test @@ -1715,9 +1715,9 @@ void testSubagentFailedEventAllFields() throws Exception { var event = (SubagentFailedEvent) parseJson(json); assertNotNull(event); - assertEquals("tc-sub-3", event.getData().getToolCallId()); - assertEquals("broken-agent", event.getData().getAgentName()); - assertEquals("Connection timeout", event.getData().getError()); + assertEquals("tc-sub-3", event.getData().toolCallId()); + assertEquals("broken-agent", event.getData().agentName()); + assertEquals("Connection timeout", event.getData().error()); } @Test @@ -1835,7 +1835,7 @@ void testAbortEventAllFields() throws Exception { var event = (AbortEvent) parseJson(json); assertNotNull(event); - assertEquals("user_cancelled", event.getData().getReason()); + assertEquals("user_cancelled", event.getData().reason()); } @Test @@ -1877,8 +1877,8 @@ void testSessionInfoEventAllFields() throws Exception { var event = (SessionInfoEvent) parseJson(json); assertNotNull(event); - assertEquals("model_selection", event.getData().getInfoType()); - assertEquals("Using gpt-4-turbo for this task", event.getData().getMessage()); + assertEquals("model_selection", event.getData().infoType()); + assertEquals("Using gpt-4-turbo for this task", event.getData().message()); } // ========================================================================= From 48c18a29b2ad55875913c6fb3243464333983ab0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:47:56 +0000 Subject: [PATCH 056/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 9a69863bd..76bb49190 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 78.9% - 78.9% + 78.2% + 78.2% From b9c714f014c00b3e52a6edc76af84b2436731928 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 13:51:06 -0500 Subject: [PATCH 057/147] Potential fix for pull request finding 'Useless parameter' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../java/com/github/copilot/sdk/SessionEventHandlingTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java index a79abef4f..6a6737754 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java @@ -855,7 +855,7 @@ private SessionStartEvent createSessionStartEvent() { private SessionStartEvent createSessionStartEvent(String sessionId) { var event = new SessionStartEvent(); - var data = new SessionStartEvent.SessionStartData("test-session", 0, null, null, null, null); + var data = new SessionStartEvent.SessionStartData(sessionId, 0, null, null, null, null); event.setData(data); return event; } From cd72693605e878d99d5878006eb9b7b6ba415018 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:58:09 +0000 Subject: [PATCH 058/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 76bb49190..dd5c086cb 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 78.2% - 78.2% + 77.4% + 77.4% From 4b731515b864120beb8679aa37071ccff00ad406 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 14:08:58 -0500 Subject: [PATCH 059/147] test: add unit tests for ProviderConfig and AzureOptions classes --- .../copilot/sdk/ProviderConfigTest.java | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 src/test/java/com/github/copilot/sdk/ProviderConfigTest.java diff --git a/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java b/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java new file mode 100644 index 000000000..d3e18010b --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java @@ -0,0 +1,389 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.github.copilot.sdk.json.AzureOptions; +import com.github.copilot.sdk.json.ProviderConfig; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * Tests for {@link ProviderConfig} and {@link AzureOptions} BYOK (Bring Your + * Own Key) configuration. + * + *

+ * Covers fluent setters, JSON serialization, null-field omission, and + * integration with {@link SessionConfig} and {@link ResumeSessionConfig}. + *

+ */ +public class ProviderConfigTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + // ========================================================================= + // Fluent setters and getters + // ========================================================================= + + @Test + void testDefaultsAreNull() { + var provider = new ProviderConfig(); + + assertNull(provider.getType()); + assertNull(provider.getWireApi()); + assertNull(provider.getBaseUrl()); + assertNull(provider.getApiKey()); + assertNull(provider.getBearerToken()); + assertNull(provider.getAzure()); + } + + @Test + void testFluentSettersReturnSameInstance() { + var provider = new ProviderConfig(); + + ProviderConfig result = provider.setType("openai").setWireApi("completions") + .setBaseUrl("https://api.openai.com/v1").setApiKey("sk-test-key").setBearerToken("bearer-token") + .setAzure(new AzureOptions()); + + // All chained calls should return the same instance + assertEquals(provider, result); + } + + @Test + void testGettersReturnSetValues() { + var azure = new AzureOptions().setApiVersion("2024-02-01"); + var provider = new ProviderConfig().setType("azure-openai").setWireApi("chat") + .setBaseUrl("https://my-resource.openai.azure.com").setApiKey("my-key").setBearerToken("my-token") + .setAzure(azure); + + assertEquals("azure-openai", provider.getType()); + assertEquals("chat", provider.getWireApi()); + assertEquals("https://my-resource.openai.azure.com", provider.getBaseUrl()); + assertEquals("my-key", provider.getApiKey()); + assertEquals("my-token", provider.getBearerToken()); + assertNotNull(provider.getAzure()); + assertEquals("2024-02-01", provider.getAzure().getApiVersion()); + } + + // ========================================================================= + // AzureOptions + // ========================================================================= + + @Test + void testAzureOptionsDefaultsAreNull() { + var azure = new AzureOptions(); + assertNull(azure.getApiVersion()); + } + + @Test + void testAzureOptionsFluentSetter() { + var azure = new AzureOptions(); + AzureOptions result = azure.setApiVersion("2023-12-01-preview"); + + assertEquals(azure, result); + assertEquals("2023-12-01-preview", azure.getApiVersion()); + } + + // ========================================================================= + // JSON serialization — OpenAI BYOK + // ========================================================================= + + @Test + void testSerializeOpenAiProvider() throws Exception { + var provider = new ProviderConfig().setType("openai").setBaseUrl("https://api.openai.com/v1") + .setApiKey("sk-test-key"); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("openai", json.get("type").asText()); + assertEquals("https://api.openai.com/v1", json.get("baseUrl").asText()); + assertEquals("sk-test-key", json.get("apiKey").asText()); + // Null fields must be omitted (NON_NULL) + assertTrue(json.path("wireApi").isMissingNode()); + assertTrue(json.path("bearerToken").isMissingNode()); + assertTrue(json.path("azure").isMissingNode()); + } + + @Test + void testDeserializeOpenAiProvider() throws Exception { + String json = """ + { + "type": "openai", + "baseUrl": "https://api.openai.com/v1", + "apiKey": "sk-test-key" + } + """; + + ProviderConfig provider = MAPPER.readValue(json, ProviderConfig.class); + + assertEquals("openai", provider.getType()); + assertEquals("https://api.openai.com/v1", provider.getBaseUrl()); + assertEquals("sk-test-key", provider.getApiKey()); + assertNull(provider.getWireApi()); + assertNull(provider.getBearerToken()); + assertNull(provider.getAzure()); + } + + // ========================================================================= + // JSON serialization — Azure OpenAI BYOK + // ========================================================================= + + @Test + void testSerializeAzureOpenAiProvider() throws Exception { + var provider = new ProviderConfig().setType("azure-openai").setBaseUrl("https://my-resource.openai.azure.com") + .setApiKey("azure-api-key").setAzure(new AzureOptions().setApiVersion("2024-02-01")); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("azure-openai", json.get("type").asText()); + assertEquals("https://my-resource.openai.azure.com", json.get("baseUrl").asText()); + assertEquals("azure-api-key", json.get("apiKey").asText()); + assertNotNull(json.get("azure")); + assertEquals("2024-02-01", json.get("azure").get("apiVersion").asText()); + } + + @Test + void testDeserializeAzureOpenAiProvider() throws Exception { + String json = """ + { + "type": "azure-openai", + "baseUrl": "https://my-resource.openai.azure.com", + "apiKey": "azure-key", + "azure": { + "apiVersion": "2024-02-01" + } + } + """; + + ProviderConfig provider = MAPPER.readValue(json, ProviderConfig.class); + + assertEquals("azure-openai", provider.getType()); + assertEquals("https://my-resource.openai.azure.com", provider.getBaseUrl()); + assertEquals("azure-key", provider.getApiKey()); + assertNotNull(provider.getAzure()); + assertEquals("2024-02-01", provider.getAzure().getApiVersion()); + } + + // ========================================================================= + // JSON serialization — Bearer token authentication + // ========================================================================= + + @Test + void testSerializeBearerTokenProvider() throws Exception { + var provider = new ProviderConfig().setType("openai").setBaseUrl("https://custom-provider.example.com/v1") + .setBearerToken("eyJhbGciOiJSUzI1NiIs..."); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("openai", json.get("type").asText()); + assertEquals("https://custom-provider.example.com/v1", json.get("baseUrl").asText()); + assertEquals("eyJhbGciOiJSUzI1NiIs...", json.get("bearerToken").asText()); + assertTrue(json.path("apiKey").isMissingNode()); + } + + @Test + void testDeserializeBearerTokenProvider() throws Exception { + String json = """ + { + "type": "openai", + "baseUrl": "https://custom-provider.example.com/v1", + "bearerToken": "my-bearer-token" + } + """; + + ProviderConfig provider = MAPPER.readValue(json, ProviderConfig.class); + + assertEquals("openai", provider.getType()); + assertEquals("https://custom-provider.example.com/v1", provider.getBaseUrl()); + assertEquals("my-bearer-token", provider.getBearerToken()); + assertNull(provider.getApiKey()); + } + + // ========================================================================= + // JSON serialization — custom wire API + // ========================================================================= + + @Test + void testSerializeCustomWireApi() throws Exception { + var provider = new ProviderConfig().setType("openai").setBaseUrl("https://custom.example.com").setApiKey("key") + .setWireApi("responses"); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("responses", json.get("wireApi").asText()); + } + + // ========================================================================= + // JSON serialization — all fields populated + // ========================================================================= + + @Test + void testSerializeAllFields() throws Exception { + var provider = new ProviderConfig().setType("azure-openai").setWireApi("completions") + .setBaseUrl("https://my-resource.openai.azure.com").setApiKey("my-api-key") + .setBearerToken("my-bearer-token").setAzure(new AzureOptions().setApiVersion("2024-02-01")); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("azure-openai", json.get("type").asText()); + assertEquals("completions", json.get("wireApi").asText()); + assertEquals("https://my-resource.openai.azure.com", json.get("baseUrl").asText()); + assertEquals("my-api-key", json.get("apiKey").asText()); + assertEquals("my-bearer-token", json.get("bearerToken").asText()); + assertEquals("2024-02-01", json.get("azure").get("apiVersion").asText()); + assertEquals(6, json.size(), "Expected exactly 6 JSON fields"); + } + + @Test + void testSerializeEmptyProviderOmitsAllFields() throws Exception { + var provider = new ProviderConfig(); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals(0, json.size(), "Empty ProviderConfig should serialize to {}"); + } + + @Test + void testSerializeEmptyAzureOptionsOmitsAllFields() throws Exception { + var azure = new AzureOptions(); + + JsonNode json = MAPPER.valueToTree(azure); + + assertEquals(0, json.size(), "Empty AzureOptions should serialize to {}"); + } + + // ========================================================================= + // JSON round-trip + // ========================================================================= + + @Test + void testRoundTripProviderConfig() throws Exception { + var original = new ProviderConfig().setType("azure-openai").setWireApi("completions") + .setBaseUrl("https://my-resource.openai.azure.com").setApiKey("my-key").setBearerToken("my-token") + .setAzure(new AzureOptions().setApiVersion("2024-02-01")); + + String json = MAPPER.writeValueAsString(original); + ProviderConfig deserialized = MAPPER.readValue(json, ProviderConfig.class); + + assertEquals(original.getType(), deserialized.getType()); + assertEquals(original.getWireApi(), deserialized.getWireApi()); + assertEquals(original.getBaseUrl(), deserialized.getBaseUrl()); + assertEquals(original.getApiKey(), deserialized.getApiKey()); + assertEquals(original.getBearerToken(), deserialized.getBearerToken()); + assertNotNull(deserialized.getAzure()); + assertEquals(original.getAzure().getApiVersion(), deserialized.getAzure().getApiVersion()); + } + + @Test + void testForwardCompatibilityIgnoresUnknownFields() throws Exception { + String json = """ + { + "type": "openai", + "baseUrl": "https://api.openai.com/v1", + "apiKey": "sk-key", + "unknownFutureField": "some-value", + "anotherNewField": 42 + } + """; + + // Should not throw - ObjectMapper is configured with + // FAIL_ON_UNKNOWN_PROPERTIES = false + ProviderConfig provider = MAPPER.readValue(json, ProviderConfig.class); + + assertEquals("openai", provider.getType()); + assertEquals("https://api.openai.com/v1", provider.getBaseUrl()); + assertEquals("sk-key", provider.getApiKey()); + } + + // ========================================================================= + // Integration with SessionConfig + // ========================================================================= + + @Test + void testSessionConfigWithOpenAiProvider() throws Exception { + var config = new SessionConfig().setModel("gpt-4").setProvider(new ProviderConfig().setType("openai") + .setBaseUrl("https://api.openai.com/v1").setApiKey("sk-test-key")); + + JsonNode json = MAPPER.valueToTree(config); + + assertNotNull(json.get("provider")); + assertEquals("openai", json.get("provider").get("type").asText()); + assertEquals("https://api.openai.com/v1", json.get("provider").get("baseUrl").asText()); + assertEquals("sk-test-key", json.get("provider").get("apiKey").asText()); + assertEquals("gpt-4", json.get("model").asText()); + } + + @Test + void testSessionConfigWithAzureProvider() throws Exception { + var config = new SessionConfig().setModel("gpt-4").setProvider( + new ProviderConfig().setType("azure-openai").setBaseUrl("https://my-resource.openai.azure.com") + .setApiKey("azure-key").setAzure(new AzureOptions().setApiVersion("2024-02-01"))); + + JsonNode json = MAPPER.valueToTree(config); + + JsonNode providerNode = json.get("provider"); + assertNotNull(providerNode); + assertEquals("azure-openai", providerNode.get("type").asText()); + assertEquals("2024-02-01", providerNode.get("azure").get("apiVersion").asText()); + } + + @Test + void testSessionConfigWithoutProviderOmitsField() throws Exception { + var config = new SessionConfig().setModel("gpt-4"); + + JsonNode json = MAPPER.valueToTree(config); + + assertTrue(json.path("provider").isMissingNode(), "provider field should be omitted when null"); + } + + // ========================================================================= + // Integration with ResumeSessionConfig + // ========================================================================= + + @Test + void testResumeSessionConfigWithProvider() throws Exception { + var config = new ResumeSessionConfig().setStreaming(true).setProvider(new ProviderConfig().setType("openai") + .setBaseUrl("https://api.openai.com/v1").setBearerToken("my-bearer-token")); + + assertNotNull(config.getProvider()); + assertEquals("openai", config.getProvider().getType()); + assertEquals("https://api.openai.com/v1", config.getProvider().getBaseUrl()); + assertEquals("my-bearer-token", config.getProvider().getBearerToken()); + } + + @Test + void testResumeSessionConfigProviderSerialization() throws Exception { + var config = new ResumeSessionConfig().setProvider( + new ProviderConfig().setType("azure-openai").setBaseUrl("https://my-resource.openai.azure.com") + .setApiKey("key").setAzure(new AzureOptions().setApiVersion("2024-02-01"))); + + JsonNode json = MAPPER.valueToTree(config); + + JsonNode providerNode = json.get("provider"); + assertNotNull(providerNode); + assertEquals("azure-openai", providerNode.get("type").asText()); + assertEquals("https://my-resource.openai.azure.com", providerNode.get("baseUrl").asText()); + assertEquals("key", providerNode.get("apiKey").asText()); + assertEquals("2024-02-01", providerNode.get("azure").get("apiVersion").asText()); + } + + @Test + void testResumeSessionConfigWithoutProviderOmitsField() throws Exception { + var config = new ResumeSessionConfig().setStreaming(true); + + JsonNode json = MAPPER.valueToTree(config); + + assertTrue(json.path("provider").isMissingNode(), "provider field should be omitted when null"); + } +} From f1c66b7cd409c1f65e8d0985995b5798a4bcf3f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 19:12:38 +0000 Subject: [PATCH 060/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index dd5c086cb..a310734d0 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 77.4% - 77.4% + 78.3% + 78.3% From f4fe554167e4135d55f211676c22330669e711c9 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 14:39:47 -0500 Subject: [PATCH 061/147] chore: update pre-commit hook to skip Spotless check if no changes in src/ --- .githooks/pre-commit | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 18313be52..d9372a61b 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -5,6 +5,12 @@ # git config core.hooksPath .githooks # +# Only run Spotless if staged changes include files under src/ +if ! git diff --cached --name-only | grep -q '^src/'; then + echo "No changes in src/, skipping Spotless check." + exit 0 +fi + echo "Running Spotless check..." # Run spotless check From 493b21a9b42bd8ae6668ba12f3a8850f8ad5018e Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 14:39:52 -0500 Subject: [PATCH 062/147] docs: update commit-as-pull-request prompt to clarify build verification steps --- .../prompts/commit-as-pull-request.prompt.md | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/prompts/commit-as-pull-request.prompt.md b/.github/prompts/commit-as-pull-request.prompt.md index 75368faed..91b33fabc 100644 --- a/.github/prompts/commit-as-pull-request.prompt.md +++ b/.github/prompts/commit-as-pull-request.prompt.md @@ -5,6 +5,8 @@ You are an automated assistant that takes the current uncommitted changes in the ## Prerequisites - The workspace must be a git repository with a configured remote named `origin`. +- The project must be compiling successfully with the current changes (if applicable). +- The project must be building successfully with the current changes (if applicable). - There must be uncommitted changes (staged or unstaged) in the working tree. - The GitHub MCP tools must be available for creating and merging pull requests. @@ -29,7 +31,7 @@ eval "$(.github/scripts/ci/parse-repo-info.sh)" # Sets: REPO_OWNER, REPO_NAME ``` -### Step 2: Auto-detect branch name and commit message +### Step 2: Auto-detect branch name and define commit message Analyze the changed files using `git diff` (and `git diff --cached` for staged changes) to understand what was modified. Generate: @@ -40,7 +42,19 @@ Analyze the changed files using `git diff` (and `git diff --cached` for staged c If the user has provided an explicit branch name or commit message, use those instead. -### Step 3: Commit and push +### Step 3: Verify the project builds + +If the changes include Java source files, run the build to confirm the project compiles and tests pass: + +```bash +mvn clean verify +``` + +If only non-Java files changed (e.g., documentation, scripts, configuration), this step may be skipped. + +**Stop immediately if the build fails.** Do not proceed to commit broken code. + +### Step 4: Commit and push Runs the formatter (if applicable), creates the branch, stages all changes, commits, and pushes: @@ -51,27 +65,27 @@ Runs the formatter (if applicable), creates the branch, stages all changes, comm Pass `--skip-format` as a third argument to skip `mvn spotless:apply` (e.g., when only non-Java files changed). -### Step 4: Create a pull request +### Step 5: Create a pull request Use the GitHub MCP `create_pull_request` tool with: - **owner** and **repo**: from Step 1 - **title**: the first line of the commit message -- **head**: the branch name from Step 3 +- **head**: the branch name from Step 4 - **base**: `main` (or the repository's default branch) - **body**: A well-structured PR description including: - **Summary**: What the change does and why - **Changes**: Bullet list of files/areas modified - **Testing**: How the changes were verified -### Step 5: Merge the pull request +### Step 6: Merge the pull request Use the GitHub MCP `merge_pull_request` tool with: - **merge_method**: `squash` - **commit_title**: ` (#)` -### Step 6: Sync and clean up +### Step 7: Sync and clean up ```bash .github/scripts/ci/sync-after-merge.sh "" From ec936f0572821d3893d9079fc5695cfd56ea5468 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 14:46:56 -0500 Subject: [PATCH 063/147] test: add unit tests for RpcHandlerDispatcher to cover edge cases and error paths --- .../copilot/sdk/RpcHandlerDispatcherTest.java | 530 ++++++++++++++++++ 1 file changed, 530 insertions(+) create mode 100644 src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java diff --git a/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java b/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java new file mode 100644 index 000000000..883900892 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java @@ -0,0 +1,530 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiConsumer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PreToolUseHookOutput; +import com.github.copilot.sdk.json.SessionHooks; +import com.github.copilot.sdk.json.SessionLifecycleEvent; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.ToolResultObject; +import com.github.copilot.sdk.json.UserInputResponse; + +/** + * Unit tests for {@link RpcHandlerDispatcher} focusing on coverage gaps + * identified by JaCoCo: unknown sessions, missing fields, error paths, and edge + * cases for each handler method. + */ +class RpcHandlerDispatcherTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + private static final int SOCKET_TIMEOUT_MS = 5000; + + private Socket clientSideSocket; + private Socket serverSideSocket; + private JsonRpcClient rpc; + private Map sessions; + private CopyOnWriteArrayList lifecycleEvents; + private RpcHandlerDispatcher dispatcher; + private InputStream responseStream; + private Map> handlers; + + @BeforeEach + void setup() throws Exception { + // Create a socket pair for the JsonRpcClient + try (ServerSocket ss = new ServerSocket(0)) { + clientSideSocket = new Socket("localhost", ss.getLocalPort()); + serverSideSocket = ss.accept(); + } + serverSideSocket.setSoTimeout(SOCKET_TIMEOUT_MS); + + rpc = JsonRpcClient.fromSocket(clientSideSocket); + responseStream = serverSideSocket.getInputStream(); + + sessions = new ConcurrentHashMap<>(); + lifecycleEvents = new CopyOnWriteArrayList<>(); + dispatcher = new RpcHandlerDispatcher(sessions, lifecycleEvents::add); + dispatcher.registerHandlers(rpc); + + // Extract the registered handlers via reflection so we can invoke them directly + Field f = JsonRpcClient.class.getDeclaredField("notificationHandlers"); + f.setAccessible(true); + @SuppressWarnings("unchecked") + Map> h = (Map>) f.get(rpc); + handlers = h; + } + + @AfterEach + void teardown() throws Exception { + if (rpc != null) { + rpc.close(); + } + if (serverSideSocket != null) { + serverSideSocket.close(); + } + if (clientSideSocket != null) { + clientSideSocket.close(); + } + } + + /** Invoke a registered RPC handler directly. */ + private void invokeHandler(String method, String requestId, JsonNode params) { + handlers.get(method).accept(requestId, params); + } + + /** Read a single JSON-RPC response message from the server-side socket. */ + private JsonNode readResponse() throws Exception { + StringBuilder header = new StringBuilder(); + while (!header.toString().endsWith("\r\n\r\n")) { + int b = responseStream.read(); + if (b == -1) { + throw new java.io.IOException("Unexpected end of stream"); + } + header.append((char) b); + } + String headerStr = header.toString().trim(); + int idx = headerStr.indexOf(':'); + int contentLength = Integer.parseInt(headerStr.substring(idx + 1).trim()); + byte[] body = responseStream.readNBytes(contentLength); + return MAPPER.readTree(body); + } + + /** Create and register a CopilotSession in the sessions map. */ + private CopilotSession createSession(String sessionId) { + CopilotSession session = new CopilotSession(sessionId, rpc); + sessions.put(sessionId, session); + return session; + } + + // ===== session.event tests ===== + + @Test + void sessionEventWithNullEventNode() throws Exception { + CopilotSession session = createSession("s1"); + var dispatched = new CopyOnWriteArrayList<>(); + session.on(dispatched::add); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + // "event" field is absent → eventNode is null + + invokeHandler("session.event", null, params); + + // Give a moment for async processing (though this handler is synchronous) + Thread.sleep(50); + assertTrue(dispatched.isEmpty(), "No events should be dispatched when eventNode is null"); + } + + @Test + void sessionEventWithUnknownSession() { + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "unknown"); + ObjectNode event = params.putObject("event"); + event.put("type", "assistantMessage"); + event.putObject("data").put("content", "hello"); + + // Should not throw — silently skips when session is not found + assertDoesNotThrow(() -> invokeHandler("session.event", null, params)); + } + + // ===== session.lifecycle tests ===== + + @Test + void lifecycleEventWithMissingTypeAndSessionId() { + ObjectNode params = MAPPER.createObjectNode(); + // No "type" or "sessionId" fields — defaults to "" + + invokeHandler("session.lifecycle", null, params); + + assertEquals(1, lifecycleEvents.size()); + assertEquals("", lifecycleEvents.get(0).getType()); + assertEquals("", lifecycleEvents.get(0).getSessionId()); + } + + @Test + void lifecycleEventWithoutMetadata() { + ObjectNode params = MAPPER.createObjectNode(); + params.put("type", "started"); + params.put("sessionId", "s1"); + // No "metadata" field at all + + invokeHandler("session.lifecycle", null, params); + + assertEquals(1, lifecycleEvents.size()); + assertEquals("started", lifecycleEvents.get(0).getType()); + assertNull(lifecycleEvents.get(0).getMetadata()); + } + + @Test + void lifecycleEventWithNullMetadata() { + ObjectNode params = MAPPER.createObjectNode(); + params.put("type", "ended"); + params.put("sessionId", "s2"); + params.putNull("metadata"); + + invokeHandler("session.lifecycle", null, params); + + assertEquals(1, lifecycleEvents.size()); + assertEquals("ended", lifecycleEvents.get(0).getType()); + assertNull(lifecycleEvents.get(0).getMetadata()); + } + + // ===== tool.call tests ===== + + @Test + void toolCallWithUnknownSession() throws Exception { + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "nonexistent"); + params.put("toolCallId", "tc1"); + params.put("toolName", "my_tool"); + params.putObject("arguments"); + + invokeHandler("tool.call", "1", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32602, response.get("error").get("code").asInt()); + assertTrue(response.get("error").get("message").asText().contains("nonexistent")); + } + + @Test + void toolCallWithUnknownTool() throws Exception { + createSession("s1"); + // Don't register any tools + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("toolCallId", "tc1"); + params.put("toolName", "nonexistent_tool"); + params.putObject("arguments"); + + invokeHandler("tool.call", "2", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("failure", result.get("resultType").asText()); + assertTrue(result.get("error").asText().contains("nonexistent_tool")); + } + + @Test + void toolCallReturnsToolResultObjectDirectly() throws Exception { + CopilotSession session = createSession("s1"); + var tool = ToolDefinition.create("my_tool", "A test tool", Map.of("type", "object"), + invocation -> CompletableFuture.completedFuture( + new ToolResultObject().setResultType("success").setTextResultForLlm("direct result"))); + session.registerTools(List.of(tool)); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("toolCallId", "tc1"); + params.put("toolName", "my_tool"); + params.putObject("arguments"); + + invokeHandler("tool.call", "3", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("success", result.get("resultType").asText()); + assertEquals("direct result", result.get("textResultForLlm").asText()); + } + + @Test + void toolCallWithNonStringResult() throws Exception { + CopilotSession session = createSession("s1"); + var tool = ToolDefinition.create("map_tool", "Returns a map", Map.of("type", "object"), + invocation -> CompletableFuture.completedFuture(Map.of("key", "value"))); + session.registerTools(List.of(tool)); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("toolCallId", "tc1"); + params.put("toolName", "map_tool"); + params.putObject("arguments"); + + invokeHandler("tool.call", "4", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("success", result.get("resultType").asText()); + // The map should be serialized to JSON string + assertNotNull(result.get("textResultForLlm").asText()); + } + + @Test + void toolCallHandlerFails() throws Exception { + CopilotSession session = createSession("s1"); + var tool = ToolDefinition.create("fail_tool", "Fails", Map.of("type", "object"), + invocation -> CompletableFuture.failedFuture(new RuntimeException("tool error"))); + session.registerTools(List.of(tool)); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("toolCallId", "tc1"); + params.put("toolName", "fail_tool"); + params.putObject("arguments"); + + invokeHandler("tool.call", "5", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("failure", result.get("resultType").asText()); + } + + // ===== permission.request tests ===== + + @Test + void permissionRequestWithUnknownSession() throws Exception { + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "nonexistent"); + params.putObject("permissionRequest"); + + invokeHandler("permission.request", "10", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("denied-no-approval-rule-and-could-not-request-from-user", result.get("kind").asText()); + } + + @Test + void permissionRequestWithHandler() throws Exception { + CopilotSession session = createSession("s1"); + session.registerPermissionHandler((request, invocation) -> CompletableFuture + .completedFuture(new PermissionRequestResult().setKind("allow"))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.putObject("permissionRequest"); + + invokeHandler("permission.request", "11", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("allow", result.get("kind").asText()); + } + + @Test + void permissionRequestHandlerFails() throws Exception { + CopilotSession session = createSession("s1"); + session.registerPermissionHandler( + (request, invocation) -> CompletableFuture.failedFuture(new RuntimeException("permission error"))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.putObject("permissionRequest"); + + invokeHandler("permission.request", "12", params); + + JsonNode response = readResponse(); + // CopilotSession catches the exception and returns a denied result + JsonNode result = response.get("result").get("result"); + assertEquals("denied-no-approval-rule-and-could-not-request-from-user", result.get("kind").asText()); + } + + // ===== userInput.request tests ===== + + @Test + void userInputRequestWithUnknownSession() throws Exception { + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "nonexistent"); + params.put("question", "What?"); + + invokeHandler("userInput.request", "20", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32602, response.get("error").get("code").asInt()); + } + + @Test + void userInputRequestWithNullChoicesAndFreeform() throws Exception { + CopilotSession session = createSession("s1"); + session.registerUserInputHandler((request, invocation) -> CompletableFuture + .completedFuture(new UserInputResponse().setAnswer("my answer").setWasFreeform(true))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("question", "What is your name?"); + // No "choices" or "allowFreeform" fields + + invokeHandler("userInput.request", "21", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result"); + assertEquals("my answer", result.get("answer").asText()); + assertTrue(result.get("wasFreeform").asBoolean()); + } + + @Test + void userInputRequestWithNullAnswer() throws Exception { + CopilotSession session = createSession("s1"); + session.registerUserInputHandler((request, invocation) -> CompletableFuture + .completedFuture(new UserInputResponse().setAnswer(null).setWasFreeform(false))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("question", "Choose something"); + + invokeHandler("userInput.request", "22", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result"); + // Null answer should be replaced with empty string + assertEquals("", result.get("answer").asText()); + assertFalse(result.get("wasFreeform").asBoolean()); + } + + @Test + void userInputRequestWithNoHandler() throws Exception { + // Session exists but no user input handler registered + createSession("s1"); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("question", "What?"); + + invokeHandler("userInput.request", "23", params); + + JsonNode response = readResponse(); + // No handler → CopilotSession returns failedFuture → dispatcher's + // .exceptionally() fires + assertNotNull(response.get("error")); + assertEquals(-32603, response.get("error").get("code").asInt()); + assertTrue(response.get("error").get("message").asText().contains("User input handler error")); + } + + @Test + void userInputRequestHandlerFails() throws Exception { + CopilotSession session = createSession("s1"); + session.registerUserInputHandler( + (request, invocation) -> CompletableFuture.failedFuture(new RuntimeException("handler failed"))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("question", "What?"); + + invokeHandler("userInput.request", "24", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32603, response.get("error").get("code").asInt()); + } + + // ===== hooks.invoke tests ===== + + @Test + void hooksInvokeWithUnknownSession() throws Exception { + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "nonexistent"); + params.put("hookType", "preToolUse"); + params.putObject("input"); + + invokeHandler("hooks.invoke", "30", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32602, response.get("error").get("code").asInt()); + } + + @Test + void hooksInvokeWithNullOutput() throws Exception { + CopilotSession session = createSession("s1"); + // Register empty hooks — no specific handler for preToolUse → returns null + session.registerHooks(new SessionHooks()); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("hookType", "preToolUse"); + params.putObject("input"); + + invokeHandler("hooks.invoke", "31", params); + + JsonNode response = readResponse(); + // Null output triggers NPE in Map.of() → falls to .exceptionally() → error + // response + assertNotNull(response.get("error")); + assertEquals(-32603, response.get("error").get("code").asInt()); + } + + @Test + void hooksInvokeWithNonNullOutput() throws Exception { + CopilotSession session = createSession("s1"); + session.registerHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> CompletableFuture + .completedFuture(new PreToolUseHookOutput().setPermissionDecision("allow")))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("hookType", "preToolUse"); + ObjectNode input = params.putObject("input"); + input.put("toolName", "some_tool"); + input.put("toolCallId", "tc1"); + + invokeHandler("hooks.invoke", "32", params); + + JsonNode response = readResponse(); + JsonNode output = response.get("result").get("output"); + assertNotNull(output); + assertEquals("allow", output.get("permissionDecision").asText()); + } + + @Test + void hooksInvokeHandlerFails() throws Exception { + CopilotSession session = createSession("s1"); + session.registerHooks(new SessionHooks().setOnPreToolUse( + (input, invocation) -> CompletableFuture.failedFuture(new RuntimeException("hook error")))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("hookType", "preToolUse"); + ObjectNode input = params.putObject("input"); + input.put("toolName", "some_tool"); + input.put("toolCallId", "tc1"); + + invokeHandler("hooks.invoke", "33", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32603, response.get("error").get("code").asInt()); + assertTrue(response.get("error").get("message").asText().contains("Hooks handler error")); + } + + @Test + void hooksInvokeWithNoHooksRegistered() throws Exception { + // Session exists but no hooks registered at all → returns null output + createSession("s1"); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("hookType", "preToolUse"); + params.putObject("input"); + + invokeHandler("hooks.invoke", "34", params); + + JsonNode response = readResponse(); + // Null output triggers NPE in Map.of() → falls to .exceptionally() → error + // response + assertNotNull(response.get("error")); + assertEquals(-32603, response.get("error").get("code").asInt()); + } +} From d6a09cfa06b199c5df987bb21be4e71c3f3a1ed8 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 14:49:17 -0500 Subject: [PATCH 064/147] refactor: simplify response handling in RpcHandlerDispatcher and update tests for null output --- .../com/github/copilot/sdk/RpcHandlerDispatcher.java | 7 ++----- .../github/copilot/sdk/RpcHandlerDispatcherTest.java | 12 ++++-------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java index 9a0997cb1..21a4c4056 100644 --- a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java +++ b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.logging.Level; @@ -282,11 +283,7 @@ private void handleHooksInvoke(JsonRpcClient rpc, String requestId, JsonNode par session.handleHooksInvoke(hookType, input).thenAccept(output -> { try { - if (output != null) { - rpc.sendResponse(Long.parseLong(requestId), Map.of("output", output)); - } else { - rpc.sendResponse(Long.parseLong(requestId), Map.of("output", (Object) null)); - } + rpc.sendResponse(Long.parseLong(requestId), Collections.singletonMap("output", output)); } catch (IOException e) { LOG.log(Level.SEVERE, "Error sending hooks response", e); } diff --git a/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java b/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java index 883900892..a5ae8e443 100644 --- a/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java +++ b/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java @@ -461,10 +461,8 @@ void hooksInvokeWithNullOutput() throws Exception { invokeHandler("hooks.invoke", "31", params); JsonNode response = readResponse(); - // Null output triggers NPE in Map.of() → falls to .exceptionally() → error - // response - assertNotNull(response.get("error")); - assertEquals(-32603, response.get("error").get("code").asInt()); + JsonNode output = response.get("result").get("output"); + assertTrue(output == null || output.isNull(), "Output should be null when no hook handler is set"); } @Test @@ -522,9 +520,7 @@ void hooksInvokeWithNoHooksRegistered() throws Exception { invokeHandler("hooks.invoke", "34", params); JsonNode response = readResponse(); - // Null output triggers NPE in Map.of() → falls to .exceptionally() → error - // response - assertNotNull(response.get("error")); - assertEquals(-32603, response.get("error").get("code").asInt()); + JsonNode output = response.get("result").get("output"); + assertTrue(output == null || output.isNull(), "Output should be null when no hooks registered"); } } From 3f462f8f4da102b1a3fd027b0afd398dcccba22a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 19:52:39 +0000 Subject: [PATCH 065/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index a310734d0..56a1a015f 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -6,13 +6,13 @@ - + coverage coverage - 78.3% - 78.3% + 80.2% + 80.2% From aef332d8b01174929c03fb95cc23ad1bca13799a Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 14:55:28 -0500 Subject: [PATCH 066/147] test: add unit tests for CliServerManager to improve coverage and validate functionality --- .../copilot/sdk/CliServerManagerTest.java | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 src/test/java/com/github/copilot/sdk/CliServerManagerTest.java diff --git a/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java b/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java new file mode 100644 index 000000000..61ce938cf --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java @@ -0,0 +1,215 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.URI; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.CopilotClientOptions; + +/** + * Unit tests for {@link CliServerManager} covering parseCliUrl, + * connectToServer, resolveCliCommand, and ProcessInfo coverage gaps identified + * by JaCoCo. + */ +class CliServerManagerTest { + + // ===== parseCliUrl tests ===== + + @Test + void parseCliUrlWithPortNumber() { + URI uri = CliServerManager.parseCliUrl("8080"); + assertEquals("http://localhost:8080", uri.toString()); + } + + @Test + void parseCliUrlWithHostColonPort() { + URI uri = CliServerManager.parseCliUrl("myhost:9090"); + assertEquals("https://myhost:9090", uri.toString()); + } + + @Test + void parseCliUrlWithHttpPrefix() { + URI uri = CliServerManager.parseCliUrl("http://example.com:3000"); + assertEquals("http://example.com:3000", uri.toString()); + } + + @Test + void parseCliUrlWithHttpsPrefix() { + URI uri = CliServerManager.parseCliUrl("https://secure.host:443"); + assertEquals("https://secure.host:443", uri.toString()); + } + + @Test + void parseCliUrlWithHostOnly() { + URI uri = CliServerManager.parseCliUrl("copilot.example.com"); + assertEquals("https://copilot.example.com", uri.toString()); + } + + // ===== connectToServer tests ===== + + @Test + void connectToServerTcpMode() throws Exception { + var options = new CopilotClientOptions(); + var manager = new CliServerManager(options); + + // Start a temporary server socket to connect to + try (ServerSocket ss = new ServerSocket(0)) { + int port = ss.getLocalPort(); + JsonRpcClient client = manager.connectToServer(null, "localhost", port); + assertNotNull(client); + client.close(); + } + } + + @Test + void connectToServerStdioMode() throws Exception { + var options = new CopilotClientOptions(); + var manager = new CliServerManager(options); + + // Create a dummy process for stdio mode + Process process = new ProcessBuilder("cat").start(); + try { + JsonRpcClient client = manager.connectToServer(process, null, null); + assertNotNull(client); + client.close(); + } finally { + process.destroyForcibly(); + } + } + + @Test + void connectToServerNoProcessNoHost() { + var options = new CopilotClientOptions(); + var manager = new CliServerManager(options); + + var ex = assertThrows(IllegalStateException.class, () -> manager.connectToServer(null, null, null)); + assertTrue(ex.getMessage().contains("Cannot connect")); + } + + @Test + void connectToServerNullHostNonNullPort() { + var options = new CopilotClientOptions(); + var manager = new CliServerManager(options); + + // tcpHost is null but tcpPort is non-null → falls to process check → process + // null → exception + var ex = assertThrows(IllegalStateException.class, () -> manager.connectToServer(null, null, 8080)); + assertTrue(ex.getMessage().contains("Cannot connect")); + } + + // ===== ProcessInfo record tests ===== + + @Test + void processInfoRecord() { + var info = new CliServerManager.ProcessInfo(null, 12345); + assertNull(info.process()); + assertEquals(12345, info.port()); + } + + @Test + void processInfoWithNullPort() { + var info = new CliServerManager.ProcessInfo(null, null); + assertNull(info.process()); + assertNull(info.port()); + } + + // ===== resolveCliCommand tests (via startCliServer) ===== + // resolveCliCommand is private, so we test indirectly through startCliServer + // with specific cliPath values. + + @Test + void startCliServerWithJsFile() throws Exception { + // Using a .js file path causes resolveCliCommand to prepend "node" + // node is on PATH so the process starts, but the script doesn't exist + // so node exits quickly — verifying the .js branch was taken + var options = new CopilotClientOptions().setCliPath("/nonexistent/script.js").setUseStdio(true); + var manager = new CliServerManager(options); + + try { + var info = manager.startCliServer(); + // If process started, clean it up + info.process().destroyForcibly(); + } catch (IOException e) { + // Expected — node may fail or not be present; either way the branch is hit + assertNotNull(e); + } + } + + @Test + void startCliServerWithCliArgs() throws Exception { + // Test that cliArgs are included in the command + var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot") + .setCliArgs(new String[]{"--extra-flag"}).setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithExplicitPort() throws Exception { + // Test the explicit port branch (useStdio=false, port > 0) + var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setUseStdio(false).setPort(9999); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithGithubToken() throws Exception { + // Test the github token branch + var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setGithubToken("ghp_test123") + .setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithUseLoggedInUserExplicit() throws Exception { + // Test the explicit useLoggedInUser=false branch (adds --no-auto-login) + var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setUseLoggedInUser(false) + .setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithGithubTokenAndNoExplicitUseLoggedInUser() throws Exception { + // When githubToken is set and useLoggedInUser is null, defaults to false + var options = new CopilotClientOptions().setCliPath("/nonexistent/copilot").setGithubToken("ghp_test123") + .setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithNullCliPath() throws Exception { + // Test the null cliPath branch (defaults to "copilot") + var options = new CopilotClientOptions().setCliPath(null).setUseStdio(true); + var manager = new CliServerManager(options); + + // "copilot" likely doesn't exist in the test env — that's fine + try { + var info = manager.startCliServer(); + info.process().destroyForcibly(); + } catch (IOException e) { + // Expected if "copilot" is not on PATH + assertNotNull(e); + } + } +} From 26a2647c11027a4dcfe57c06b98b9e53dcf745e5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 19:58:59 +0000 Subject: [PATCH 067/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 56a1a015f..acfc8b3f8 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 80.2% - 80.2% + 81.1% + 81.1% From a9e193a9bcb97437bbd3c972fe23316c155e030c Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 15:07:58 -0500 Subject: [PATCH 068/147] test: add unit tests for LifecycleEventManager to cover event handling and error paths --- .../sdk/LifecycleEventManagerTest.java | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 src/test/java/com/github/copilot/sdk/LifecycleEventManagerTest.java diff --git a/src/test/java/com/github/copilot/sdk/LifecycleEventManagerTest.java b/src/test/java/com/github/copilot/sdk/LifecycleEventManagerTest.java new file mode 100644 index 000000000..1500f2794 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/LifecycleEventManagerTest.java @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.SessionLifecycleEvent; + +/** + * Unit tests for {@link LifecycleEventManager} covering subscribe, unsubscribe, + * dispatch, typed handlers, wildcard handlers, and error handling paths + * identified as gaps by JaCoCo. + */ +class LifecycleEventManagerTest { + + private LifecycleEventManager manager; + + @BeforeEach + void setup() { + manager = new LifecycleEventManager(); + } + + private static SessionLifecycleEvent event(String type) { + var e = new SessionLifecycleEvent(); + e.setType(type); + return e; + } + + // ===== wildcard subscribe / dispatch ===== + + @Test + void wildcardHandlerReceivesAllEvents() { + var received = new ArrayList(); + manager.subscribe(received::add); + + manager.dispatch(event("created")); + manager.dispatch(event("deleted")); + + assertEquals(2, received.size()); + assertEquals("created", received.get(0).getType()); + assertEquals("deleted", received.get(1).getType()); + } + + @Test + void wildcardUnsubscribeStopsDelivery() throws Exception { + var received = new ArrayList(); + AutoCloseable sub = manager.subscribe(received::add); + + manager.dispatch(event("created")); + assertEquals(1, received.size()); + + sub.close(); + + manager.dispatch(event("deleted")); + assertEquals(1, received.size(), "Should not receive events after unsubscribe"); + } + + // ===== typed subscribe / dispatch ===== + + @Test + void typedHandlerReceivesOnlyMatchingEvents() { + var received = new ArrayList(); + manager.subscribe("created", received::add); + + manager.dispatch(event("created")); + manager.dispatch(event("deleted")); + + assertEquals(1, received.size()); + assertEquals("created", received.get(0).getType()); + } + + @Test + void typedUnsubscribeStopsDelivery() throws Exception { + var received = new ArrayList(); + AutoCloseable sub = manager.subscribe("created", received::add); + + manager.dispatch(event("created")); + assertEquals(1, received.size()); + + sub.close(); + + manager.dispatch(event("created")); + assertEquals(1, received.size(), "Should not receive events after unsubscribe"); + } + + // ===== both typed + wildcard ===== + + @Test + void bothTypedAndWildcardReceiveEvent() { + var typedReceived = new ArrayList(); + var wildcardReceived = new ArrayList(); + + manager.subscribe("created", typedReceived::add); + manager.subscribe(wildcardReceived::add); + + manager.dispatch(event("created")); + + assertEquals(1, typedReceived.size()); + assertEquals(1, wildcardReceived.size()); + } + + // ===== dispatch with no handlers ===== + + @Test + void dispatchWithNoHandlersDoesNotThrow() { + assertDoesNotThrow(() -> manager.dispatch(event("created"))); + } + + @Test + void dispatchWithNoTypedMatchDoesNotThrow() { + var received = new ArrayList(); + manager.subscribe("deleted", received::add); + + assertDoesNotThrow(() -> manager.dispatch(event("created"))); + assertTrue(received.isEmpty()); + } + + // ===== error handling ===== + + @Test + void typedHandlerExceptionDoesNotPreventOtherHandlers() { + var received = new ArrayList(); + + // First handler throws + manager.subscribe("created", e -> { + throw new RuntimeException("typed handler error"); + }); + // Second handler should still receive the event + manager.subscribe("created", received::add); + + assertDoesNotThrow(() -> manager.dispatch(event("created"))); + assertEquals(1, received.size()); + } + + @Test + void wildcardHandlerExceptionDoesNotPreventOtherHandlers() { + var received = new ArrayList(); + + manager.subscribe(e -> { + throw new RuntimeException("wildcard handler error"); + }); + manager.subscribe(received::add); + + assertDoesNotThrow(() -> manager.dispatch(event("created"))); + assertEquals(1, received.size()); + } + + @Test + void typedAndWildcardErrorsDoNotAffectEachOther() { + var wildcardReceived = new ArrayList(); + + // Typed handler throws + manager.subscribe("created", e -> { + throw new RuntimeException("typed error"); + }); + // Wildcard still receives + manager.subscribe(wildcardReceived::add); + + assertDoesNotThrow(() -> manager.dispatch(event("created"))); + assertEquals(1, wildcardReceived.size()); + } + + // ===== multiple handlers ===== + + @Test + void multipleWildcardHandlersAllReceive() { + var list1 = new ArrayList(); + var list2 = new ArrayList(); + + manager.subscribe(list1::add); + manager.subscribe(list2::add); + + manager.dispatch(event("updated")); + + assertEquals(1, list1.size()); + assertEquals(1, list2.size()); + } + + @Test + void multipleTypedHandlersAllReceive() { + var list1 = new ArrayList(); + var list2 = new ArrayList(); + + manager.subscribe("updated", list1::add); + manager.subscribe("updated", list2::add); + + manager.dispatch(event("updated")); + + assertEquals(1, list1.size()); + assertEquals(1, list2.size()); + } +} From 4aad552476fbadce4581824498e3db2360af9dfe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:11:36 +0000 Subject: [PATCH 069/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index acfc8b3f8..ead4274b5 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 81.1% - 81.1% + 82.5% + 82.5% From 46b38cc8a7ea99f4c52ab87b7bb9bfabf239e63e Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 15:38:04 -0500 Subject: [PATCH 070/147] refactor: convert response classes to records and update method references for improved readability --- .../com/github/copilot/sdk/CopilotClient.java | 31 ++++--- .../github/copilot/sdk/CopilotSession.java | 7 +- .../sdk/json/CreateSessionResponse.java | 32 ++------ .../sdk/json/DeleteSessionResponse.java | 50 ++---------- .../json/GetForegroundSessionResponse.java | 38 ++------- .../sdk/json/GetLastSessionIdResponse.java | 11 +-- .../copilot/sdk/json/GetMessagesResponse.java | 13 +-- .../sdk/json/ListSessionsResponse.java | 26 +----- .../github/copilot/sdk/json/PingResponse.java | 80 +++---------------- .../sdk/json/ResumeSessionResponse.java | 32 ++------ .../copilot/sdk/json/SendMessageResponse.java | 26 +----- .../json/SessionLifecycleEventMetadata.java | 36 +-------- .../json/SetForegroundSessionResponse.java | 38 ++------- .../github/copilot/sdk/CopilotClientTest.java | 6 +- 14 files changed, 70 insertions(+), 356 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index ad2d76191..72305c046 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -179,15 +179,15 @@ private void verifyProtocolVersion(Connection connection) throws Exception { params.put("message", null); PingResponse pingResponse = connection.rpc.invoke("ping", params, PingResponse.class).get(30, TimeUnit.SECONDS); - if (pingResponse.getProtocolVersion() == null) { + if (pingResponse.protocolVersion() == null) { throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion + ", but server does not report a protocol version. " + "Please update your server to ensure compatibility."); } - if (pingResponse.getProtocolVersion() != expectedVersion) { + if (pingResponse.protocolVersion() != expectedVersion) { throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion - + ", but server reports version " + pingResponse.getProtocolVersion() + ". " + + ", but server reports version " + pingResponse.protocolVersion() + ". " + "Please update your SDK or server to ensure compatibility."); } } @@ -272,9 +272,9 @@ public CompletableFuture createSession(SessionConfig config) { var request = SessionRequestBuilder.buildCreateRequest(config); return connection.rpc.invoke("session.create", request, CreateSessionResponse.class).thenApply(response -> { - var session = new CopilotSession(response.getSessionId(), connection.rpc, response.getWorkspacePath()); + var session = new CopilotSession(response.sessionId(), connection.rpc, response.workspacePath()); SessionRequestBuilder.configureSession(session, config); - sessions.put(response.getSessionId(), session); + sessions.put(response.sessionId(), session); return session; }); }); @@ -310,9 +310,9 @@ public CompletableFuture resumeSession(String sessionId, ResumeS var request = SessionRequestBuilder.buildResumeRequest(sessionId, config); return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class).thenApply(response -> { - var session = new CopilotSession(response.getSessionId(), connection.rpc, response.getWorkspacePath()); + var session = new CopilotSession(response.sessionId(), connection.rpc, response.workspacePath()); SessionRequestBuilder.configureSession(session, config); - sessions.put(response.getSessionId(), session); + sessions.put(response.sessionId(), session); return session; }); }); @@ -432,7 +432,7 @@ public CompletableFuture> listModels() { public CompletableFuture getLastSessionId() { return ensureConnected().thenCompose( connection -> connection.rpc.invoke("session.getLastId", Map.of(), GetLastSessionIdResponse.class) - .thenApply(GetLastSessionIdResponse::getSessionId)); + .thenApply(GetLastSessionIdResponse::sessionId)); } /** @@ -450,9 +450,8 @@ public CompletableFuture deleteSession(String sessionId) { return ensureConnected().thenCompose(connection -> connection.rpc .invoke("session.delete", Map.of("sessionId", sessionId), DeleteSessionResponse.class) .thenAccept(response -> { - if (!response.isSuccess()) { - throw new RuntimeException( - "Failed to delete session " + sessionId + ": " + response.getError()); + if (!response.success()) { + throw new RuntimeException("Failed to delete session " + sessionId + ": " + response.error()); } sessions.remove(sessionId); })); @@ -471,7 +470,7 @@ public CompletableFuture deleteSession(String sessionId) { public CompletableFuture> listSessions() { return ensureConnected() .thenCompose(connection -> connection.rpc.invoke("session.list", Map.of(), ListSessionsResponse.class) - .thenApply(ListSessionsResponse::getSessions)); + .thenApply(ListSessionsResponse::sessions)); } /** @@ -487,7 +486,7 @@ public CompletableFuture getForegroundSessionId() { return ensureConnected().thenCompose(connection -> connection.rpc .invoke("session.getForeground", Map.of(), com.github.copilot.sdk.json.GetForegroundSessionResponse.class) - .thenApply(com.github.copilot.sdk.json.GetForegroundSessionResponse::getSessionId)); + .thenApply(com.github.copilot.sdk.json.GetForegroundSessionResponse::sessionId)); } /** @@ -509,9 +508,9 @@ public CompletableFuture setForegroundSessionId(String sessionId) { .invoke("session.setForeground", Map.of("sessionId", sessionId), com.github.copilot.sdk.json.SetForegroundSessionResponse.class) .thenAccept(response -> { - if (!response.isSuccess()) { - throw new RuntimeException(response.getError() != null - ? response.getError() + if (!response.success()) { + throw new RuntimeException(response.error() != null + ? response.error() : "Failed to set foreground session"); } })); diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index 78bf668ab..8235c0537 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -303,8 +303,7 @@ public CompletableFuture send(MessageOptions options) { request.setAttachments(options.getAttachments()); request.setMode(options.getMode()); - return rpc.invoke("session.send", request, SendMessageResponse.class) - .thenApply(SendMessageResponse::getMessageId); + return rpc.invoke("session.send", request, SendMessageResponse.class).thenApply(SendMessageResponse::messageId); } /** @@ -747,8 +746,8 @@ public CompletableFuture> getMessages() { return rpc.invoke("session.getMessages", Map.of("sessionId", sessionId), GetMessagesResponse.class) .thenApply(response -> { var events = new ArrayList(); - if (response.getEvents() != null) { - for (JsonNode eventNode : response.getEvents()) { + if (response.events() != null) { + for (JsonNode eventNode : response.events()) { try { AbstractSessionEvent event = SessionEventParser.parse(eventNode); if (event != null) { diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java index dd9f73a42..5b1a177f0 100644 --- a/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java @@ -6,32 +6,14 @@ /** * Internal response object from creating a session. * + * @param sessionId + * the session ID assigned by the server + * @param workspacePath + * the workspace path, or {@code null} if infinite sessions are + * disabled * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public final class CreateSessionResponse { - @JsonProperty("sessionId") - private String sessionId; - - @JsonProperty("workspacePath") - private String workspacePath; - - public String getSessionId() { - return sessionId; - } - public void setSessionId(String sessionId) { - this.sessionId = sessionId; - } - - /** - * Gets the workspace path when infinite sessions are enabled. - * - * @return the workspace path, or {@code null} if infinite sessions are disabled - */ - public String getWorkspacePath() { - return workspacePath; - } - public void setWorkspacePath(String workspacePath) { - this.workspacePath = workspacePath; - } +public record CreateSessionResponse(@JsonProperty("sessionId") String sessionId, + @JsonProperty("workspacePath") String workspacePath) { } diff --git a/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java b/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java index b997f4d43..1f53dfac3 100644 --- a/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java @@ -17,49 +17,9 @@ * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public final class DeleteSessionResponse { - - @JsonProperty("success") - private boolean success; - - @JsonProperty("error") - private String error; - - /** - * Returns whether the deletion was successful. - * - * @return {@code true} if the session was deleted successfully - */ - public boolean isSuccess() { - return success; - } - - /** - * Sets whether the deletion was successful. - * - * @param success - * {@code true} if successful - */ - public void setSuccess(boolean success) { - this.success = success; - } - - /** - * Gets the error message if the deletion failed. - * - * @return the error message, or {@code null} if successful - */ - public String getError() { - return error; - } - - /** - * Sets the error message. - * - * @param error - * the error message - */ - public void setError(String error) { - this.error = error; - } +public record DeleteSessionResponse( + /** Whether the deletion was successful. */ + @JsonProperty("success") boolean success, + /** The error message, or {@code null} if successful. */ + @JsonProperty("error") String error) { } diff --git a/src/main/java/com/github/copilot/sdk/json/GetForegroundSessionResponse.java b/src/main/java/com/github/copilot/sdk/json/GetForegroundSessionResponse.java index b259fdd28..96962c690 100644 --- a/src/main/java/com/github/copilot/sdk/json/GetForegroundSessionResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/GetForegroundSessionResponse.java @@ -16,37 +16,9 @@ * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) -public class GetForegroundSessionResponse { - - @JsonProperty("sessionId") - private String sessionId; - - @JsonProperty("workspacePath") - private String workspacePath; - - /** - * Gets the session ID currently displayed in the TUI. - * - * @return the session ID, or null if no foreground session - */ - public String getSessionId() { - return sessionId; - } - - public void setSessionId(String sessionId) { - this.sessionId = sessionId; - } - - /** - * Gets the workspace path of the foreground session. - * - * @return the workspace path, or null - */ - public String getWorkspacePath() { - return workspacePath; - } - - public void setWorkspacePath(String workspacePath) { - this.workspacePath = workspacePath; - } +public record GetForegroundSessionResponse( + /** The session ID currently displayed in the TUI, or null if none. */ + @JsonProperty("sessionId") String sessionId, + /** The workspace path of the foreground session, or null. */ + @JsonProperty("workspacePath") String workspacePath) { } diff --git a/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java b/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java index d4e391aea..52042f57c 100644 --- a/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java @@ -9,14 +9,5 @@ * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public final class GetLastSessionIdResponse { - @JsonProperty("sessionId") - private String sessionId; - - public String getSessionId() { - return sessionId; - } - public void setSessionId(String sessionId) { - this.sessionId = sessionId; - } +public record GetLastSessionIdResponse(@JsonProperty("sessionId") String sessionId) { } diff --git a/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java b/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java index 920fba9ff..1a3ed6aae 100644 --- a/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java @@ -12,16 +12,5 @@ * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public final class GetMessagesResponse { - - @JsonProperty("events") - private List events; - - public List getEvents() { - return events; - } - - public void setEvents(List events) { - this.events = events; - } +public record GetMessagesResponse(@JsonProperty("events") List events) { } diff --git a/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java b/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java index cd0ce44d8..b535ee396 100644 --- a/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java @@ -20,27 +20,7 @@ * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public final class ListSessionsResponse { - - @JsonProperty("sessions") - private List sessions; - - /** - * Gets the list of sessions. - * - * @return the list of session metadata - */ - public List getSessions() { - return sessions; - } - - /** - * Sets the list of sessions. - * - * @param sessions - * the list of session metadata - */ - public void setSessions(List sessions) { - this.sessions = sessions; - } +public record ListSessionsResponse( + /** The list of session metadata. */ + @JsonProperty("sessions") List sessions) { } diff --git a/src/main/java/com/github/copilot/sdk/json/PingResponse.java b/src/main/java/com/github/copilot/sdk/json/PingResponse.java index 65f5e87bc..e86499b2f 100644 --- a/src/main/java/com/github/copilot/sdk/json/PingResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/PingResponse.java @@ -17,74 +17,14 @@ * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public class PingResponse { - - @JsonProperty("message") - private String message; - - @JsonProperty("timestamp") - private long timestamp; - - @JsonProperty("protocolVersion") - private Integer protocolVersion; - - /** - * Gets the echo message from the server. - * - * @return the message echoed back by the server - */ - public String getMessage() { - return message; - } - - /** - * Sets the message. - * - * @param message - * the message - */ - public void setMessage(String message) { - this.message = message; - } - - /** - * Gets the server timestamp. - * - * @return the timestamp in milliseconds since epoch - */ - public long getTimestamp() { - return timestamp; - } - - /** - * Sets the timestamp. - * - * @param timestamp - * the timestamp - */ - public void setTimestamp(long timestamp) { - this.timestamp = timestamp; - } - - /** - * Gets the SDK protocol version supported by the server. - *

- * The SDK validates that this version matches the expected version to ensure - * compatibility. - * - * @return the protocol version, or {@code null} if not reported - */ - public Integer getProtocolVersion() { - return protocolVersion; - } - - /** - * Sets the protocol version. - * - * @param protocolVersion - * the protocol version - */ - public void setProtocolVersion(Integer protocolVersion) { - this.protocolVersion = protocolVersion; - } +public record PingResponse( + /** The echo message from the server. */ + @JsonProperty("message") String message, + /** The server timestamp in milliseconds since epoch. */ + @JsonProperty("timestamp") long timestamp, + /** + * The SDK protocol version supported by the server. The SDK validates that this + * version matches the expected version to ensure compatibility. + */ + @JsonProperty("protocolVersion") Integer protocolVersion) { } diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java index 883c4e0ee..654c1486c 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java @@ -6,32 +6,14 @@ /** * Internal response object from resuming a session. * + * @param sessionId + * the session ID + * @param workspacePath + * the workspace path, or {@code null} if infinite sessions are + * disabled * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public final class ResumeSessionResponse { - @JsonProperty("sessionId") - private String sessionId; - - @JsonProperty("workspacePath") - private String workspacePath; - - public String getSessionId() { - return sessionId; - } - public void setSessionId(String sessionId) { - this.sessionId = sessionId; - } - - /** - * Gets the workspace path when infinite sessions are enabled. - * - * @return the workspace path, or {@code null} if infinite sessions are disabled - */ - public String getWorkspacePath() { - return workspacePath; - } - public void setWorkspacePath(String workspacePath) { - this.workspacePath = workspacePath; - } +public record ResumeSessionResponse(@JsonProperty("sessionId") String sessionId, + @JsonProperty("workspacePath") String workspacePath) { } diff --git a/src/main/java/com/github/copilot/sdk/json/SendMessageResponse.java b/src/main/java/com/github/copilot/sdk/json/SendMessageResponse.java index c288be658..7d79a7a2b 100644 --- a/src/main/java/com/github/copilot/sdk/json/SendMessageResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/SendMessageResponse.java @@ -17,27 +17,7 @@ * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public final class SendMessageResponse { - - @JsonProperty("messageId") - private String messageId; - - /** - * Gets the message ID assigned by the server. - * - * @return the message ID - */ - public String getMessageId() { - return messageId; - } - - /** - * Sets the message ID. - * - * @param messageId - * the message ID - */ - public void setMessageId(String messageId) { - this.messageId = messageId; - } +public record SendMessageResponse( + /** The message ID assigned by the server. */ + @JsonProperty("messageId") String messageId) { } diff --git a/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEventMetadata.java b/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEventMetadata.java index 5764f7fe2..7c76c07aa 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEventMetadata.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEventMetadata.java @@ -13,38 +13,6 @@ * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) -public class SessionLifecycleEventMetadata { - - @JsonProperty("startTime") - private String startTime; - - @JsonProperty("modifiedTime") - private String modifiedTime; - - @JsonProperty("summary") - private String summary; - - public String getStartTime() { - return startTime; - } - - public void setStartTime(String startTime) { - this.startTime = startTime; - } - - public String getModifiedTime() { - return modifiedTime; - } - - public void setModifiedTime(String modifiedTime) { - this.modifiedTime = modifiedTime; - } - - public String getSummary() { - return summary; - } - - public void setSummary(String summary) { - this.summary = summary; - } +public record SessionLifecycleEventMetadata(@JsonProperty("startTime") String startTime, + @JsonProperty("modifiedTime") String modifiedTime, @JsonProperty("summary") String summary) { } diff --git a/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionResponse.java b/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionResponse.java index d3ce2279c..c4680c95d 100644 --- a/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionResponse.java +++ b/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionResponse.java @@ -16,37 +16,9 @@ * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) -public class SetForegroundSessionResponse { - - @JsonProperty("success") - private boolean success; - - @JsonProperty("error") - private String error; - - /** - * Whether the operation was successful. - * - * @return true if successful - */ - public boolean isSuccess() { - return success; - } - - public void setSuccess(boolean success) { - this.success = success; - } - - /** - * Gets the error message if the operation failed. - * - * @return the error message, or null if successful - */ - public String getError() { - return error; - } - - public void setError(String error) { - this.error = error; - } +public record SetForegroundSessionResponse( + /** Whether the operation was successful. */ + @JsonProperty("success") boolean success, + /** The error message, or null if successful. */ + @JsonProperty("error") String error) { } diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index 23e0d85ce..8887e6e12 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -135,8 +135,8 @@ void testStartAndConnectUsingStdio() throws Exception { assertEquals(ConnectionState.CONNECTED, client.getState()); PingResponse pong = client.ping("test message").get(); - assertEquals("pong: test message", pong.getMessage()); - assertTrue(pong.getTimestamp() >= 0); + assertEquals("pong: test message", pong.message()); + assertTrue(pong.timestamp() >= 0); client.stop().get(); assertEquals(ConnectionState.DISCONNECTED, client.getState()); @@ -155,7 +155,7 @@ void testStartAndConnectUsingTcp() throws Exception { assertEquals(ConnectionState.CONNECTED, client.getState()); PingResponse pong = client.ping("test message").get(); - assertEquals("pong: test message", pong.getMessage()); + assertEquals("pong: test message", pong.message()); client.stop().get(); } From fa91864f2b7cc9be960afe6f7c74f638a2d06fae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:42:11 +0000 Subject: [PATCH 071/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index ead4274b5..b18af7050 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 82.5% - 82.5% + 82.8% + 82.8% From e5b347898159b1b4e7283ac8ca658d5ca0cb02f8 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 16:34:04 -0500 Subject: [PATCH 072/147] test: add unit tests for JsonRpcClient to validate functionality and error handling --- .../github/copilot/sdk/JsonRpcClientTest.java | 452 ++++++++++++++++++ 1 file changed, 452 insertions(+) create mode 100644 src/test/java/com/github/copilot/sdk/JsonRpcClientTest.java diff --git a/src/test/java/com/github/copilot/sdk/JsonRpcClientTest.java b/src/test/java/com/github/copilot/sdk/JsonRpcClientTest.java new file mode 100644 index 000000000..8b5c1858c --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/JsonRpcClientTest.java @@ -0,0 +1,452 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +class JsonRpcClientTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + // ---- Helpers ---- + + private record SocketPair(JsonRpcClient client, Socket serverSide, + ServerSocket serverSocket) implements AutoCloseable { + + @Override + public void close() throws Exception { + client.close(); + serverSide.close(); + serverSocket.close(); + } + } + + private SocketPair createSocketPair() throws Exception { + var serverSocket = new ServerSocket(0); + var clientSocket = new Socket("localhost", serverSocket.getLocalPort()); + var serverSide = serverSocket.accept(); + var client = JsonRpcClient.fromSocket(clientSocket); + return new SocketPair(client, serverSide, serverSocket); + } + + /** Write a raw JSON-RPC message (with Content-Length header) to a stream. */ + private void writeRpcMessage(OutputStream out, String json) throws IOException { + byte[] content = json.getBytes(StandardCharsets.UTF_8); + String header = "Content-Length: " + content.length + "\r\n\r\n"; + out.write(header.getBytes(StandardCharsets.UTF_8)); + out.write(content); + out.flush(); + } + + /** Read a single JSON-RPC message (Content-Length framed) from a stream. */ + private String readRpcMessage(InputStream in) throws IOException { + var headerLine = new StringBuilder(); + int contentLength = -1; + boolean lastWasCR = false; + boolean inHeaders = true; + + while (inHeaders) { + int b = in.read(); + if (b == -1) + throw new IOException("EOF"); + if (b == '\r') { + lastWasCR = true; + } else if (b == '\n') { + String line = headerLine.toString(); + headerLine.setLength(0); + lastWasCR = false; + if (line.isEmpty()) { + inHeaders = false; + } else if (line.toLowerCase().startsWith("content-length:")) { + contentLength = Integer.parseInt(line.substring(15).trim()); + } + } else { + if (lastWasCR) { + headerLine.append('\r'); + lastWasCR = false; + } + headerLine.append((char) b); + } + } + + byte[] buffer = new byte[contentLength]; + int read = 0; + while (read < contentLength) { + int result = in.read(buffer, read, contentLength - read); + if (result == -1) + throw new IOException("EOF"); + read += result; + } + return new String(buffer, StandardCharsets.UTF_8); + } + + // ---- notify() ---- + + @Test + void testNotify() throws Exception { + try (var pair = createSocketPair()) { + pair.client.notify("test.method", Map.of("key", "value")); + + String msg = readRpcMessage(pair.serverSide.getInputStream()); + JsonNode node = MAPPER.readTree(msg); + assertEquals("2.0", node.get("jsonrpc").asText()); + assertEquals("test.method", node.get("method").asText()); + assertNull(node.get("id"), "Notification should not have an id"); + assertEquals("value", node.get("params").get("key").asText()); + } + } + + // ---- isConnected() ---- + + @Test + void testIsConnectedWithSocket() throws Exception { + try (var pair = createSocketPair()) { + assertTrue(pair.client.isConnected()); + } + } + + @Test + void testIsConnectedWithSocketClosed() throws Exception { + var pair = createSocketPair(); + pair.client.close(); + assertFalse(pair.client.isConnected()); + pair.serverSide.close(); + pair.serverSocket.close(); + } + + @Test + void testIsConnectedWithProcess() throws Exception { + Process proc = new ProcessBuilder("cat").start(); + try (var client = JsonRpcClient.fromProcess(proc)) { + assertTrue(client.isConnected()); + } + } + + @Test + void testIsConnectedWithProcessDead() throws Exception { + Process proc = new ProcessBuilder("cat").start(); + var client = JsonRpcClient.fromProcess(proc); + proc.destroy(); + proc.waitFor(5, TimeUnit.SECONDS); + assertFalse(client.isConnected()); + client.close(); + } + + // ---- getProcess() ---- + + @Test + void testGetProcessReturnsProcess() throws Exception { + Process proc = new ProcessBuilder("cat").start(); + try (var client = JsonRpcClient.fromProcess(proc)) { + assertSame(proc, client.getProcess()); + } + } + + @Test + void testGetProcessNullForSocket() throws Exception { + try (var pair = createSocketPair()) { + assertNull(pair.client.getProcess()); + } + } + + // ---- invoke() edge cases ---- + + @Test + void testInvokeWithVoidPrimitive() throws Exception { + try (var pair = createSocketPair()) { + CompletableFuture future = pair.client.invoke("test", Map.of(), void.class); + + String request = readRpcMessage(pair.serverSide.getInputStream()); + long id = MAPPER.readTree(request).get("id").asLong(); + + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":{\"any\":\"thing\"}}"); + + assertNull(future.get(5, TimeUnit.SECONDS)); + } + } + + @Test + void testInvokeWithSendFailure() throws Exception { + var serverSocket = new ServerSocket(0); + var clientSocket = new Socket("localhost", serverSocket.getLocalPort()); + var serverSide = serverSocket.accept(); + var client = JsonRpcClient.fromSocket(clientSocket); + + // Close the client socket so write will fail + clientSocket.close(); + Thread.sleep(100); + + CompletableFuture future = client.invoke("test", Map.of(), JsonNode.class); + + var ex = assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS)); + assertInstanceOf(IOException.class, ex.getCause()); + client.close(); + serverSide.close(); + serverSocket.close(); + } + + @Test + void testInvokeWithDeserializationError() throws Exception { + try (var pair = createSocketPair()) { + // Integer cannot be deserialized from a JSON object + CompletableFuture future = pair.client.invoke("test", Map.of(), Integer.class); + + String request = readRpcMessage(pair.serverSide.getInputStream()); + long id = MAPPER.readTree(request).get("id").asLong(); + + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":{\"complex\":\"object\"}}"); + + var ex = assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS)); + // CompletableFuture unwraps CompletionException, so cause is the + // underlying JsonProcessingException + assertInstanceOf(com.fasterxml.jackson.core.JsonProcessingException.class, ex.getCause()); + } + } + + // ---- handleMessage: response handling ---- + + @Test + void testResponseWithUnknownId() throws Exception { + try (var pair = createSocketPair()) { + // Response for an id that has no pending request - silently ignored + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":99999,\"result\":{\"ok\":true}}"); + + Thread.sleep(200); + // No exception, just silently dropped + } + } + + @Test + void testErrorResponseWithoutMessage() throws Exception { + try (var pair = createSocketPair()) { + CompletableFuture future = pair.client.invoke("test", Map.of(), JsonNode.class); + + String request = readRpcMessage(pair.serverSide.getInputStream()); + long id = MAPPER.readTree(request).get("id").asLong(); + + // Error with code but no message field + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"error\":{\"code\":-32600}}"); + + var ex = assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS)); + var rpcEx = assertInstanceOf(JsonRpcException.class, ex.getCause()); + assertEquals("Unknown error", rpcEx.getMessage()); + assertEquals(-32600, rpcEx.getCode()); + } + } + + @Test + void testErrorResponseWithoutCode() throws Exception { + try (var pair = createSocketPair()) { + CompletableFuture future = pair.client.invoke("test", Map.of(), JsonNode.class); + + String request = readRpcMessage(pair.serverSide.getInputStream()); + long id = MAPPER.readTree(request).get("id").asLong(); + + // Error with message but no code field + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"error\":{\"message\":\"bad request\"}}"); + + var ex = assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS)); + var rpcEx = assertInstanceOf(JsonRpcException.class, ex.getCause()); + assertEquals("bad request", rpcEx.getMessage()); + assertEquals(-1, rpcEx.getCode()); + } + } + + // ---- handleMessage: server method calls ---- + + @Test + void testNoHandlerForNotification() throws Exception { + try (var pair = createSocketPair()) { + // Notification (no id) for unregistered method -- silently logged + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"method\":\"unknown.method\",\"params\":{}}"); + + Thread.sleep(200); + } + } + + @Test + void testNoHandlerForRequestSendsErrorResponse() throws Exception { + try (var pair = createSocketPair()) { + // Request (with id) for unregistered method -> -32601 Method not found + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":42,\"method\":\"unknown.method\",\"params\":{}}"); + + String response = readRpcMessage(pair.serverSide.getInputStream()); + JsonNode node = MAPPER.readTree(response); + assertEquals("2.0", node.get("jsonrpc").asText()); + assertTrue(node.has("error")); + assertEquals(-32601, node.get("error").get("code").asInt()); + assertTrue(node.get("error").get("message").asText().contains("Method not found")); + } + } + + @Test + void testHandlerThrowsExceptionWithId() throws Exception { + try (var pair = createSocketPair()) { + pair.client.registerMethodHandler("fail.method", (id, params) -> { + throw new RuntimeException("handler error"); + }); + + // Request with id - handler throws -> -32603 Internal error + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"fail.method\",\"params\":{}}"); + + String response = readRpcMessage(pair.serverSide.getInputStream()); + JsonNode node = MAPPER.readTree(response); + assertTrue(node.has("error")); + assertEquals(-32603, node.get("error").get("code").asInt()); + assertEquals("handler error", node.get("error").get("message").asText()); + } + } + + @Test + void testHandlerThrowsExceptionWithoutId() throws Exception { + try (var pair = createSocketPair()) { + pair.client.registerMethodHandler("fail.notify", (id, params) -> { + throw new RuntimeException("notify error"); + }); + + // Notification (no id) - handler throws -> just logged, no error response + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"method\":\"fail.notify\",\"params\":{}}"); + + Thread.sleep(200); + // Should not crash + } + } + + @Test + void testMethodCallWithNullId() throws Exception { + try (var pair = createSocketPair()) { + var received = new AtomicReference(); + pair.client.registerMethodHandler("test.null.id", (id, params) -> { + received.set(id); + }); + + // Explicit null id - should be treated as notification + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":null,\"method\":\"test.null.id\",\"params\":{}}"); + + Thread.sleep(200); + assertNull(received.get()); + } + } + + // ---- handleMessage: edge cases ---- + + @Test + void testInvalidJson() throws Exception { + try (var pair = createSocketPair()) { + writeRpcMessage(pair.serverSide.getOutputStream(), "not valid json {{{"); + + Thread.sleep(200); + // Should not crash, error is logged + } + } + + @Test + void testMessageWithNeitherResponseNorMethod() throws Exception { + try (var pair = createSocketPair()) { + // JSON object with id but no result/error/method - silently ignored + writeRpcMessage(pair.serverSide.getOutputStream(), "{\"jsonrpc\":\"2.0\",\"id\":1}"); + + Thread.sleep(200); + } + } + + // ---- reader: header parsing edge cases ---- + + @Test + void testReaderWithUnknownHeader() throws Exception { + try (var pair = createSocketPair()) { + var received = new CompletableFuture(); + pair.client.registerMethodHandler("test.header", (id, params) -> { + received.complete(params); + }); + + // Send a message with an extra header before Content-Length + var out = pair.serverSide.getOutputStream(); + String json = "{\"jsonrpc\":\"2.0\",\"method\":\"test.header\",\"params\":{\"ok\":true}}"; + byte[] content = json.getBytes(StandardCharsets.UTF_8); + String msg = "X-Custom-Header: value\r\nContent-Length: " + content.length + "\r\n\r\n"; + out.write(msg.getBytes(StandardCharsets.UTF_8)); + out.write(content); + out.flush(); + + JsonNode params = received.get(5, TimeUnit.SECONDS); + assertTrue(params.get("ok").asBoolean()); + } + } + + @Test + void testReaderWithMissingContentLength() throws Exception { + try (var pair = createSocketPair()) { + var received = new CompletableFuture(); + pair.client.registerMethodHandler("test.after", (id, params) -> { + received.complete(params); + }); + + var out = pair.serverSide.getOutputStream(); + + // First: send a message with no Content-Length header (just blank line) - + // should skip + out.write("X-Only-Header: no-length\r\n\r\n".getBytes(StandardCharsets.UTF_8)); + out.flush(); + + // Then: send a proper message that should be received + String json = "{\"jsonrpc\":\"2.0\",\"method\":\"test.after\",\"params\":{\"ok\":true}}"; + byte[] content = json.getBytes(StandardCharsets.UTF_8); + String proper = "Content-Length: " + content.length + "\r\n\r\n"; + out.write(proper.getBytes(StandardCharsets.UTF_8)); + out.write(content); + out.flush(); + + JsonNode params = received.get(5, TimeUnit.SECONDS); + assertTrue(params.get("ok").asBoolean()); + } + } + + // ---- close() ---- + + @Test + void testCloseWithPendingRequests() throws Exception { + var pair = createSocketPair(); + CompletableFuture future = pair.client.invoke("test", Map.of(), JsonNode.class); + // Read the outgoing request to avoid blocking + readRpcMessage(pair.serverSide.getInputStream()); + + // Close without responding - should cancel pending request + pair.client.close(); + + var ex = assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS)); + assertInstanceOf(IOException.class, ex.getCause()); + + pair.serverSide.close(); + pair.serverSocket.close(); + } +} From 5db1d092626117e1bf7c0b268e5e112b0e7902fd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:37:30 +0000 Subject: [PATCH 073/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index b18af7050..2b4ee6ff1 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 82.8% - 82.8% + 84.2% + 84.2% From b1afb3f7505bfeadc6071876562385f250556d22 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 20:08:03 -0500 Subject: [PATCH 074/147] refactor: convert simple POJO classes to records for improved readability and simplify method usage --- .../github/copilot/sdk/json/Attachment.java | 96 ++----------- .../copilot/sdk/json/MessageOptions.java | 2 +- .../sdk/json/PostToolUseHookOutput.java | 82 ++--------- .../copilot/sdk/json/SessionEndHandler.java | 5 +- .../copilot/sdk/json/SessionEndHookInput.java | 136 ++---------------- .../sdk/json/SessionEndHookOutput.java | 83 ++--------- .../github/copilot/sdk/json/SessionHooks.java | 6 +- .../copilot/sdk/json/SessionStartHandler.java | 5 +- .../sdk/json/SessionStartHookInput.java | 107 ++------------ .../sdk/json/SessionStartHookOutput.java | 56 +------- .../copilot/sdk/json/ToolBinaryResult.java | 116 ++------------- .../sdk/json/UserPromptSubmittedHandler.java | 6 +- .../json/UserPromptSubmittedHookInput.java | 82 ++--------- .../json/UserPromptSubmittedHookOutput.java | 83 ++--------- src/site/markdown/advanced.md | 5 +- src/site/markdown/hooks.md | 17 ++- 16 files changed, 111 insertions(+), 776 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/json/Attachment.java b/src/main/java/com/github/copilot/sdk/json/Attachment.java index 1b0c70076..bf6610804 100644 --- a/src/main/java/com/github/copilot/sdk/json/Attachment.java +++ b/src/main/java/com/github/copilot/sdk/json/Attachment.java @@ -11,100 +11,24 @@ * Represents a file attachment to include with a message. *

* Attachments provide additional context to the AI assistant, such as source - * code files, documents, or other relevant content. All setter methods return - * {@code this} for method chaining. + * code files, documents, or other relevant content. * *

Example Usage

* *
{@code
- * var attachment = new Attachment().setType("file").setPath("/path/to/source.java").setDisplayName("Main Source File");
+ * var attachment = new Attachment("file", "/path/to/source.java", "Main Source File");
  * }
* + * @param type + * the attachment type (e.g., "file") + * @param path + * the absolute path to the file on the filesystem + * @param displayName + * a human-readable display name for the attachment * @see MessageOptions#setAttachments(java.util.List) * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public class Attachment { - - @JsonProperty("type") - private String type; - - @JsonProperty("path") - private String path; - - @JsonProperty("displayName") - private String displayName; - - /** - * Gets the attachment type. - * - * @return the type (e.g., "file") - */ - public String getType() { - return type; - } - - /** - * Sets the attachment type. - *

- * Currently supported types: - *

    - *
  • "file" - A file from the filesystem
  • - *
- * - * @param type - * the attachment type - * @return this attachment for method chaining - */ - public Attachment setType(String type) { - this.type = type; - return this; - } - - /** - * Gets the file path. - * - * @return the absolute path to the file - */ - public String getPath() { - return path; - } - - /** - * Sets the file path. - *

- * This should be an absolute path to the file on the filesystem. - * - * @param path - * the absolute file path - * @return this attachment for method chaining - */ - public Attachment setPath(String path) { - this.path = path; - return this; - } - - /** - * Gets the display name. - * - * @return the display name for the attachment - */ - public String getDisplayName() { - return displayName; - } - - /** - * Sets a human-readable display name for the attachment. - *

- * This name is shown to the assistant and may be used when referring to the - * file in responses. - * - * @param displayName - * the display name - * @return this attachment for method chaining - */ - public Attachment setDisplayName(String displayName) { - this.displayName = displayName; - return this; - } +public record Attachment(@JsonProperty("type") String type, @JsonProperty("path") String path, + @JsonProperty("displayName") String displayName) { } diff --git a/src/main/java/com/github/copilot/sdk/json/MessageOptions.java b/src/main/java/com/github/copilot/sdk/json/MessageOptions.java index 5155ed7c8..99c4214b8 100644 --- a/src/main/java/com/github/copilot/sdk/json/MessageOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/MessageOptions.java @@ -19,7 +19,7 @@ * *

{@code
  * var options = new MessageOptions().setPrompt("Explain this code")
- * 		.setAttachments(List.of(new Attachment().setType("file").setPath("/path/to/file.java")));
+ * 		.setAttachments(List.of(new Attachment("file", "/path/to/file.java", null)));
  *
  * session.send(options).get();
  * }
diff --git a/src/main/java/com/github/copilot/sdk/json/PostToolUseHookOutput.java b/src/main/java/com/github/copilot/sdk/json/PostToolUseHookOutput.java index d8e30dd40..a532bf15e 100644 --- a/src/main/java/com/github/copilot/sdk/json/PostToolUseHookOutput.java +++ b/src/main/java/com/github/copilot/sdk/json/PostToolUseHookOutput.java @@ -11,80 +11,16 @@ /** * Output for a post-tool-use hook. * + * @param modifiedResult + * the modified tool result, or {@code null} to use original + * @param additionalContext + * additional context to provide to the model + * @param suppressOutput + * {@code true} to suppress output * @since 1.0.6 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public class PostToolUseHookOutput { - - @JsonProperty("modifiedResult") - private JsonNode modifiedResult; - - @JsonProperty("additionalContext") - private String additionalContext; - - @JsonProperty("suppressOutput") - private Boolean suppressOutput; - - /** - * Gets the modified tool result. - * - * @return the modified result, or {@code null} to use original - */ - public JsonNode getModifiedResult() { - return modifiedResult; - } - - /** - * Sets the modified tool result. - * - * @param modifiedResult - * the modified result - * @return this instance for method chaining - */ - public PostToolUseHookOutput setModifiedResult(JsonNode modifiedResult) { - this.modifiedResult = modifiedResult; - return this; - } - - /** - * Gets additional context to provide to the model. - * - * @return the additional context - */ - public String getAdditionalContext() { - return additionalContext; - } - - /** - * Sets additional context to provide to the model. - * - * @param additionalContext - * the additional context - * @return this instance for method chaining - */ - public PostToolUseHookOutput setAdditionalContext(String additionalContext) { - this.additionalContext = additionalContext; - return this; - } - - /** - * Returns whether to suppress output. - * - * @return {@code true} to suppress output - */ - public Boolean getSuppressOutput() { - return suppressOutput; - } - - /** - * Sets whether to suppress output. - * - * @param suppressOutput - * {@code true} to suppress output - * @return this instance for method chaining - */ - public PostToolUseHookOutput setSuppressOutput(Boolean suppressOutput) { - this.suppressOutput = suppressOutput; - return this; - } +public record PostToolUseHookOutput(@JsonProperty("modifiedResult") JsonNode modifiedResult, + @JsonProperty("additionalContext") String additionalContext, + @JsonProperty("suppressOutput") Boolean suppressOutput) { } diff --git a/src/main/java/com/github/copilot/sdk/json/SessionEndHandler.java b/src/main/java/com/github/copilot/sdk/json/SessionEndHandler.java index fb3c779b4..e8fc908df 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionEndHandler.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionEndHandler.java @@ -16,9 +16,8 @@ * *
{@code
  * SessionEndHandler handler = (input, invocation) -> {
- * 	System.out.println("Session ended: " + input.getReason());
- * 	return CompletableFuture
- * 			.completedFuture(new SessionEndHookOutput().setSessionSummary("Session completed successfully"));
+ * 	System.out.println("Session ended: " + input.reason());
+ * 	return CompletableFuture.completedFuture(new SessionEndHookOutput(null, null, "Session completed successfully"));
  * };
  * }
* diff --git a/src/main/java/com/github/copilot/sdk/json/SessionEndHookInput.java b/src/main/java/com/github/copilot/sdk/json/SessionEndHookInput.java index 94249af93..79f63fbb9 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionEndHookInput.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionEndHookInput.java @@ -13,129 +13,21 @@ * This hook is invoked when a session ends, allowing you to perform cleanup or * logging. * + * @param timestamp + * the timestamp in milliseconds since epoch when the session ended + * @param cwd + * the current working directory + * @param reason + * the reason: "complete", "error", "abort", "timeout", or + * "user_exit" + * @param finalMessage + * the final message, or {@code null} + * @param error + * the error message, or {@code null} * @since 1.0.7 */ @JsonIgnoreProperties(ignoreUnknown = true) -public class SessionEndHookInput { - - @JsonProperty("timestamp") - private long timestamp; - - @JsonProperty("cwd") - private String cwd; - - @JsonProperty("reason") - private String reason; - - @JsonProperty("finalMessage") - private String finalMessage; - - @JsonProperty("error") - private String error; - - /** - * Gets the timestamp when the session ended. - * - * @return the timestamp in milliseconds since epoch - */ - public long getTimestamp() { - return timestamp; - } - - /** - * Sets the timestamp when the session ended. - * - * @param timestamp - * the timestamp in milliseconds since epoch - * @return this instance for method chaining - */ - public SessionEndHookInput setTimestamp(long timestamp) { - this.timestamp = timestamp; - return this; - } - - /** - * Gets the current working directory. - * - * @return the current working directory - */ - public String getCwd() { - return cwd; - } - - /** - * Sets the current working directory. - * - * @param cwd - * the current working directory - * @return this instance for method chaining - */ - public SessionEndHookInput setCwd(String cwd) { - this.cwd = cwd; - return this; - } - - /** - * Gets the reason for session end. - * - * @return the reason: "complete", "error", "abort", "timeout", or "user_exit" - */ - public String getReason() { - return reason; - } - - /** - * Sets the reason for session end. - * - * @param reason - * the reason: "complete", "error", "abort", "timeout", or - * "user_exit" - * @return this instance for method chaining - */ - public SessionEndHookInput setReason(String reason) { - this.reason = reason; - return this; - } - - /** - * Gets the final message, if any. - * - * @return the final message, or {@code null} - */ - public String getFinalMessage() { - return finalMessage; - } - - /** - * Sets the final message. - * - * @param finalMessage - * the final message - * @return this instance for method chaining - */ - public SessionEndHookInput setFinalMessage(String finalMessage) { - this.finalMessage = finalMessage; - return this; - } - - /** - * Gets the error message, if the session ended due to an error. - * - * @return the error message, or {@code null} - */ - public String getError() { - return error; - } - - /** - * Sets the error message. - * - * @param error - * the error message - * @return this instance for method chaining - */ - public SessionEndHookInput setError(String error) { - this.error = error; - return this; - } +public record SessionEndHookInput(@JsonProperty("timestamp") long timestamp, @JsonProperty("cwd") String cwd, + @JsonProperty("reason") String reason, @JsonProperty("finalMessage") String finalMessage, + @JsonProperty("error") String error) { } diff --git a/src/main/java/com/github/copilot/sdk/json/SessionEndHookOutput.java b/src/main/java/com/github/copilot/sdk/json/SessionEndHookOutput.java index 99d14e5d2..23ebf958e 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionEndHookOutput.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionEndHookOutput.java @@ -4,7 +4,6 @@ package com.github.copilot.sdk.json; -import java.util.Collections; import java.util.List; import com.fasterxml.jackson.annotation.JsonInclude; @@ -15,80 +14,16 @@ *

* Allows specifying cleanup actions and session summary. * + * @param suppressOutput + * {@code true} to suppress output, or {@code null} + * @param cleanupActions + * the cleanup actions to perform, or {@code null} + * @param sessionSummary + * the session summary, or {@code null} * @since 1.0.7 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public class SessionEndHookOutput { - - @JsonProperty("suppressOutput") - private Boolean suppressOutput; - - @JsonProperty("cleanupActions") - private List cleanupActions; - - @JsonProperty("sessionSummary") - private String sessionSummary; - - /** - * Gets whether output should be suppressed. - * - * @return {@code true} to suppress output, or {@code null} - */ - public Boolean getSuppressOutput() { - return suppressOutput; - } - - /** - * Sets whether to suppress the output. - * - * @param suppressOutput - * {@code true} to suppress output - * @return this instance for method chaining - */ - public SessionEndHookOutput setSuppressOutput(Boolean suppressOutput) { - this.suppressOutput = suppressOutput; - return this; - } - - /** - * Gets the list of cleanup actions. - * - * @return the cleanup actions, or {@code null} - */ - public List getCleanupActions() { - return cleanupActions == null ? null : Collections.unmodifiableList(cleanupActions); - } - - /** - * Sets the cleanup actions to perform. - * - * @param cleanupActions - * the cleanup actions - * @return this instance for method chaining - */ - public SessionEndHookOutput setCleanupActions(List cleanupActions) { - this.cleanupActions = cleanupActions; - return this; - } - - /** - * Gets the session summary. - * - * @return the session summary, or {@code null} - */ - public String getSessionSummary() { - return sessionSummary; - } - - /** - * Sets a summary of the session. - * - * @param sessionSummary - * the session summary - * @return this instance for method chaining - */ - public SessionEndHookOutput setSessionSummary(String sessionSummary) { - this.sessionSummary = sessionSummary; - return this; - } +public record SessionEndHookOutput(@JsonProperty("suppressOutput") Boolean suppressOutput, + @JsonProperty("cleanupActions") List cleanupActions, + @JsonProperty("sessionSummary") String sessionSummary) { } diff --git a/src/main/java/com/github/copilot/sdk/json/SessionHooks.java b/src/main/java/com/github/copilot/sdk/json/SessionHooks.java index 3bd9fa89b..b44a91b76 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionHooks.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionHooks.java @@ -20,13 +20,13 @@ * System.out.println("Tool result: " + input.getToolResult()); * return CompletableFuture.completedFuture(null); * }).setOnUserPromptSubmitted((input, invocation) -> { - * System.out.println("User prompt: " + input.getPrompt()); + * System.out.println("User prompt: " + input.prompt()); * return CompletableFuture.completedFuture(null); * }).setOnSessionStart((input, invocation) -> { - * System.out.println("Session started: " + input.getSource()); + * System.out.println("Session started: " + input.source()); * return CompletableFuture.completedFuture(null); * }).setOnSessionEnd((input, invocation) -> { - * System.out.println("Session ended: " + input.getReason()); + * System.out.println("Session ended: " + input.reason()); * return CompletableFuture.completedFuture(null); * }); * diff --git a/src/main/java/com/github/copilot/sdk/json/SessionStartHandler.java b/src/main/java/com/github/copilot/sdk/json/SessionStartHandler.java index be44421a4..fd631cb7f 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionStartHandler.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionStartHandler.java @@ -16,9 +16,8 @@ * *

{@code
  * SessionStartHandler handler = (input, invocation) -> {
- * 	System.out.println("Session started from: " + input.getSource());
- * 	return CompletableFuture
- * 			.completedFuture(new SessionStartHookOutput().setAdditionalContext("Custom initialization context"));
+ * 	System.out.println("Session started from: " + input.source());
+ * 	return CompletableFuture.completedFuture(new SessionStartHookOutput("Custom initialization context", null));
  * };
  * }
* diff --git a/src/main/java/com/github/copilot/sdk/json/SessionStartHookInput.java b/src/main/java/com/github/copilot/sdk/json/SessionStartHookInput.java index 8ad92c8d8..204717557 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionStartHookInput.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionStartHookInput.java @@ -13,104 +13,17 @@ * This hook is invoked when a session starts, allowing you to perform * initialization or modify the session configuration. * + * @param timestamp + * the timestamp in milliseconds since epoch when the session started + * @param cwd + * the current working directory + * @param source + * the source: "startup", "resume", or "new" + * @param initialPrompt + * the initial prompt, or {@code null} * @since 1.0.7 */ @JsonIgnoreProperties(ignoreUnknown = true) -public class SessionStartHookInput { - - @JsonProperty("timestamp") - private long timestamp; - - @JsonProperty("cwd") - private String cwd; - - @JsonProperty("source") - private String source; - - @JsonProperty("initialPrompt") - private String initialPrompt; - - /** - * Gets the timestamp when the session started. - * - * @return the timestamp in milliseconds since epoch - */ - public long getTimestamp() { - return timestamp; - } - - /** - * Sets the timestamp when the session started. - * - * @param timestamp - * the timestamp in milliseconds since epoch - * @return this instance for method chaining - */ - public SessionStartHookInput setTimestamp(long timestamp) { - this.timestamp = timestamp; - return this; - } - - /** - * Gets the current working directory. - * - * @return the current working directory - */ - public String getCwd() { - return cwd; - } - - /** - * Sets the current working directory. - * - * @param cwd - * the current working directory - * @return this instance for method chaining - */ - public SessionStartHookInput setCwd(String cwd) { - this.cwd = cwd; - return this; - } - - /** - * Gets the source of the session start. - * - * @return the source: "startup", "resume", or "new" - */ - public String getSource() { - return source; - } - - /** - * Sets the source of the session start. - * - * @param source - * the source: "startup", "resume", or "new" - * @return this instance for method chaining - */ - public SessionStartHookInput setSource(String source) { - this.source = source; - return this; - } - - /** - * Gets the initial prompt, if any. - * - * @return the initial prompt, or {@code null} - */ - public String getInitialPrompt() { - return initialPrompt; - } - - /** - * Sets the initial prompt. - * - * @param initialPrompt - * the initial prompt - * @return this instance for method chaining - */ - public SessionStartHookInput setInitialPrompt(String initialPrompt) { - this.initialPrompt = initialPrompt; - return this; - } +public record SessionStartHookInput(@JsonProperty("timestamp") long timestamp, @JsonProperty("cwd") String cwd, + @JsonProperty("source") String source, @JsonProperty("initialPrompt") String initialPrompt) { } diff --git a/src/main/java/com/github/copilot/sdk/json/SessionStartHookOutput.java b/src/main/java/com/github/copilot/sdk/json/SessionStartHookOutput.java index 729660fdd..3ef5971c4 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionStartHookOutput.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionStartHookOutput.java @@ -4,7 +4,6 @@ package com.github.copilot.sdk.json; -import java.util.Collections; import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; @@ -15,56 +14,13 @@ *

* Allows adding additional context or modifying session configuration. * + * @param additionalContext + * additional context to be added to the session, or {@code null} + * @param modifiedConfig + * modified configuration options for the session, or {@code null} * @since 1.0.7 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public class SessionStartHookOutput { - - @JsonProperty("additionalContext") - private String additionalContext; - - @JsonProperty("modifiedConfig") - private Map modifiedConfig; - - /** - * Gets the additional context to add. - * - * @return the additional context, or {@code null} - */ - public String getAdditionalContext() { - return additionalContext; - } - - /** - * Sets additional context to be added to the session. - * - * @param additionalContext - * the additional context - * @return this instance for method chaining - */ - public SessionStartHookOutput setAdditionalContext(String additionalContext) { - this.additionalContext = additionalContext; - return this; - } - - /** - * Gets the modified configuration. - * - * @return the modified configuration map, or {@code null} - */ - public Map getModifiedConfig() { - return modifiedConfig == null ? null : Collections.unmodifiableMap(modifiedConfig); - } - - /** - * Sets modified configuration options for the session. - * - * @param modifiedConfig - * the modified configuration - * @return this instance for method chaining - */ - public SessionStartHookOutput setModifiedConfig(Map modifiedConfig) { - this.modifiedConfig = modifiedConfig; - return this; - } +public record SessionStartHookOutput(@JsonProperty("additionalContext") String additionalContext, + @JsonProperty("modifiedConfig") Map modifiedConfig) { } diff --git a/src/main/java/com/github/copilot/sdk/json/ToolBinaryResult.java b/src/main/java/com/github/copilot/sdk/json/ToolBinaryResult.java index c63899a8d..e00fce9cf 100644 --- a/src/main/java/com/github/copilot/sdk/json/ToolBinaryResult.java +++ b/src/main/java/com/github/copilot/sdk/json/ToolBinaryResult.java @@ -10,117 +10,29 @@ /** * Binary result from a tool execution. *

- * This class represents binary data (such as images) returned by a tool. The + * This record represents binary data (such as images) returned by a tool. The * data is base64-encoded for JSON transmission. * *

Example Usage

* *
{@code
- * var binaryResult = new ToolBinaryResult().setType("image").setMimeType("image/png")
- * 		.setData(Base64.getEncoder().encodeToString(imageBytes)).setDescription("Generated chart");
+ * var binaryResult = new ToolBinaryResult(Base64.getEncoder().encodeToString(imageBytes), "image/png", "image",
+ * 		"Generated chart");
  * }
* + * @param data + * the base64-encoded binary data + * @param mimeType + * the MIME type (e.g., "image/png", "application/pdf") + * @param type + * the content type (e.g., "image", "file") + * @param description + * the content description, helps the assistant understand the + * content * @see ToolResultObject#setBinaryResultsForLlm(java.util.List) * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public final class ToolBinaryResult { - - @JsonProperty("data") - private String data; - - @JsonProperty("mimeType") - private String mimeType; - - @JsonProperty("type") - private String type; - - @JsonProperty("description") - private String description; - - /** - * Gets the base64-encoded binary data. - * - * @return the base64-encoded data string - */ - public String getData() { - return data; - } - - /** - * Sets the base64-encoded binary data. - * - * @param data - * the base64-encoded data - * @return this result for method chaining - */ - public ToolBinaryResult setData(String data) { - this.data = data; - return this; - } - - /** - * Gets the MIME type of the binary data. - * - * @return the MIME type (e.g., "image/png", "application/pdf") - */ - public String getMimeType() { - return mimeType; - } - - /** - * Sets the MIME type of the binary data. - * - * @param mimeType - * the MIME type - * @return this result for method chaining - */ - public ToolBinaryResult setMimeType(String mimeType) { - this.mimeType = mimeType; - return this; - } - - /** - * Gets the type of binary content. - * - * @return the content type (e.g., "image", "file") - */ - public String getType() { - return type; - } - - /** - * Sets the type of binary content. - * - * @param type - * the content type - * @return this result for method chaining - */ - public ToolBinaryResult setType(String type) { - this.type = type; - return this; - } - - /** - * Gets the description of the binary content. - * - * @return the content description - */ - public String getDescription() { - return description; - } - - /** - * Sets a description of the binary content. - *

- * This helps the assistant understand the content. - * - * @param description - * the content description - * @return this result for method chaining - */ - public ToolBinaryResult setDescription(String description) { - this.description = description; - return this; - } +public record ToolBinaryResult(@JsonProperty("data") String data, @JsonProperty("mimeType") String mimeType, + @JsonProperty("type") String type, @JsonProperty("description") String description) { } diff --git a/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHandler.java b/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHandler.java index 780e966e4..0dc59762b 100644 --- a/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHandler.java +++ b/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHandler.java @@ -16,10 +16,10 @@ * *

{@code
  * UserPromptSubmittedHandler handler = (input, invocation) -> {
- * 	System.out.println("User submitted: " + input.getPrompt());
+ * 	System.out.println("User submitted: " + input.prompt());
  * 	// Optionally modify the prompt
- * 	return CompletableFuture.completedFuture(
- * 			new UserPromptSubmittedHookOutput().setModifiedPrompt(input.getPrompt() + " (enhanced)"));
+ * 	return CompletableFuture
+ * 			.completedFuture(new UserPromptSubmittedHookOutput(input.prompt() + " (enhanced)", null, null));
  * };
  * }
* diff --git a/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookInput.java b/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookInput.java index 40af90acd..bbfdf85bb 100644 --- a/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookInput.java +++ b/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookInput.java @@ -13,80 +13,16 @@ * This hook is invoked when the user submits a prompt, allowing you to * intercept and modify the prompt before it is processed. * + * @param timestamp + * the timestamp in milliseconds since epoch when the prompt was + * submitted + * @param cwd + * the current working directory + * @param prompt + * the user's prompt text * @since 1.0.7 */ @JsonIgnoreProperties(ignoreUnknown = true) -public class UserPromptSubmittedHookInput { - - @JsonProperty("timestamp") - private long timestamp; - - @JsonProperty("cwd") - private String cwd; - - @JsonProperty("prompt") - private String prompt; - - /** - * Gets the timestamp when the prompt was submitted. - * - * @return the timestamp in milliseconds since epoch - */ - public long getTimestamp() { - return timestamp; - } - - /** - * Sets the timestamp when the prompt was submitted. - * - * @param timestamp - * the timestamp in milliseconds since epoch - * @return this instance for method chaining - */ - public UserPromptSubmittedHookInput setTimestamp(long timestamp) { - this.timestamp = timestamp; - return this; - } - - /** - * Gets the current working directory. - * - * @return the current working directory - */ - public String getCwd() { - return cwd; - } - - /** - * Sets the current working directory. - * - * @param cwd - * the current working directory - * @return this instance for method chaining - */ - public UserPromptSubmittedHookInput setCwd(String cwd) { - this.cwd = cwd; - return this; - } - - /** - * Gets the user's prompt. - * - * @return the prompt text - */ - public String getPrompt() { - return prompt; - } - - /** - * Sets the user's prompt. - * - * @param prompt - * the prompt text - * @return this instance for method chaining - */ - public UserPromptSubmittedHookInput setPrompt(String prompt) { - this.prompt = prompt; - return this; - } +public record UserPromptSubmittedHookInput(@JsonProperty("timestamp") long timestamp, @JsonProperty("cwd") String cwd, + @JsonProperty("prompt") String prompt) { } diff --git a/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookOutput.java b/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookOutput.java index 0eb1befa0..d5b345556 100644 --- a/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookOutput.java +++ b/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookOutput.java @@ -12,80 +12,17 @@ *

* Allows modifying the user's prompt before processing. * + * @param modifiedPrompt + * the modified prompt to use instead of the original, or + * {@code null} to use the original + * @param additionalContext + * additional context to be added to the prompt, or {@code null} + * @param suppressOutput + * {@code true} to suppress output, or {@code null} * @since 1.0.7 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public class UserPromptSubmittedHookOutput { - - @JsonProperty("modifiedPrompt") - private String modifiedPrompt; - - @JsonProperty("additionalContext") - private String additionalContext; - - @JsonProperty("suppressOutput") - private Boolean suppressOutput; - - /** - * Gets the modified prompt. - * - * @return the modified prompt, or {@code null} to use the original - */ - public String getModifiedPrompt() { - return modifiedPrompt; - } - - /** - * Sets a modified version of the user's prompt. - * - * @param modifiedPrompt - * the modified prompt to use instead of the original - * @return this instance for method chaining - */ - public UserPromptSubmittedHookOutput setModifiedPrompt(String modifiedPrompt) { - this.modifiedPrompt = modifiedPrompt; - return this; - } - - /** - * Gets the additional context to add. - * - * @return the additional context, or {@code null} - */ - public String getAdditionalContext() { - return additionalContext; - } - - /** - * Sets additional context to be added to the prompt. - * - * @param additionalContext - * the additional context - * @return this instance for method chaining - */ - public UserPromptSubmittedHookOutput setAdditionalContext(String additionalContext) { - this.additionalContext = additionalContext; - return this; - } - - /** - * Gets whether output should be suppressed. - * - * @return {@code true} to suppress output, or {@code null} - */ - public Boolean getSuppressOutput() { - return suppressOutput; - } - - /** - * Sets whether to suppress the output. - * - * @param suppressOutput - * {@code true} to suppress output - * @return this instance for method chaining - */ - public UserPromptSubmittedHookOutput setSuppressOutput(Boolean suppressOutput) { - this.suppressOutput = suppressOutput; - return this; - } +public record UserPromptSubmittedHookOutput(@JsonProperty("modifiedPrompt") String modifiedPrompt, + @JsonProperty("additionalContext") String additionalContext, + @JsonProperty("suppressOutput") Boolean suppressOutput) { } diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index fc0878159..ff7179bb8 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -115,10 +115,7 @@ Include files as context for the AI to analyze. session.send(new MessageOptions() .setPrompt("Review this file for bugs") .setAttachments(List.of( - new Attachment() - .setType("file") - .setPath("/path/to/file.java") - .setDisplayName("MyService.java") + new Attachment("file", "/path/to/file.java", "MyService.java") )) ).get(); ``` diff --git a/src/site/markdown/hooks.md b/src/site/markdown/hooks.md index 68f6bbcfb..78f1c3cb2 100644 --- a/src/site/markdown/hooks.md +++ b/src/site/markdown/hooks.md @@ -184,8 +184,7 @@ var hooks = new SessionHooks() if (input.getToolName().equals("read_file")) { String context = "Note: This file was last modified 2 hours ago."; return CompletableFuture.completedFuture( - new PostToolUseHookOutput() - .setAdditionalContext(context) + new PostToolUseHookOutput(null, context, null) ); } return CompletableFuture.completedFuture(null); @@ -214,12 +213,12 @@ Return `null` - this hook is observation-only. ```java var hooks = new SessionHooks() .setOnUserPromptSubmitted((input, invocation) -> { - System.out.println("User asked: " + input.getPrompt()); + System.out.println("User asked: " + input.prompt()); // Track prompts for analytics analytics.track("user_prompt", Map.of( "sessionId", invocation.getSessionId(), - "promptLength", input.getPrompt().length() + "promptLength", input.prompt().length() )); return CompletableFuture.completedFuture(null); @@ -249,7 +248,7 @@ Return `null` - this hook is observation-only. var hooks = new SessionHooks() .setOnSessionStart((input, invocation) -> { System.out.println("Session started: " + invocation.getSessionId()); - System.out.println("Source: " + input.getSource()); + System.out.println("Source: " + input.source()); // Initialize session-specific resources sessionResources.put(invocation.getSessionId(), new ResourceManager()); @@ -280,7 +279,7 @@ Return `null` - this hook is observation-only. ```java var hooks = new SessionHooks() .setOnSessionEnd((input, invocation) -> { - System.out.println("Session ended: " + input.getReason()); + System.out.println("Session ended: " + input.reason()); // Clean up session resources var resources = sessionResources.remove(invocation.getSessionId()); @@ -335,17 +334,17 @@ public class HooksExample { // Analytics: track user prompts .setOnUserPromptSubmitted((input, invocation) -> { - System.out.println("User: " + input.getPrompt()); + System.out.println("User: " + input.prompt()); return CompletableFuture.completedFuture(null); }) // Lifecycle: initialization and cleanup .setOnSessionStart((input, invocation) -> { - System.out.println("Session started (" + input.getSource() + ")"); + System.out.println("Session started (" + input.source() + ")"); return CompletableFuture.completedFuture(null); }) .setOnSessionEnd((input, invocation) -> { - System.out.println("Session ended: " + input.getReason()); + System.out.println("Session ended: " + input.reason()); return CompletableFuture.completedFuture(null); }); From bc2ed461a5958938ad01e484b24cc36db6cbcf8b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:11:14 +0000 Subject: [PATCH 075/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 2b4ee6ff1..2535b481c 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 84.2% - 84.2% + 86% + 86% From fba8d73c3b36a240378d245782e0ddb408b280ad Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 20:11:31 -0500 Subject: [PATCH 076/147] chore: update weekly upstream sync workflow to assign created issues to Copilot --- .../workflows/weekly-upstream-sync.lock.yml | 186 ++++++++++-------- .github/workflows/weekly-upstream-sync.md | 1 + 2 files changed, 106 insertions(+), 81 deletions(-) diff --git a/.github/workflows/weekly-upstream-sync.lock.yml b/.github/workflows/weekly-upstream-sync.lock.yml index 17c726f5d..163600e1e 100644 --- a/.github/workflows/weekly-upstream-sync.lock.yml +++ b/.github/workflows/weekly-upstream-sync.lock.yml @@ -13,7 +13,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.42.17). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.43.2). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -22,7 +22,7 @@ # Weekly upstream sync workflow. Checks for new commits in the official # Copilot SDK (github/copilot-sdk) and assigns to Copilot to port changes. # -# frontmatter-hash: d578661b88d0b1225af55ed3d5ad443003b08248f078bbe0fc306f89af2ad801 +# frontmatter-hash: 27b0f4284357c75008ce6e8d1d772fe9871a3548be6a6a4996125b6ca980cc2e name: "Weekly Upstream Sync Agentic Workflow" "on": @@ -48,11 +48,11 @@ jobs: comment_repo: "" steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.2 with: destination: /opt/gh-aw/actions - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_WORKFLOW_FILE: "weekly-upstream-sync.lock.yml" with: @@ -89,16 +89,16 @@ jobs: secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.2 with: destination: /opt/gh-aw/actions - name: Checkout .github and .agents folders - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: sparse-checkout: | .github .agents - depth: 1 + fetch-depth: 1 persist-credentials: false - name: Create gh-aw temp directory run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh @@ -117,7 +117,7 @@ jobs: id: checkout-pr if: | github.event.pull_request - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: @@ -127,6 +127,51 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); await main(); + - name: Generate agentic run info + id: generate_aw_info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "copilot", + engine_name: "GitHub Copilot CLI", + model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", + version: "", + agent_version: "0.0.405", + cli_version: "v0.43.2", + workflow_name: "Weekly Upstream Sync Agentic Workflow", + experimental: false, + supports_tools_allowlist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults","github"], + firewall_enabled: true, + awf_version: "v0.13.12", + awmg_version: "", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in PR + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/jobs + core.setOutput('model', awInfo.model); - name: Validate COPILOT_GITHUB_TOKEN secret id: validate-secret run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default @@ -159,7 +204,7 @@ jobs: cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' [ { - "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[upstream-sync] \". Labels [upstream-sync] will be automatically added.", + "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[upstream-sync] \". Labels [upstream-sync] will be automatically added. Assignees [copilot] will be automatically assigned.", "inputSchema": { "additionalProperties": false, "properties": { @@ -460,13 +505,12 @@ jobs: id: safe-outputs-config run: | # Generate a secure random API key (360 bits of entropy, 40+ chars) - API_KEY="" + # Mask immediately to prevent timing vulnerabilities API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - PORT=3001 - - # Register API key as secret to mask it from logs echo "::add-mask::${API_KEY}" + PORT=3001 + # Set outputs for next steps { echo "safe_outputs_api_key=${API_KEY}" @@ -510,15 +554,13 @@ jobs: # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="80" export MCP_GATEWAY_DOMAIN="host.docker.internal" - MCP_GATEWAY_API_KEY="" 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 DEBUG="*" - # Register API key as secret to mask it from logs - echo "::add-mask::${MCP_GATEWAY_API_KEY}" export GH_AW_ENGINE="copilot" export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -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_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_LOCKDOWN -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 GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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 ghcr.io/github/gh-aw-mcpg:v0.0.113' @@ -552,53 +594,8 @@ jobs: } } MCPCONFIG_EOF - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "copilot", - engine_name: "GitHub Copilot CLI", - model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", - version: "", - agent_version: "0.0.405", - cli_version: "v0.42.17", - workflow_name: "Weekly Upstream Sync Agentic Workflow", - experimental: false, - supports_tools_allowlist: true, - supports_http_transport: true, - run_id: context.runId, - run_number: context.runNumber, - run_attempt: process.env.GITHUB_RUN_ATTEMPT, - repository: context.repo.owner + '/' + context.repo.repo, - ref: context.ref, - sha: context.sha, - actor: context.actor, - event_name: context.eventName, - staged: false, - allowed_domains: ["defaults","github"], - firewall_enabled: true, - awf_version: "v0.13.12", - awmg_version: "v0.0.113", - steps: { - firewall: "squid" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/aw_info.json'; - fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); - console.log('Generated aw_info.json at:', tmpPath); - console.log(JSON.stringify(awInfo, null, 2)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); @@ -674,7 +671,7 @@ jobs: {{#runtime-import .github/workflows/weekly-upstream-sync.md}} PROMPT_EOF - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_GITHUB_ACTOR: ${{ github.actor }} @@ -704,7 +701,7 @@ jobs: } }); - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt with: @@ -721,6 +718,8 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Clean git credentials + run: bash /opt/gh-aw/actions/clean_git_credentials.sh - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): @@ -742,6 +741,17 @@ jobs: GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} GITHUB_WORKSPACE: ${{ github.workspace }} XDG_CONFIG_HOME: /home/runner + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" - name: Copy Copilot session state files to logs if: always() continue-on-error: true @@ -770,7 +780,7 @@ jobs: bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" - name: Redact secrets in logs if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); @@ -792,7 +802,7 @@ jobs: if-no-files-found: warn - name: Ingest agent output id: collect_output - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.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" @@ -821,7 +831,7 @@ jobs: if-no-files-found: ignore - name: Parse agent logs for step summary if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ with: @@ -832,7 +842,7 @@ jobs: await main(); - name: Parse MCP gateway logs for step summary if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); @@ -883,7 +893,7 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.2 with: destination: /opt/gh-aw/actions - name: Debug job inputs @@ -910,7 +920,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - name: Process No-Op Messages id: noop - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: 1 @@ -924,7 +934,7 @@ jobs: await main(); - name: Record Missing Tool id: missing_tool - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" @@ -937,12 +947,13 @@ jobs: await main(); - name: Handle Agent Failure id: handle_agent_failure - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" 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: "weekly-upstream-sync" GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_ASSIGNMENT_ERRORS: ${{ needs.safe_outputs.outputs.assign_to_agent_assignment_errors }} @@ -956,7 +967,7 @@ jobs: await main(); - name: Handle No-Op Message id: handle_noop_message - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" @@ -973,7 +984,7 @@ jobs: await main(); - name: Update reaction comment with completion status id: conclusion - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} @@ -1002,7 +1013,7 @@ jobs: success: ${{ steps.parse_results.outputs.success }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.2 with: destination: /opt/gh-aw/actions - name: Download agent artifacts @@ -1023,7 +1034,7 @@ jobs: run: | echo "Agent output-types: $AGENT_OUTPUT_TYPES" - name: Setup threat detection - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: WORKFLOW_NAME: "Weekly Upstream Sync Agentic Workflow" WORKFLOW_DESCRIPTION: "Weekly upstream sync workflow. Checks for new commits in the official\nCopilot SDK (github/copilot-sdk) and assigns to Copilot to port changes." @@ -1076,7 +1087,7 @@ jobs: XDG_CONFIG_HOME: /home/runner - name: Parse threat detection results id: parse_results - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); @@ -1117,7 +1128,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.2 with: destination: /opt/gh-aw/actions - name: Download agent output artifact @@ -1133,10 +1144,11 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - name: Process Safe Outputs id: process_safe_outputs - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"close_issue\":{\"max\":10,\"required_labels\":[\"upstream-sync\"],\"target\":\"*\"},\"create_issue\":{\"expires\":144,\"labels\":[\"upstream-sync\"],\"max\":1,\"title_prefix\":\"[upstream-sync] \"},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"close_issue\":{\"max\":10,\"required_labels\":[\"upstream-sync\"],\"target\":\"*\"},\"create_issue\":{\"assignees\":[\"copilot\"],\"expires\":144,\"labels\":[\"upstream-sync\"],\"max\":1,\"title_prefix\":\"[upstream-sync] \"},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_ASSIGN_COPILOT: "true" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1144,10 +1156,22 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); + - name: Assign Copilot to created issues + if: steps.process_safe_outputs.outputs.issues_to_assign_copilot != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_ISSUES_TO_ASSIGN_COPILOT: ${{ steps.process_safe_outputs.outputs.issues_to_assign_copilot }} + with: + github-token: ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/assign_copilot_to_created_issues.cjs'); + await main(); - name: Assign To Agent id: assign_to_agent if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'assign_to_agent')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_AGENT_DEFAULT: "copilot" diff --git a/.github/workflows/weekly-upstream-sync.md b/.github/workflows/weekly-upstream-sync.md index c2ae5840d..15843a050 100644 --- a/.github/workflows/weekly-upstream-sync.md +++ b/.github/workflows/weekly-upstream-sync.md @@ -24,6 +24,7 @@ tools: safe-outputs: create-issue: title-prefix: "[upstream-sync] " + assignees: [copilot] labels: [upstream-sync] expires: 6 close-issue: From d1a3fd772a5ca241bc4cc47d914aeaf5515dfb8c Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 20:59:05 -0500 Subject: [PATCH 077/147] test: add lifecycle event handling tests for CopilotClient --- .../github/copilot/sdk/CopilotClientTest.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index 8887e6e12..26aa9b2bc 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -9,11 +9,15 @@ import com.github.copilot.sdk.json.CopilotClientOptions; import com.github.copilot.sdk.json.PingResponse; +import com.github.copilot.sdk.json.SessionLifecycleEvent; +import com.github.copilot.sdk.json.SessionLifecycleEventTypes; import java.io.BufferedReader; import java.io.InputStreamReader; +import java.lang.reflect.Field; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import static org.junit.jupiter.api.Assertions.*; @@ -218,4 +222,105 @@ void testUseLoggedInUserWithCliUrlThrows() { assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); } + + // ===== onLifecycle tests ===== + + /** + * Gets the internal LifecycleEventManager from a CopilotClient via reflection + * so we can dispatch events for testing. + */ + private static LifecycleEventManager getLifecycleManager(CopilotClient client) throws Exception { + Field f = CopilotClient.class.getDeclaredField("lifecycleManager"); + f.setAccessible(true); + return (LifecycleEventManager) f.get(client); + } + + private static SessionLifecycleEvent lifecycleEvent(String type) { + var e = new SessionLifecycleEvent(); + e.setType(type); + e.setSessionId("test-session-id"); + return e; + } + + @Test + void testOnLifecycleWildcardReceivesAllEvents() throws Exception { + try (var client = new CopilotClient()) { + var received = new ArrayList(); + client.onLifecycle(received::add); + + LifecycleEventManager mgr = getLifecycleManager(client); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.CREATED)); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.DELETED)); + + assertEquals(2, received.size()); + assertEquals(SessionLifecycleEventTypes.CREATED, received.get(0).getType()); + assertEquals(SessionLifecycleEventTypes.DELETED, received.get(1).getType()); + } + } + + @Test + void testOnLifecycleTypedReceivesOnlyMatchingEvents() throws Exception { + try (var client = new CopilotClient()) { + var received = new ArrayList(); + client.onLifecycle(SessionLifecycleEventTypes.CREATED, received::add); + + LifecycleEventManager mgr = getLifecycleManager(client); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.CREATED)); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.DELETED)); + + assertEquals(1, received.size()); + assertEquals(SessionLifecycleEventTypes.CREATED, received.get(0).getType()); + } + } + + @Test + void testOnLifecycleUnsubscribeStopsDelivery() throws Exception { + try (var client = new CopilotClient()) { + var received = new ArrayList(); + AutoCloseable sub = client.onLifecycle(received::add); + + LifecycleEventManager mgr = getLifecycleManager(client); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.CREATED)); + assertEquals(1, received.size()); + + sub.close(); + + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.DELETED)); + assertEquals(1, received.size(), "Should not receive events after unsubscribe"); + } + } + + @Test + void testOnLifecycleTypedUnsubscribeStopsDelivery() throws Exception { + try (var client = new CopilotClient()) { + var received = new ArrayList(); + AutoCloseable sub = client.onLifecycle(SessionLifecycleEventTypes.UPDATED, received::add); + + LifecycleEventManager mgr = getLifecycleManager(client); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.UPDATED)); + assertEquals(1, received.size()); + + sub.close(); + + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.UPDATED)); + assertEquals(1, received.size(), "Should not receive events after unsubscribe"); + } + } + + @Test + void testOnLifecycleMultipleHandlers() throws Exception { + try (var client = new CopilotClient()) { + var wildcard = new ArrayList(); + var typed = new ArrayList(); + + client.onLifecycle(wildcard::add); + client.onLifecycle(SessionLifecycleEventTypes.CREATED, typed::add); + + LifecycleEventManager mgr = getLifecycleManager(client); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.CREATED)); + + assertEquals(1, wildcard.size()); + assertEquals(1, typed.size()); + } + } } From 357d0ebadc795360771d3f83b4952bdebf59d949 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:02:10 +0000 Subject: [PATCH 078/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 2535b481c..a55f386c6 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 86% - 86% + 86.2% + 86.2% From fbf9589e31d742c4a43e7944cbb6a21761bf97e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:46:00 +0000 Subject: [PATCH 079/147] Initial plan From 9134c6572ef6579ca48cb09d5bb06087d4712903 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:48:06 +0000 Subject: [PATCH 080/147] chore: upstream sync to 4dc5629 (no changes needed - Python-only fix) Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .lastmerge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lastmerge b/.lastmerge index 0db96638f..097aad7ba 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -b904431d1917e2b86f76fcde79a563921d8ef28c +4dc562951a51097618a69a1837f7af06c73d1113 From bd38cecdb43850e38df069fc4ad01c61f93e27b3 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 22:12:54 -0500 Subject: [PATCH 081/147] test: add coverage for CopilotClient state management and connection handling --- .../github/copilot/sdk/CopilotClientTest.java | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index 26aa9b2bc..05e7290b3 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -18,6 +18,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import static org.junit.jupiter.api.Assertions.*; @@ -323,4 +325,128 @@ void testOnLifecycleMultipleHandlers() throws Exception { assertEquals(1, typed.size()); } } + + // ===== getState() coverage ===== + + @Test + void testGetStateErrorAfterFailedStart() throws Exception { + // Use a non-existent CLI path to trigger a startup failure + var options = new CopilotClientOptions().setCliPath("/nonexistent/path/to/cli").setAutoStart(false); + + try (var client = new CopilotClient(options)) { + // Manually start to trigger the error + CompletableFuture startFuture = client.start(); + + // Wait for the start to fail + try { + startFuture.get(); + } catch (ExecutionException e) { + // Expected + } + + assertEquals(ConnectionState.ERROR, client.getState()); + } + } + + @Test + void testGetStateConnectingDuringStart() throws Exception { + // Use a non-existent CLI path; the future won't complete immediately + var options = new CopilotClientOptions().setCliPath("/nonexistent/path/to/cli").setAutoStart(false); + + try (var client = new CopilotClient(options)) { + // Start is async - grab state before completion + client.start(); + + // The state should be either CONNECTING or ERROR depending on timing + ConnectionState state = client.getState(); + assertTrue(state == ConnectionState.CONNECTING || state == ConnectionState.ERROR, + "State should be CONNECTING or ERROR, was: " + state); + } + } + + // ===== ensureConnected throws when autoStart=false and not connected ===== + + @Test + void testEnsureConnectedThrowsWhenNotStartedAndAutoStartDisabled() { + var options = new CopilotClientOptions().setAutoStart(false); + + try (var client = new CopilotClient(options)) { + // Calling ping (which calls ensureConnected) without start() should throw + assertThrows(IllegalStateException.class, () -> client.ping("test")); + } + } + + // ===== close() idempotency ===== + + @Test + void testCloseIsIdempotent() { + var client = new CopilotClient(); + + // First close + client.close(); + // Second close should not throw + assertDoesNotThrow(() -> client.close()); + } + + @Test + void testCloseAfterFailedStart() throws Exception { + var options = new CopilotClientOptions().setCliPath("/nonexistent/path/to/cli").setAutoStart(false); + var client = new CopilotClient(options); + + CompletableFuture startFuture = client.start(); + try { + startFuture.get(); + } catch (ExecutionException e) { + // Expected + } + + // close() after a failed start should not throw + assertDoesNotThrow(() -> client.close()); + } + + // ===== stop() with no connection ===== + + @Test + void testStopWithNoConnectionCompletes() throws Exception { + try (var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false))) { + // stop() without start() should complete without error + client.stop().get(); + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + } + } + + @Test + void testForceStopWithNoConnectionCompletes() throws Exception { + try (var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false))) { + // forceStop() without start() should complete without error + client.forceStop().get(); + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + } + } + + // ===== start() idempotency ===== + + @Test + void testStartIsIdempotentSingleConnectionAttempt() throws Exception { + var options = new CopilotClientOptions().setCliPath("/nonexistent/path/to/cli").setAutoStart(false); + + try (var client = new CopilotClient(options)) { + client.start(); + client.start(); + + // Both calls should result in the same state (single connection attempt) + ConnectionState state = client.getState(); + assertTrue(state == ConnectionState.CONNECTING || state == ConnectionState.ERROR, + "State should be CONNECTING or ERROR after start(), was: " + state); + } + } + + // ===== null options defaulting ===== + + @Test + void testNullOptionsDefaultsToEmpty() { + try (var client = new CopilotClient(null)) { + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + } + } } From e6851d95a993e03f749f3620487f5043be409a55 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 03:16:22 +0000 Subject: [PATCH 082/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index a55f386c6..812e6354a 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 86.2% - 86.2% + 86.4% + 86.4% From 6b901a18f7797c4969420da6fc1b1d9ab6bc3452 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 22:16:23 -0500 Subject: [PATCH 083/147] docs: update weekly upstream sync documentation to clarify Pull Request creation requirements --- .github/workflows/weekly-upstream-sync.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/weekly-upstream-sync.md b/.github/workflows/weekly-upstream-sync.md index 15843a050..31a445df4 100644 --- a/.github/workflows/weekly-upstream-sync.md +++ b/.github/workflows/weekly-upstream-sync.md @@ -47,6 +47,8 @@ This document describes the `weekly-upstream-sync.yml` GitHub Actions workflow, The workflow runs on a **weekly schedule** (every Monday at 10:00 UTC) and can also be triggered manually. It does **not** perform the actual merge — instead, it detects upstream changes and creates a GitHub issue assigned to `copilot`, instructing the agent to follow the [agentic-merge-upstream](../prompts/agentic-merge-upstream.prompt.md) prompt to port the changes. +The agent must also create the Pull Request with the label `upstream-sync`. This allows the workflow to track the merge progress and avoid creating duplicate issues if the agent is still working on a previous sync. + ## Trigger | Trigger | Schedule | From 1c48adb0516d29d0b2bfa7ae0255bbfdedac07e7 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 22:30:24 -0500 Subject: [PATCH 084/147] docs: enhance test running instructions for AI agents to improve failure diagnosis --- .github/copilot-instructions.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 65d62e0b0..bc28a29fb 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -36,6 +36,35 @@ mvn clean package -DskipTests mvn test -Pdebug ``` +### Running Tests from AI Agents / Copilot + +When running tests to verify changes, **always use `mvn verify` without `-q` and without piping through `grep`**. The full output is needed to diagnose failures. Do NOT use commands like: + +```bash +# BAD - hides critical failure details, often requires a second run +mvn verify -q 2>&1 | grep -E 'Tests run:|BUILD' +mvn verify 2>&1 | grep -E 'Tests run.*in com|BUILD|test failure' +``` + +Instead, use one of these approaches: + +```bash +# GOOD - run tests with full output (preferred for investigating failures) +mvn verify + +# GOOD - run tests showing just the summary and result using Maven's built-in log level +mvn verify -B --fail-at-end 2>&1 | tail -30 + +# GOOD - run a single test class when debugging a specific test +mvn test -Dtest=CopilotClientTest +``` + +**Interpreting results:** +- `BUILD SUCCESS` at the end means all tests passed. No further investigation needed. +- `BUILD FAILURE` means something failed. The lines immediately above `BUILD FAILURE` will contain the relevant error information. Look for `[ERROR]` lines near the bottom of the output. +- The Surefire summary line `Tests run: X, Failures: Y, Errors: Z, Skipped: W` appears near the end and gives the counts. +- **Do NOT run tests a second time** just to get different output formatting. One run with full output is sufficient. + ## Architecture ### Core Components From b8b7a7df944c7fd062d4ea73fab27ddafab66907 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 22:41:14 -0500 Subject: [PATCH 085/147] test: refactor CopilotSessionTest to use String overloads for send methods and add SessionHandlerTest for internal handler methods --- .../copilot/sdk/CopilotSessionTest.java | 7 +- .../copilot/sdk/SessionHandlerTest.java | 323 ++++++++++++++++++ 2 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/github/copilot/sdk/SessionHandlerTest.java diff --git a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java index 233be5236..02a7b580a 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -196,7 +196,8 @@ void testSendReturnsImmediatelyWhileEventsStreamInBackground() throws Exception }); // Use a slow command so we can verify send() returns before completion - session.send(new MessageOptions().setPrompt("Run 'sleep 2 && echo done'")).get(); + // Use String convenience overload (covers send(String) path) + session.send("Run 'sleep 2 && echo done'").get(); // At this point, we might not have received session.idle yet // The event handling happens asynchronously @@ -231,8 +232,8 @@ void testSendAndWaitBlocksUntilSessionIdleAndReturnsFinalAssistantMessage() thro var events = new ArrayList(); session.on(evt -> events.add(evt.getType())); - AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, - TimeUnit.SECONDS); + // Use String convenience overload (covers sendAndWait(String) path) + AssistantMessageEvent response = session.sendAndWait("What is 2+2?").get(60, TimeUnit.SECONDS); assertNotNull(response); assertEquals("assistant.message", response.getType()); diff --git a/src/test/java/com/github/copilot/sdk/SessionHandlerTest.java b/src/test/java/com/github/copilot/sdk/SessionHandlerTest.java new file mode 100644 index 000000000..53b478eab --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/SessionHandlerTest.java @@ -0,0 +1,323 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.SessionEndHookOutput; +import com.github.copilot.sdk.json.SessionHooks; +import com.github.copilot.sdk.json.SessionStartHookOutput; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.UserInputRequest; +import com.github.copilot.sdk.json.UserInputResponse; +import com.github.copilot.sdk.json.UserPromptSubmittedHookOutput; + +/** + * Unit tests for CopilotSession internal handler methods. + *

+ * Tests package-private handler and hook dispatch logic that doesn't require a + * live CLI connection. + */ +public class SessionHandlerTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + private CopilotSession session; + + @BeforeEach + void setup() throws Exception { + var constructor = CopilotSession.class.getDeclaredConstructor(String.class, JsonRpcClient.class, String.class); + constructor.setAccessible(true); + session = constructor.newInstance("handler-test-session", null, null); + } + + // ===== setEventErrorPolicy ===== + + @Test + void testSetEventErrorPolicyNullThrowsNPE() { + assertThrows(NullPointerException.class, () -> session.setEventErrorPolicy(null)); + } + + @Test + void testSetEventErrorPolicySetsValue() { + session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); + // No exception means success; the policy is stored internally + } + + // ===== handlePermissionRequest: no handler registered ===== + + @Test + void testHandlePermissionRequestWithNoHandlerReturnsDenied() throws Exception { + JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file", "resource", "/tmp/test")); + + PermissionRequestResult result = session.handlePermissionRequest(data).get(); + + assertEquals("denied-no-approval-rule-and-could-not-request-from-user", result.getKind()); + } + + // ===== handlePermissionRequest: handler throws ===== + + @Test + void testHandlePermissionRequestHandlerExceptionReturnsDenied() throws Exception { + session.registerPermissionHandler((request, invocation) -> { + throw new RuntimeException("handler boom"); + }); + + JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file")); + + PermissionRequestResult result = session.handlePermissionRequest(data).get(); + + assertEquals("denied-no-approval-rule-and-could-not-request-from-user", result.getKind()); + } + + // ===== handlePermissionRequest: handler future fails ===== + + @Test + void testHandlePermissionRequestHandlerFutureFailsReturnsDenied() throws Exception { + session.registerPermissionHandler( + (request, invocation) -> CompletableFuture.failedFuture(new RuntimeException("async handler boom"))); + + JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file")); + + PermissionRequestResult result = session.handlePermissionRequest(data).get(); + + assertEquals("denied-no-approval-rule-and-could-not-request-from-user", result.getKind()); + } + + // ===== handlePermissionRequest: handler succeeds ===== + + @Test + void testHandlePermissionRequestHandlerSucceeds() throws Exception { + session.registerPermissionHandler((request, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + var res = new PermissionRequestResult(); + res.setKind("allow"); + return CompletableFuture.completedFuture(res); + }); + + JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file")); + + PermissionRequestResult result = session.handlePermissionRequest(data).get(); + + assertEquals("allow", result.getKind()); + } + + // ===== handleUserInputRequest: no handler registered ===== + + @Test + void testHandleUserInputRequestNoHandler() { + var request = new UserInputRequest(); + + ExecutionException ex = assertThrows(ExecutionException.class, + () -> session.handleUserInputRequest(request).get()); + assertInstanceOf(IllegalStateException.class, ex.getCause()); + } + + // ===== handleUserInputRequest: handler throws synchronously ===== + + @Test + void testHandleUserInputRequestHandlerThrowsSynchronously() { + session.registerUserInputHandler((req, invocation) -> { + throw new RuntimeException("sync user input boom"); + }); + + var request = new UserInputRequest(); + + ExecutionException ex = assertThrows(ExecutionException.class, + () -> session.handleUserInputRequest(request).get()); + assertInstanceOf(RuntimeException.class, ex.getCause()); + } + + // ===== handleUserInputRequest: handler future fails ===== + + @Test + void testHandleUserInputRequestHandlerFutureFails() { + session.registerUserInputHandler( + (req, invocation) -> CompletableFuture.failedFuture(new RuntimeException("async user input boom"))); + + var request = new UserInputRequest(); + + ExecutionException ex = assertThrows(ExecutionException.class, + () -> session.handleUserInputRequest(request).get()); + assertInstanceOf(RuntimeException.class, ex.getCause()); + } + + // ===== handleUserInputRequest: handler succeeds ===== + + @Test + void testHandleUserInputRequestHandlerSucceeds() throws Exception { + session.registerUserInputHandler((req, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + return CompletableFuture.completedFuture(new UserInputResponse().setAnswer("user typed this")); + }); + + var request = new UserInputRequest(); + + UserInputResponse response = session.handleUserInputRequest(request).get(); + + assertEquals("user typed this", response.getAnswer()); + } + + // ===== handleHooksInvoke: no hooks registered ===== + + @Test + void testHandleHooksInvokeNoHooksReturnsNull() throws Exception { + JsonNode input = MAPPER.valueToTree(Map.of()); + + Object result = session.handleHooksInvoke("preToolUse", input).get(); + + assertNull(result); + } + + // ===== handleHooksInvoke: userPromptSubmitted ===== + + @Test + void testHandleHooksInvokeUserPromptSubmitted() throws Exception { + var hooks = new SessionHooks().setOnUserPromptSubmitted((hookInput, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + return CompletableFuture + .completedFuture(new UserPromptSubmittedHookOutput("modified prompt", "extra context", false)); + }); + session.registerHooks(hooks); + + JsonNode input = MAPPER + .valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "prompt", "original prompt")); + + Object result = session.handleHooksInvoke("userPromptSubmitted", input).get(); + + assertInstanceOf(UserPromptSubmittedHookOutput.class, result); + var output = (UserPromptSubmittedHookOutput) result; + assertEquals("modified prompt", output.modifiedPrompt()); + } + + // ===== handleHooksInvoke: sessionStart ===== + + @Test + void testHandleHooksInvokeSessionStart() throws Exception { + var hooks = new SessionHooks().setOnSessionStart((hookInput, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + return CompletableFuture.completedFuture(new SessionStartHookOutput("additional context", null)); + }); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "source", "test")); + + Object result = session.handleHooksInvoke("sessionStart", input).get(); + + assertInstanceOf(SessionStartHookOutput.class, result); + var output = (SessionStartHookOutput) result; + assertEquals("additional context", output.additionalContext()); + } + + // ===== handleHooksInvoke: sessionEnd ===== + + @Test + void testHandleHooksInvokeSessionEnd() throws Exception { + var hooks = new SessionHooks().setOnSessionEnd((hookInput, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + return CompletableFuture.completedFuture(new SessionEndHookOutput(false, null, "summary")); + }); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "reason", "user_closed")); + + Object result = session.handleHooksInvoke("sessionEnd", input).get(); + + assertInstanceOf(SessionEndHookOutput.class, result); + var output = (SessionEndHookOutput) result; + assertEquals("summary", output.sessionSummary()); + } + + // ===== handleHooksInvoke: unhandled hook type ===== + + @Test + void testHandleHooksInvokeUnhandledHookType() throws Exception { + session.registerHooks(new SessionHooks()); + + JsonNode input = MAPPER.valueToTree(Map.of()); + + Object result = session.handleHooksInvoke("unknownHookType", input).get(); + + assertNull(result); + } + + // ===== handleHooksInvoke: handler throws ===== + + @Test + void testHandleHooksInvokeHandlerThrows() throws Exception { + var hooks = new SessionHooks().setOnSessionStart((hookInput, invocation) -> { + throw new RuntimeException("hook boom"); + }); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "source", "test")); + + ExecutionException ex = assertThrows(ExecutionException.class, + () -> session.handleHooksInvoke("sessionStart", input).get()); + assertInstanceOf(RuntimeException.class, ex.getCause()); + } + + // ===== handleHooksInvoke: invalid JSON for hook input ===== + + @Test + void testHandleHooksInvokeInvalidJsonFails() throws Exception { + var hooks = new SessionHooks().setOnSessionStart( + (hookInput, invocation) -> CompletableFuture.completedFuture(new SessionStartHookOutput(null, null))); + session.registerHooks(hooks); + + // Pass an array node which can't be deserialized into SessionStartHookInput + JsonNode input = MAPPER.valueToTree(List.of("not", "an", "object")); + + ExecutionException ex = assertThrows(ExecutionException.class, + () -> session.handleHooksInvoke("sessionStart", input).get()); + assertInstanceOf(Exception.class, ex.getCause()); + } + + // ===== handleHooksInvoke: hook handler with null callback ===== + + @Test + void testHandleHooksInvokeNullCallbackReturnsNull() throws Exception { + // SessionHooks with only userPromptSubmitted set, sessionStart is null + var hooks = new SessionHooks().setOnUserPromptSubmitted((hookInput, invocation) -> CompletableFuture + .completedFuture(new UserPromptSubmittedHookOutput(null, null, null))); + session.registerHooks(hooks); + + // Invoke sessionStart hook - its handler is null + JsonNode input = MAPPER.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "source", "test")); + + Object result = session.handleHooksInvoke("sessionStart", input).get(); + + assertNull(result); + } + + // ===== registerTools ===== + + @Test + void testRegisterToolsNullIsSafe() { + session.registerTools(null); + assertNull(session.getTool("anything")); + } + + @Test + void testRegisterToolsEmptyListClearsTools() { + session.registerTools(List.of(ToolDefinition.create("my_tool", "desc", Map.of(), + invocation -> CompletableFuture.completedFuture("result")))); + assertNotNull(session.getTool("my_tool")); + + session.registerTools(List.of()); + assertNull(session.getTool("my_tool")); + } +} From d4eb34717fbf9e954f22951bd1be002b3a358286 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 23:04:37 -0500 Subject: [PATCH 086/147] test: add unit tests for SessionRequestBuilder to improve branch coverage --- .../sdk/SessionRequestBuilderTest.java | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java diff --git a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java new file mode 100644 index 000000000..d322dc8bc --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.CreateSessionRequest; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.ResumeSessionRequest; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionHooks; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.UserInputResponse; + +/** + * Unit tests for {@link SessionRequestBuilder} branch coverage. + *

+ * Exercises branches in buildCreateRequest, buildResumeRequest, and + * configureSession that are not reached by E2E tests. + */ +public class SessionRequestBuilderTest { + + // ========================================================================= + // buildCreateRequest + // ========================================================================= + + @Test + void testBuildCreateRequestNullConfig() { + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(null); + assertNotNull(request); + assertNull(request.getModel()); + } + + @Test + void testBuildCreateRequestHooksNonNullButEmpty() { + // Hooks object exists but hasHooks() returns false + var config = new SessionConfig().setHooks(new SessionHooks()); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertNull(request.getHooks(), "Should be null when hooks are empty"); + } + + @Test + void testBuildCreateRequestHooksWithHandler() { + var hooks = new SessionHooks().setOnPreToolUse((input, inv) -> CompletableFuture.completedFuture(null)); + var config = new SessionConfig().setHooks(hooks); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertTrue(request.getHooks(), "Should be true when hooks have handlers"); + } + + // ========================================================================= + // buildResumeRequest + // ========================================================================= + + @Test + void testBuildResumeRequestNullConfig() { + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", null); + assertEquals("sid-1", request.getSessionId()); + assertNull(request.getModel()); + } + + @Test + void testBuildResumeRequestWithTools() { + var tool = ToolDefinition.create("my_tool", "A tool", Map.of("type", "object"), + inv -> CompletableFuture.completedFuture("result")); + var config = new ResumeSessionConfig().setTools(List.of(tool)); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-2", config); + + assertNotNull(request.getTools()); + assertEquals(1, request.getTools().size()); + assertEquals("my_tool", request.getTools().get(0).getName()); + } + + @Test + void testBuildResumeRequestWithUserInputHandler() { + var config = new ResumeSessionConfig() + .setOnUserInputRequest((req, inv) -> CompletableFuture.completedFuture(new UserInputResponse())); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-3", config); + + assertTrue(request.getRequestUserInput()); + } + + @Test + void testBuildResumeRequestHooksNonNullButEmpty() { + var config = new ResumeSessionConfig().setHooks(new SessionHooks()); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-4", config); + + assertNull(request.getHooks(), "Should be null when hooks are empty"); + } + + @Test + void testBuildResumeRequestHooksWithHandler() { + var hooks = new SessionHooks().setOnSessionEnd((input, inv) -> CompletableFuture.completedFuture(null)); + var config = new ResumeSessionConfig().setHooks(hooks); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-5", config); + + assertTrue(request.getHooks(), "Should be true when hooks have handlers"); + } + + @Test + void testBuildResumeRequestDisableResume() { + var config = new ResumeSessionConfig().setDisableResume(true); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-6", config); + + assertTrue(request.getDisableResume()); + } + + @Test + void testBuildResumeRequestStreaming() { + var config = new ResumeSessionConfig().setStreaming(true); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-7", config); + + assertTrue(request.getStreaming()); + } + + // ========================================================================= + // configureSession (ResumeSessionConfig overload) + // ========================================================================= + + @Test + void testConfigureResumeSessionNullConfig() throws Exception { + var session = createTestSession(); + // Should not throw + SessionRequestBuilder.configureSession(session, (ResumeSessionConfig) null); + } + + @Test + void testConfigureResumeSessionWithTools() throws Exception { + var session = createTestSession(); + var tool = ToolDefinition.create("resume_tool", "desc", Map.of(), + inv -> CompletableFuture.completedFuture("ok")); + var config = new ResumeSessionConfig().setTools(List.of(tool)); + + SessionRequestBuilder.configureSession(session, config); + + assertNotNull(session.getTool("resume_tool")); + } + + @Test + void testConfigureResumeSessionWithUserInputHandler() throws Exception { + var session = createTestSession(); + var config = new ResumeSessionConfig() + .setOnUserInputRequest((req, inv) -> CompletableFuture.completedFuture(new UserInputResponse())); + + SessionRequestBuilder.configureSession(session, config); + + // Handler was registered — verify by calling handleUserInputRequest + // (package-private) + var response = session.handleUserInputRequest(new com.github.copilot.sdk.json.UserInputRequest()).get(); + assertNotNull(response); + } + + @Test + void testConfigureResumeSessionWithHooks() throws Exception { + var session = createTestSession(); + var hooks = new SessionHooks().setOnPreToolUse((input, inv) -> CompletableFuture.completedFuture(null)); + var config = new ResumeSessionConfig().setHooks(hooks); + + SessionRequestBuilder.configureSession(session, config); + + // Hooks registered — handleHooksInvoke should dispatch preToolUse + var mapper = JsonRpcClient.getObjectMapper(); + var input = mapper.valueToTree(Map.of("toolName", "test_tool")); + var result = session.handleHooksInvoke("preToolUse", input).get(); + assertNull(result); // handler returns null + } + + // ========================================================================= + // Helper + // ========================================================================= + + private CopilotSession createTestSession() throws Exception { + var constructor = CopilotSession.class.getDeclaredConstructor(String.class, JsonRpcClient.class, String.class); + constructor.setAccessible(true); + return constructor.newInstance("builder-test-session", null, null); + } +} From b27117bfa410faadea905199eebde1f3b4574f3e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:08:02 +0000 Subject: [PATCH 087/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 812e6354a..d7245adb2 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -6,13 +6,13 @@ - + coverage coverage - 86.4% - 86.4% + 90.1% + 90.1% From 4f30838d40604bedbbb4260d20b05a2868440b37 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 10 Feb 2026 23:16:48 -0500 Subject: [PATCH 088/147] refactor: update tool handling to use ToolDefinition and simplify SessionRequestBuilder --- .../github/copilot/sdk/CopilotSession.java | 2 +- .../copilot/sdk/RpcHandlerDispatcher.java | 4 +- .../copilot/sdk/SessionRequestBuilder.java | 13 +- .../sdk/json/CreateSessionRequest.java | 6 +- .../sdk/json/ResumeSessionRequest.java | 6 +- .../com/github/copilot/sdk/json/ToolDef.java | 109 ------------- .../copilot/sdk/json/ToolDefinition.java | 149 ++---------------- .../sdk/SessionRequestBuilderTest.java | 2 +- 8 files changed, 23 insertions(+), 268 deletions(-) delete mode 100644 src/main/java/com/github/copilot/sdk/json/ToolDef.java diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index 8235c0537..722407c50 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -546,7 +546,7 @@ void registerTools(List tools) { toolHandlers.clear(); if (tools != null) { for (ToolDefinition tool : tools) { - toolHandlers.put(tool.getName(), tool); + toolHandlers.put(tool.name(), tool); } } } diff --git a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java index 21a4c4056..830c731d4 100644 --- a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java +++ b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java @@ -129,7 +129,7 @@ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params } ToolDefinition tool = session.getTool(toolName); - if (tool == null || tool.getHandler() == null) { + if (tool == null || tool.handler() == null) { var result = new ToolResultObject().setTextResultForLlm("Tool '" + toolName + "' is not supported.") .setResultType("failure").setError("tool '" + toolName + "' not supported"); rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); @@ -139,7 +139,7 @@ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params var invocation = new ToolInvocation().setSessionId(sessionId).setToolCallId(toolCallId) .setToolName(toolName).setArguments(arguments); - tool.getHandler().invoke(invocation).thenAccept(result -> { + tool.handler().invoke(invocation).thenAccept(result -> { try { ToolResultObject toolResult; if (result instanceof ToolResultObject tr) { diff --git a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java index 041a04841..a59940bcf 100644 --- a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java +++ b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java @@ -4,13 +4,10 @@ package com.github.copilot.sdk; -import java.util.stream.Collectors; - import com.github.copilot.sdk.json.CreateSessionRequest; import com.github.copilot.sdk.json.ResumeSessionConfig; import com.github.copilot.sdk.json.ResumeSessionRequest; import com.github.copilot.sdk.json.SessionConfig; -import com.github.copilot.sdk.json.ToolDef; /** * Builds JSON-RPC request objects from session configuration. @@ -41,10 +38,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config) { request.setModel(config.getModel()); request.setSessionId(config.getSessionId()); request.setReasoningEffort(config.getReasoningEffort()); - request.setTools(config.getTools() != null - ? config.getTools().stream().map(t -> new ToolDef(t.getName(), t.getDescription(), t.getParameters())) - .collect(Collectors.toList()) - : null); + request.setTools(config.getTools()); request.setSystemMessage(config.getSystemMessage()); request.setAvailableTools(config.getAvailableTools()); request.setExcludedTools(config.getExcludedTools()); @@ -83,10 +77,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setModel(config.getModel()); request.setReasoningEffort(config.getReasoningEffort()); - request.setTools(config.getTools() != null - ? config.getTools().stream().map(t -> new ToolDef(t.getName(), t.getDescription(), t.getParameters())) - .collect(Collectors.toList()) - : null); + request.setTools(config.getTools()); request.setSystemMessage(config.getSystemMessage()); request.setAvailableTools(config.getAvailableTools()); request.setExcludedTools(config.getExcludedTools()); diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java index 328161030..1e1ee621a 100644 --- a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java @@ -35,7 +35,7 @@ public final class CreateSessionRequest { private String reasoningEffort; @JsonProperty("tools") - private List tools; + private List tools; @JsonProperty("systemMessage") private SystemMessageConfig systemMessage; @@ -115,12 +115,12 @@ public void setReasoningEffort(String reasoningEffort) { } /** Gets the tools. @return the tool definitions */ - public List getTools() { + public List getTools() { return tools == null ? null : Collections.unmodifiableList(tools); } /** Sets the tools. @param tools the tool definitions */ - public void setTools(List tools) { + public void setTools(List tools) { this.tools = tools; } diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java index 86effad8f..19c51aef0 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java @@ -36,7 +36,7 @@ public final class ResumeSessionRequest { private String reasoningEffort; @JsonProperty("tools") - private List tools; + private List tools; @JsonProperty("systemMessage") private SystemMessageConfig systemMessage; @@ -119,12 +119,12 @@ public void setReasoningEffort(String reasoningEffort) { } /** Gets the tools. @return the tool definitions */ - public List getTools() { + public List getTools() { return tools == null ? null : Collections.unmodifiableList(tools); } /** Sets the tools. @param tools the tool definitions */ - public void setTools(List tools) { + public void setTools(List tools) { this.tools = tools; } diff --git a/src/main/java/com/github/copilot/sdk/json/ToolDef.java b/src/main/java/com/github/copilot/sdk/json/ToolDef.java deleted file mode 100644 index ac56ce53d..000000000 --- a/src/main/java/com/github/copilot/sdk/json/ToolDef.java +++ /dev/null @@ -1,109 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot.sdk.json; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Low-level tool definition for JSON-RPC communication. - *

- * This is an internal class representing the wire format of a tool. For - * registering tools with the SDK, use {@link ToolDefinition} instead. - * - * @see ToolDefinition - * @since 1.0.0 - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public final class ToolDef { - - @JsonProperty("name") - private String name; - - @JsonProperty("description") - private String description; - - @JsonProperty("parameters") - private Object parameters; - - /** - * Creates an empty tool definition. - */ - public ToolDef() { - } - - /** - * Creates a tool definition with all fields. - * - * @param name - * the unique tool identifier - * @param description - * the tool description - * @param parameters - * the JSON Schema for tool parameters - */ - public ToolDef(String name, String description, Object parameters) { - this.name = name; - this.description = description; - this.parameters = parameters; - } - - /** - * Gets the tool name. - * - * @return the tool name - */ - public String getName() { - return name; - } - - /** - * Sets the tool name. - * - * @param name - * the tool name - */ - public void setName(String name) { - this.name = name; - } - - /** - * Gets the tool description. - * - * @return the tool description - */ - public String getDescription() { - return description; - } - - /** - * Sets the tool description. - * - * @param description - * the tool description - */ - public void setDescription(String description) { - this.description = description; - } - - /** - * Gets the JSON Schema for tool parameters. - * - * @return the parameters schema - */ - public Object getParameters() { - return parameters; - } - - /** - * Sets the JSON Schema for tool parameters. - * - * @param parameters - * the parameters schema - */ - public void setParameters(Object parameters) { - this.parameters = parameters; - } -} diff --git a/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java b/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java index 5d366e981..f81c26d90 100644 --- a/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java +++ b/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java @@ -6,6 +6,7 @@ import java.util.Map; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -39,149 +40,21 @@ * }); * } * + * @param name + * the unique name of the tool + * @param description + * a description of what the tool does + * @param parameters + * the JSON Schema defining the tool's parameters + * @param handler + * the handler function to execute when invoked * @see SessionConfig#setTools(java.util.List) * @see ToolHandler * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public class ToolDefinition { - - @JsonProperty("name") - private String name; - - @JsonProperty("description") - private String description; - - @JsonProperty("parameters") - private Object parameters; - - private transient ToolHandler handler; - - /** - * Creates an empty tool definition. - *

- * Use the setter methods to configure the tool. - */ - public ToolDefinition() { - } - - /** - * Creates a tool definition with all properties. - * - * @param name - * the unique name of the tool - * @param description - * a description of what the tool does - * @param parameters - * the JSON Schema defining the tool's parameters - * @param handler - * the handler function to execute when invoked - */ - public ToolDefinition(String name, String description, Object parameters, ToolHandler handler) { - this.name = name; - this.description = description; - this.parameters = parameters; - this.handler = handler; - } - - /** - * Gets the tool name. - * - * @return the unique name of the tool - */ - public String getName() { - return name; - } - - /** - * Sets the tool name. - *

- * The name should be unique within a session and follow naming conventions - * similar to function names (e.g., "get_user", "search_files"). - * - * @param name - * the unique name of the tool - * @return this tool definition for method chaining - */ - public ToolDefinition setName(String name) { - this.name = name; - return this; - } - - /** - * Gets the tool description. - * - * @return the description of what the tool does - */ - public String getDescription() { - return description; - } - - /** - * Sets the tool description. - *

- * The description helps the AI understand when and how to use the tool. Be - * clear and specific about the tool's purpose and any constraints. - * - * @param description - * the description of what the tool does - * @return this tool definition for method chaining - */ - public ToolDefinition setDescription(String description) { - this.description = description; - return this; - } - - /** - * Gets the parameter schema. - * - * @return the JSON Schema for the tool's parameters - */ - public Object getParameters() { - return parameters; - } - - /** - * Sets the parameter schema. - *

- * The schema should follow JSON Schema format and define the structure of - * arguments the tool accepts. This is typically a {@code Map} with "type", - * "properties", and "required" fields. - * - * @param parameters - * the JSON Schema for the tool's parameters - * @return this tool definition for method chaining - */ - public ToolDefinition setParameters(Object parameters) { - this.parameters = parameters; - return this; - } - - /** - * Gets the tool handler. - * - * @return the handler function that executes when the tool is invoked - */ - public ToolHandler getHandler() { - return handler; - } - - /** - * Sets the tool handler. - *

- * The handler is called when the assistant invokes this tool. It receives a - * {@link ToolInvocation} with the arguments and should return a - * {@code CompletableFuture} with the result. - * - * @param handler - * the handler function - * @return this tool definition for method chaining - * @see ToolHandler - */ - public ToolDefinition setHandler(ToolHandler handler) { - this.handler = handler; - return this; - } +public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("description") String description, + @JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler) { /** * Creates a tool definition with a JSON schema for parameters. diff --git a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java index d322dc8bc..f6de79a0f 100644 --- a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java @@ -80,7 +80,7 @@ void testBuildResumeRequestWithTools() { assertNotNull(request.getTools()); assertEquals(1, request.getTools().size()); - assertEquals("my_tool", request.getTools().get(0).getName()); + assertEquals("my_tool", request.getTools().get(0).name()); } @Test From 6a68d9fde75e0670d1e2892ef9a287aa1ea71f41 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:20:00 +0000 Subject: [PATCH 089/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index d7245adb2..d8962bc1d 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 90.1% - 90.1% + 90.4% + 90.4% From b533336937a5e10c69cbaeba7cbc38dbd4eef64a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:29:09 +0000 Subject: [PATCH 090/147] Initial plan From 2de4006a4f0a95c01a764cf0f8bbc9f07b0d485a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:29:34 +0000 Subject: [PATCH 091/147] Initial plan From d4b564ab56d5d807f403021117b819afdb1f49cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:30:10 +0000 Subject: [PATCH 092/147] Initial plan From c9b4c0006f9b8d4deb62b47832ee4ff19a80b49b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:35:18 +0000 Subject: [PATCH 093/147] Convert ToolResultObject from class to record with factory methods Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../copilot/sdk/RpcHandlerDispatcher.java | 15 +- .../copilot/sdk/json/ToolResultObject.java | 170 ++++++------------ .../copilot/sdk/RpcHandlerDispatcherTest.java | 3 +- 3 files changed, 59 insertions(+), 129 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java index 830c731d4..671aea593 100644 --- a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java +++ b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java @@ -130,8 +130,8 @@ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params ToolDefinition tool = session.getTool(toolName); if (tool == null || tool.handler() == null) { - var result = new ToolResultObject().setTextResultForLlm("Tool '" + toolName + "' is not supported.") - .setResultType("failure").setError("tool '" + toolName + "' not supported"); + var result = ToolResultObject.failure("Tool '" + toolName + "' is not supported.", + "tool '" + toolName + "' not supported"); rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); return; } @@ -145,8 +145,8 @@ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params if (result instanceof ToolResultObject tr) { toolResult = tr; } else { - toolResult = new ToolResultObject().setResultType("success").setTextResultForLlm( - result instanceof String s ? s : MAPPER.writeValueAsString(result)); + toolResult = ToolResultObject + .success(result instanceof String s ? s : MAPPER.writeValueAsString(result)); } rpc.sendResponse(Long.parseLong(requestId), Map.of("result", toolResult)); } catch (Exception e) { @@ -154,10 +154,9 @@ private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params } }).exceptionally(ex -> { try { - var result = new ToolResultObject() - .setTextResultForLlm( - "Invoking this tool produced an error. Detailed information is not available.") - .setResultType("failure").setError(ex.getMessage()); + var result = ToolResultObject.failure( + "Invoking this tool produced an error. Detailed information is not available.", + ex.getMessage()); rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result)); } catch (Exception e) { LOG.log(Level.SEVERE, "Error sending tool error", e); diff --git a/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java b/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java index 72b5291c0..e472d8325 100644 --- a/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java +++ b/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java @@ -4,7 +4,6 @@ package com.github.copilot.sdk.json; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -14,162 +13,95 @@ /** * Result object returned from a tool execution. *

- * This class represents the structured result of a tool invocation, including + * This record represents the structured result of a tool invocation, including * text output, binary data, error information, and telemetry. * *

Example: Success Result

* *
{@code
- * return new ToolResultObject().setResultType("success").setTextResultForLlm("File contents: " + content);
+ * return ToolResultObject.success("File contents: " + content);
  * }
* *

Example: Error Result

* *
{@code
- * return new ToolResultObject().setResultType("error").setError("File not found: " + path);
+ * return ToolResultObject.error("File not found: " + path);
  * }
* + *

Example: Custom Result

+ * + *
{@code
+ * return new ToolResultObject("success", "Result text", null, null, null, null);
+ * }
+ * + * @param resultType + * the result type ("success" or "error"), defaults to "success" + * @param textResultForLlm + * the text result to be sent to the LLM + * @param binaryResultsForLlm + * the list of binary results to be sent to the LLM + * @param error + * the error message, or {@code null} if successful + * @param sessionLog + * the session log text + * @param toolTelemetry + * the tool telemetry data * @see ToolHandler * @see ToolBinaryResult * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public final class ToolResultObject { - - @JsonProperty("textResultForLlm") - private String textResultForLlm; - - @JsonProperty("binaryResultsForLlm") - private List binaryResultsForLlm; - - @JsonProperty("resultType") - private String resultType = "success"; - - @JsonProperty("error") - private String error; - - @JsonProperty("sessionLog") - private String sessionLog; - - @JsonProperty("toolTelemetry") - private Map toolTelemetry; +public record ToolResultObject(@JsonProperty("resultType") String resultType, + @JsonProperty("textResultForLlm") String textResultForLlm, + @JsonProperty("binaryResultsForLlm") List binaryResultsForLlm, + @JsonProperty("error") String error, @JsonProperty("sessionLog") String sessionLog, + @JsonProperty("toolTelemetry") Map toolTelemetry) { /** - * Gets the text result to be sent to the LLM. - * - * @return the text result - */ - public String getTextResultForLlm() { - return textResultForLlm; - } - - /** - * Sets the text result to be sent to the LLM. + * Creates a success result with the given text. * * @param textResultForLlm - * the text result - * @return this result for method chaining + * the text result to be sent to the LLM + * @return a success result */ - public ToolResultObject setTextResultForLlm(String textResultForLlm) { - this.textResultForLlm = textResultForLlm; - return this; + public static ToolResultObject success(String textResultForLlm) { + return new ToolResultObject("success", textResultForLlm, null, null, null, null); } /** - * Gets the binary results to be sent to the LLM. - * - * @return the list of binary results - */ - public List getBinaryResultsForLlm() { - return binaryResultsForLlm == null ? null : Collections.unmodifiableList(binaryResultsForLlm); - } - - /** - * Sets binary results (images, files) to be sent to the LLM. - * - * @param binaryResultsForLlm - * the list of binary results - * @return this result for method chaining - */ - public ToolResultObject setBinaryResultsForLlm(List binaryResultsForLlm) { - this.binaryResultsForLlm = binaryResultsForLlm; - return this; - } - - /** - * Gets the result type. - * - * @return the result type ("success" or "error") - */ - public String getResultType() { - return resultType; - } - - /** - * Sets the result type. - * - * @param resultType - * "success" or "error" - * @return this result for method chaining - */ - public ToolResultObject setResultType(String resultType) { - this.resultType = resultType; - return this; - } - - /** - * Gets the error message. - * - * @return the error message, or {@code null} if successful - */ - public String getError() { - return error; - } - - /** - * Sets an error message for failed tool execution. + * Creates an error result with the given error message. * * @param error * the error message - * @return this result for method chaining + * @return an error result */ - public ToolResultObject setError(String error) { - this.error = error; - return this; + public static ToolResultObject error(String error) { + return new ToolResultObject("error", "An error occurred.", null, error, null, null); } /** - * Gets the session log entry. + * Creates an error result with both a text result and error message. * - * @return the session log text - */ - public String getSessionLog() { - return sessionLog; - } - - /** - * Sets a log entry to be recorded in the session. - * - * @param sessionLog - * the log entry - * @return this result for method chaining + * @param textResultForLlm + * the text result to be sent to the LLM + * @param error + * the error message + * @return an error result */ - public ToolResultObject setSessionLog(String sessionLog) { - this.sessionLog = sessionLog; - return this; + public static ToolResultObject error(String textResultForLlm, String error) { + return new ToolResultObject("error", textResultForLlm, null, error, null, null); } /** - * Gets the tool telemetry data. + * Creates a failure result with the given text and error message. * - * @return the telemetry map + * @param textResultForLlm + * the text result to be sent to the LLM + * @param error + * the error message + * @return a failure result */ - public Map getToolTelemetry() { - return toolTelemetry == null ? null : Collections.unmodifiableMap(toolTelemetry); - } - - public ToolResultObject setToolTelemetry(Map toolTelemetry) { - this.toolTelemetry = toolTelemetry; - return this; + public static ToolResultObject failure(String textResultForLlm, String error) { + return new ToolResultObject("failure", textResultForLlm, null, error, null, null); } } diff --git a/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java b/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java index a5ae8e443..041158b76 100644 --- a/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java +++ b/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java @@ -232,8 +232,7 @@ void toolCallWithUnknownTool() throws Exception { void toolCallReturnsToolResultObjectDirectly() throws Exception { CopilotSession session = createSession("s1"); var tool = ToolDefinition.create("my_tool", "A test tool", Map.of("type", "object"), - invocation -> CompletableFuture.completedFuture( - new ToolResultObject().setResultType("success").setTextResultForLlm("direct result"))); + invocation -> CompletableFuture.completedFuture(ToolResultObject.success("direct result"))); session.registerTools(List.of(tool)); ObjectNode params = MAPPER.createObjectNode(); From 1399379c3cf758431f286537789fd8652f008253 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:36:15 +0000 Subject: [PATCH 094/147] Address code review feedback: improve error() method and document failure() vs error() Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../java/com/github/copilot/sdk/json/ToolResultObject.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java b/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java index e472d8325..dcb5ad78f 100644 --- a/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java +++ b/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java @@ -76,7 +76,7 @@ public static ToolResultObject success(String textResultForLlm) { * @return an error result */ public static ToolResultObject error(String error) { - return new ToolResultObject("error", "An error occurred.", null, error, null, null); + return new ToolResultObject("error", null, null, error, null, null); } /** @@ -94,6 +94,10 @@ public static ToolResultObject error(String textResultForLlm, String error) { /** * Creates a failure result with the given text and error message. + *

+ * The "failure" result type indicates that the tool execution itself failed + * (e.g., tool not found), while "error" indicates the tool executed but + * encountered an error during processing. * * @param textResultForLlm * the text result to be sent to the LLM From b58904bc48ea34d89862984787b69fd42b91264e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:39:29 +0000 Subject: [PATCH 095/147] Convert PreToolUseHookOutput to record Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../sdk/json/PreToolUseHookOutput.java | 136 ++---------------- .../github/copilot/sdk/json/SessionHooks.java | 2 +- src/site/markdown/advanced.md | 2 +- src/site/markdown/hooks.md | 29 ++-- .../com/github/copilot/sdk/HooksTest.java | 6 +- .../copilot/sdk/RpcHandlerDispatcherTest.java | 2 +- 6 files changed, 31 insertions(+), 146 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/json/PreToolUseHookOutput.java b/src/main/java/com/github/copilot/sdk/json/PreToolUseHookOutput.java index da3e41257..894b7ac55 100644 --- a/src/main/java/com/github/copilot/sdk/json/PreToolUseHookOutput.java +++ b/src/main/java/com/github/copilot/sdk/json/PreToolUseHookOutput.java @@ -11,128 +11,22 @@ /** * Output for a pre-tool-use hook. * + * @param permissionDecision + * "allow", "deny", or "ask" + * @param permissionDecisionReason + * the reason for the permission decision + * @param modifiedArgs + * the modified tool arguments, or {@code null} to use original + * @param additionalContext + * additional context to provide to the model + * @param suppressOutput + * {@code true} to suppress output * @since 1.0.6 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public class PreToolUseHookOutput { - - @JsonProperty("permissionDecision") - private String permissionDecision; - - @JsonProperty("permissionDecisionReason") - private String permissionDecisionReason; - - @JsonProperty("modifiedArgs") - private JsonNode modifiedArgs; - - @JsonProperty("additionalContext") - private String additionalContext; - - @JsonProperty("suppressOutput") - private Boolean suppressOutput; - - /** - * Gets the permission decision. - * - * @return "allow", "deny", or "ask" - */ - public String getPermissionDecision() { - return permissionDecision; - } - - /** - * Sets the permission decision. - * - * @param permissionDecision - * "allow", "deny", or "ask" - * @return this instance for method chaining - */ - public PreToolUseHookOutput setPermissionDecision(String permissionDecision) { - this.permissionDecision = permissionDecision; - return this; - } - - /** - * Gets the reason for the permission decision. - * - * @return the reason text - */ - public String getPermissionDecisionReason() { - return permissionDecisionReason; - } - - /** - * Sets the reason for the permission decision. - * - * @param permissionDecisionReason - * the reason text - * @return this instance for method chaining - */ - public PreToolUseHookOutput setPermissionDecisionReason(String permissionDecisionReason) { - this.permissionDecisionReason = permissionDecisionReason; - return this; - } - - /** - * Gets the modified tool arguments. - * - * @return the modified arguments, or {@code null} to use original - */ - public JsonNode getModifiedArgs() { - return modifiedArgs; - } - - /** - * Sets the modified tool arguments. - * - * @param modifiedArgs - * the modified arguments - * @return this instance for method chaining - */ - public PreToolUseHookOutput setModifiedArgs(JsonNode modifiedArgs) { - this.modifiedArgs = modifiedArgs; - return this; - } - - /** - * Gets additional context to provide to the model. - * - * @return the additional context - */ - public String getAdditionalContext() { - return additionalContext; - } - - /** - * Sets additional context to provide to the model. - * - * @param additionalContext - * the additional context - * @return this instance for method chaining - */ - public PreToolUseHookOutput setAdditionalContext(String additionalContext) { - this.additionalContext = additionalContext; - return this; - } - - /** - * Returns whether to suppress output. - * - * @return {@code true} to suppress output - */ - public Boolean getSuppressOutput() { - return suppressOutput; - } - - /** - * Sets whether to suppress output. - * - * @param suppressOutput - * {@code true} to suppress output - * @return this instance for method chaining - */ - public PreToolUseHookOutput setSuppressOutput(Boolean suppressOutput) { - this.suppressOutput = suppressOutput; - return this; - } +public record PreToolUseHookOutput(@JsonProperty("permissionDecision") String permissionDecision, + @JsonProperty("permissionDecisionReason") String permissionDecisionReason, + @JsonProperty("modifiedArgs") JsonNode modifiedArgs, + @JsonProperty("additionalContext") String additionalContext, + @JsonProperty("suppressOutput") Boolean suppressOutput) { } diff --git a/src/main/java/com/github/copilot/sdk/json/SessionHooks.java b/src/main/java/com/github/copilot/sdk/json/SessionHooks.java index b44a91b76..c2344e823 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionHooks.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionHooks.java @@ -15,7 +15,7 @@ *

{@code
  * var hooks = new SessionHooks().setOnPreToolUse((input, invocation) -> {
  * 	System.out.println("Tool being called: " + input.getToolName());
- * 	return CompletableFuture.completedFuture(new PreToolUseHookOutput().setPermissionDecision("allow"));
+ * 	return CompletableFuture.completedFuture(new PreToolUseHookOutput("allow", null, null, null, null));
  * }).setOnPostToolUse((input, invocation) -> {
  * 	System.out.println("Tool result: " + input.getToolResult());
  * 	return CompletableFuture.completedFuture(null);
diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md
index ff7179bb8..fb97cf53f 100644
--- a/src/site/markdown/advanced.md
+++ b/src/site/markdown/advanced.md
@@ -365,7 +365,7 @@ var hooks = new SessionHooks()
     .setOnPreToolUse((input, invocation) -> {
         System.out.println("Tool: " + input.getToolName());
         return CompletableFuture.completedFuture(
-            new PreToolUseHookOutput().setPermissionDecision("allow")
+            new PreToolUseHookOutput("allow", null, null, null, null)
         );
     })
     .setOnPostToolUse((input, invocation) -> {
diff --git a/src/site/markdown/hooks.md b/src/site/markdown/hooks.md
index 78f1c3cb2..543713602 100644
--- a/src/site/markdown/hooks.md
+++ b/src/site/markdown/hooks.md
@@ -27,7 +27,7 @@ var hooks = new SessionHooks()
     .setOnPreToolUse((input, invocation) -> {
         System.out.println("Tool: " + input.getToolName());
         return CompletableFuture.completedFuture(
-            new PreToolUseHookOutput().setPermissionDecision("allow")
+            new PreToolUseHookOutput("allow", null, null, null, null)
         );
     })
     .setOnPostToolUse((input, invocation) -> {
@@ -83,23 +83,20 @@ var hooks = new SessionHooks()
         // Block file deletion
         if (tool.equals("delete_file")) {
             return CompletableFuture.completedFuture(
-                new PreToolUseHookOutput()
-                    .setPermissionDecision("deny")
-                    .setPermissionDecisionReason("File deletion is not allowed")
+                new PreToolUseHookOutput("deny", "File deletion is not allowed", null, null, null)
             );
         }
         
         // Require confirmation for shell commands
         if (tool.equals("run_terminal_cmd")) {
             return CompletableFuture.completedFuture(
-                new PreToolUseHookOutput()
-                    .setPermissionDecision("ask")
+                new PreToolUseHookOutput("ask", null, null, null, null)
             );
         }
         
         // Allow everything else
         return CompletableFuture.completedFuture(
-            new PreToolUseHookOutput().setPermissionDecision("allow")
+            new PreToolUseHookOutput("allow", null, null, null, null)
         );
     });
 ```
@@ -119,13 +116,11 @@ var hooks = new SessionHooks()
             modifiedArgs.set("query", input.getToolArgs().get("query"));
             
             return CompletableFuture.completedFuture(
-                new PreToolUseHookOutput()
-                    .setPermissionDecision("allow")
-                    .setModifiedArgs(modifiedArgs)
+                new PreToolUseHookOutput("allow", null, modifiedArgs, null, null)
             );
         }
         return CompletableFuture.completedFuture(
-            new PreToolUseHookOutput().setPermissionDecision("allow")
+            new PreToolUseHookOutput("allow", null, null, null, null)
         );
     });
 ```
@@ -315,14 +310,12 @@ public class HooksExample {
                     // Deny dangerous operations
                     if (input.getToolName().contains("delete")) {
                         return CompletableFuture.completedFuture(
-                            new PreToolUseHookOutput()
-                                .setPermissionDecision("deny")
-                                .setPermissionDecisionReason("Deletion not allowed")
+                            new PreToolUseHookOutput("deny", "Deletion not allowed", null, null, null)
                         );
                     }
                     
                     return CompletableFuture.completedFuture(
-                        new PreToolUseHookOutput().setPermissionDecision("allow")
+                        new PreToolUseHookOutput("allow", null, null, null, null)
                     );
                 })
                 
@@ -391,15 +384,13 @@ To handle errors gracefully in your hooks:
     try {
         // Your logic here
         return CompletableFuture.completedFuture(
-            new PreToolUseHookOutput().setPermissionDecision("allow")
+            new PreToolUseHookOutput("allow", null, null, null, null)
         );
     } catch (Exception e) {
         logger.error("Hook error", e);
         // Fail-safe: deny if something goes wrong
         return CompletableFuture.completedFuture(
-            new PreToolUseHookOutput()
-                .setPermissionDecision("deny")
-                .setPermissionDecisionReason("Internal error")
+            new PreToolUseHookOutput("deny", "Internal error", null, null, null)
         );
     }
 })
diff --git a/src/test/java/com/github/copilot/sdk/HooksTest.java b/src/test/java/com/github/copilot/sdk/HooksTest.java
index af440632d..b4dde7068 100644
--- a/src/test/java/com/github/copilot/sdk/HooksTest.java
+++ b/src/test/java/com/github/copilot/sdk/HooksTest.java
@@ -70,7 +70,7 @@ void testInvokePreToolUseHookWhenModelRunsATool() throws Exception {
         var config = new SessionConfig().setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> {
             preToolUseInputs.add(input);
             assertEquals(sessionIdHolder[0], invocation.getSessionId());
-            return CompletableFuture.completedFuture(new PreToolUseHookOutput().setPermissionDecision("allow"));
+            return CompletableFuture.completedFuture(new PreToolUseHookOutput("allow", null, null, null, null));
         }));
 
         try (CopilotClient client = ctx.createClient()) {
@@ -149,7 +149,7 @@ void testInvokeBothHooksForSingleToolCall() throws Exception {
 
         var config = new SessionConfig().setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> {
             preToolUseInputs.add(input);
-            return CompletableFuture.completedFuture(new PreToolUseHookOutput().setPermissionDecision("allow"));
+            return CompletableFuture.completedFuture(new PreToolUseHookOutput("allow", null, null, null, null));
         }).setOnPostToolUse((input, invocation) -> {
             postToolUseInputs.add(input);
             return CompletableFuture.completedFuture(null);
@@ -195,7 +195,7 @@ void testDenyToolExecutionWhenPreToolUseReturnsDeny() throws Exception {
         var config = new SessionConfig().setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> {
             preToolUseInputs.add(input);
             // Deny all tool calls
-            return CompletableFuture.completedFuture(new PreToolUseHookOutput().setPermissionDecision("deny"));
+            return CompletableFuture.completedFuture(new PreToolUseHookOutput("deny", null, null, null, null));
         }));
 
         try (CopilotClient client = ctx.createClient()) {
diff --git a/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java b/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java
index a5ae8e443..a1c6d4fed 100644
--- a/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java
+++ b/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java
@@ -469,7 +469,7 @@ void hooksInvokeWithNullOutput() throws Exception {
     void hooksInvokeWithNonNullOutput() throws Exception {
         CopilotSession session = createSession("s1");
         session.registerHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> CompletableFuture
-                .completedFuture(new PreToolUseHookOutput().setPermissionDecision("allow"))));
+                .completedFuture(new PreToolUseHookOutput("allow", null, null, null, null))));
 
         ObjectNode params = MAPPER.createObjectNode();
         params.put("sessionId", "s1");

From 432ab8a65ac6e7fa9e16489f86a64eff4182c595 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 04:41:32 +0000
Subject: [PATCH 096/147] Add static factory methods to PreToolUseHookOutput
 record

Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>
---
 .../sdk/json/PreToolUseHookOutput.java        | 52 +++++++++++++++++++
 .../github/copilot/sdk/json/SessionHooks.java |  2 +-
 src/site/markdown/advanced.md                 |  4 +-
 src/site/markdown/hooks.md                    | 32 ++++--------
 .../com/github/copilot/sdk/HooksTest.java     |  6 +--
 .../copilot/sdk/RpcHandlerDispatcherTest.java |  4 +-
 6 files changed, 69 insertions(+), 31 deletions(-)

diff --git a/src/main/java/com/github/copilot/sdk/json/PreToolUseHookOutput.java b/src/main/java/com/github/copilot/sdk/json/PreToolUseHookOutput.java
index 894b7ac55..a92c8f01a 100644
--- a/src/main/java/com/github/copilot/sdk/json/PreToolUseHookOutput.java
+++ b/src/main/java/com/github/copilot/sdk/json/PreToolUseHookOutput.java
@@ -29,4 +29,56 @@ public record PreToolUseHookOutput(@JsonProperty("permissionDecision") String pe
         @JsonProperty("modifiedArgs") JsonNode modifiedArgs,
         @JsonProperty("additionalContext") String additionalContext,
         @JsonProperty("suppressOutput") Boolean suppressOutput) {
+
+    /**
+     * Creates an output that allows the tool to execute.
+     *
+     * @return a new PreToolUseHookOutput with permission decision "allow"
+     */
+    public static PreToolUseHookOutput allow() {
+        return new PreToolUseHookOutput("allow", null, null, null, null);
+    }
+
+    /**
+     * Creates an output that denies the tool execution.
+     *
+     * @return a new PreToolUseHookOutput with permission decision "deny"
+     */
+    public static PreToolUseHookOutput deny() {
+        return new PreToolUseHookOutput("deny", null, null, null, null);
+    }
+
+    /**
+     * Creates an output that denies the tool execution with a reason.
+     *
+     * @param reason
+     *            the reason for denying the tool execution
+     * @return a new PreToolUseHookOutput with permission decision "deny" and reason
+     */
+    public static PreToolUseHookOutput deny(String reason) {
+        return new PreToolUseHookOutput("deny", reason, null, null, null);
+    }
+
+    /**
+     * Creates an output that asks for user confirmation before executing the tool.
+     *
+     * @return a new PreToolUseHookOutput with permission decision "ask"
+     */
+    public static PreToolUseHookOutput ask() {
+        return new PreToolUseHookOutput("ask", null, null, null, null);
+    }
+
+    /**
+     * Creates an output with modified tool arguments.
+     *
+     * @param permissionDecision
+     *            "allow", "deny", or "ask"
+     * @param modifiedArgs
+     *            the modified tool arguments
+     * @return a new PreToolUseHookOutput with the specified permission and modified
+     *         arguments
+     */
+    public static PreToolUseHookOutput withModifiedArgs(String permissionDecision, JsonNode modifiedArgs) {
+        return new PreToolUseHookOutput(permissionDecision, null, modifiedArgs, null, null);
+    }
 }
diff --git a/src/main/java/com/github/copilot/sdk/json/SessionHooks.java b/src/main/java/com/github/copilot/sdk/json/SessionHooks.java
index c2344e823..8e22c3ee8 100644
--- a/src/main/java/com/github/copilot/sdk/json/SessionHooks.java
+++ b/src/main/java/com/github/copilot/sdk/json/SessionHooks.java
@@ -15,7 +15,7 @@
  * 
{@code
  * var hooks = new SessionHooks().setOnPreToolUse((input, invocation) -> {
  * 	System.out.println("Tool being called: " + input.getToolName());
- * 	return CompletableFuture.completedFuture(new PreToolUseHookOutput("allow", null, null, null, null));
+ * 	return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
  * }).setOnPostToolUse((input, invocation) -> {
  * 	System.out.println("Tool result: " + input.getToolResult());
  * 	return CompletableFuture.completedFuture(null);
diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md
index fb97cf53f..c540bf53c 100644
--- a/src/site/markdown/advanced.md
+++ b/src/site/markdown/advanced.md
@@ -364,9 +364,7 @@ Intercept tool execution and session lifecycle events using hooks.
 var hooks = new SessionHooks()
     .setOnPreToolUse((input, invocation) -> {
         System.out.println("Tool: " + input.getToolName());
-        return CompletableFuture.completedFuture(
-            new PreToolUseHookOutput("allow", null, null, null, null)
-        );
+        return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
     })
     .setOnPostToolUse((input, invocation) -> {
         System.out.println("Result: " + input.getToolResult());
diff --git a/src/site/markdown/hooks.md b/src/site/markdown/hooks.md
index 543713602..58b0bc73e 100644
--- a/src/site/markdown/hooks.md
+++ b/src/site/markdown/hooks.md
@@ -26,9 +26,7 @@ Register hooks when creating a session:
 var hooks = new SessionHooks()
     .setOnPreToolUse((input, invocation) -> {
         System.out.println("Tool: " + input.getToolName());
-        return CompletableFuture.completedFuture(
-            new PreToolUseHookOutput("allow", null, null, null, null)
-        );
+        return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
     })
     .setOnPostToolUse((input, invocation) -> {
         System.out.println("Result: " + input.getToolResult());
@@ -83,21 +81,17 @@ var hooks = new SessionHooks()
         // Block file deletion
         if (tool.equals("delete_file")) {
             return CompletableFuture.completedFuture(
-                new PreToolUseHookOutput("deny", "File deletion is not allowed", null, null, null)
+                PreToolUseHookOutput.deny("File deletion is not allowed")
             );
         }
         
         // Require confirmation for shell commands
         if (tool.equals("run_terminal_cmd")) {
-            return CompletableFuture.completedFuture(
-                new PreToolUseHookOutput("ask", null, null, null, null)
-            );
+            return CompletableFuture.completedFuture(PreToolUseHookOutput.ask());
         }
         
         // Allow everything else
-        return CompletableFuture.completedFuture(
-            new PreToolUseHookOutput("allow", null, null, null, null)
-        );
+        return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
     });
 ```
 
@@ -116,12 +110,10 @@ var hooks = new SessionHooks()
             modifiedArgs.set("query", input.getToolArgs().get("query"));
             
             return CompletableFuture.completedFuture(
-                new PreToolUseHookOutput("allow", null, modifiedArgs, null, null)
+                PreToolUseHookOutput.withModifiedArgs("allow", modifiedArgs)
             );
         }
-        return CompletableFuture.completedFuture(
-            new PreToolUseHookOutput("allow", null, null, null, null)
-        );
+        return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
     });
 ```
 
@@ -310,13 +302,11 @@ public class HooksExample {
                     // Deny dangerous operations
                     if (input.getToolName().contains("delete")) {
                         return CompletableFuture.completedFuture(
-                            new PreToolUseHookOutput("deny", "Deletion not allowed", null, null, null)
+                            PreToolUseHookOutput.deny("Deletion not allowed")
                         );
                     }
                     
-                    return CompletableFuture.completedFuture(
-                        new PreToolUseHookOutput("allow", null, null, null, null)
-                    );
+                    return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
                 })
                 
                 // Logging: track tool results
@@ -383,14 +373,12 @@ To handle errors gracefully in your hooks:
 .setOnPreToolUse((input, invocation) -> {
     try {
         // Your logic here
-        return CompletableFuture.completedFuture(
-            new PreToolUseHookOutput("allow", null, null, null, null)
-        );
+        return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
     } catch (Exception e) {
         logger.error("Hook error", e);
         // Fail-safe: deny if something goes wrong
         return CompletableFuture.completedFuture(
-            new PreToolUseHookOutput("deny", "Internal error", null, null, null)
+            PreToolUseHookOutput.deny("Internal error")
         );
     }
 })
diff --git a/src/test/java/com/github/copilot/sdk/HooksTest.java b/src/test/java/com/github/copilot/sdk/HooksTest.java
index b4dde7068..c728b9b99 100644
--- a/src/test/java/com/github/copilot/sdk/HooksTest.java
+++ b/src/test/java/com/github/copilot/sdk/HooksTest.java
@@ -70,7 +70,7 @@ void testInvokePreToolUseHookWhenModelRunsATool() throws Exception {
         var config = new SessionConfig().setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> {
             preToolUseInputs.add(input);
             assertEquals(sessionIdHolder[0], invocation.getSessionId());
-            return CompletableFuture.completedFuture(new PreToolUseHookOutput("allow", null, null, null, null));
+            return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
         }));
 
         try (CopilotClient client = ctx.createClient()) {
@@ -149,7 +149,7 @@ void testInvokeBothHooksForSingleToolCall() throws Exception {
 
         var config = new SessionConfig().setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> {
             preToolUseInputs.add(input);
-            return CompletableFuture.completedFuture(new PreToolUseHookOutput("allow", null, null, null, null));
+            return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
         }).setOnPostToolUse((input, invocation) -> {
             postToolUseInputs.add(input);
             return CompletableFuture.completedFuture(null);
@@ -195,7 +195,7 @@ void testDenyToolExecutionWhenPreToolUseReturnsDeny() throws Exception {
         var config = new SessionConfig().setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> {
             preToolUseInputs.add(input);
             // Deny all tool calls
-            return CompletableFuture.completedFuture(new PreToolUseHookOutput("deny", null, null, null, null));
+            return CompletableFuture.completedFuture(PreToolUseHookOutput.deny());
         }));
 
         try (CopilotClient client = ctx.createClient()) {
diff --git a/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java b/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java
index a1c6d4fed..3778a4a24 100644
--- a/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java
+++ b/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java
@@ -468,8 +468,8 @@ void hooksInvokeWithNullOutput() throws Exception {
     @Test
     void hooksInvokeWithNonNullOutput() throws Exception {
         CopilotSession session = createSession("s1");
-        session.registerHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> CompletableFuture
-                .completedFuture(new PreToolUseHookOutput("allow", null, null, null, null))));
+        session.registerHooks(new SessionHooks().setOnPreToolUse(
+                (input, invocation) -> CompletableFuture.completedFuture(PreToolUseHookOutput.allow())));
 
         ObjectNode params = MAPPER.createObjectNode();
         params.put("sessionId", "s1");

From 48aadc4691063aba945ced206bf25e13088e611d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 04:46:08 +0000
Subject: [PATCH 097/147] Add ToolInvocationTest to achieve 100% coverage

Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>
---
 .../copilot/sdk/ToolInvocationTest.java       | 182 ++++++++++++++++++
 1 file changed, 182 insertions(+)
 create mode 100644 src/test/java/com/github/copilot/sdk/ToolInvocationTest.java

diff --git a/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java b/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java
new file mode 100644
index 000000000..445862dcd
--- /dev/null
+++ b/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java
@@ -0,0 +1,182 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.copilot.sdk.json.ToolInvocation;
+
+/**
+ * Unit tests for {@link ToolInvocation}.
+ * 

+ * Tests getter methods, type-safe deserialization, and null handling + * to improve coverage beyond what E2E tests exercise. + */ +public class ToolInvocationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * Test all basic getters return values set via setters. + */ + @Test + void testGettersReturnSetValues() { + ToolInvocation invocation = new ToolInvocation() + .setSessionId("test-session-123") + .setToolCallId("call_abc123") + .setToolName("test_tool"); + + assertEquals("test-session-123", invocation.getSessionId()); + assertEquals("call_abc123", invocation.getToolCallId()); + assertEquals("test_tool", invocation.getToolName()); + } + + /** + * Test getArguments returns null when no arguments are set. + */ + @Test + void testGetArgumentsWhenNull() { + ToolInvocation invocation = new ToolInvocation(); + assertNull(invocation.getArguments(), "getArguments should return null when argumentsNode is null"); + } + + /** + * Test getArguments returns a Map when arguments are set. + */ + @Test + void testGetArgumentsReturnsMap() { + ToolInvocation invocation = new ToolInvocation(); + + // Create a JsonNode with some arguments + ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); + argsNode.put("location", "San Francisco"); + argsNode.put("units", "celsius"); + + invocation.setArguments(argsNode); + + var args = invocation.getArguments(); + assertNotNull(args); + assertEquals("San Francisco", args.get("location")); + assertEquals("celsius", args.get("units")); + } + + /** + * Test getArgumentsAs deserializes to a record type. + */ + @Test + void testGetArgumentsAsWithRecord() { + ToolInvocation invocation = new ToolInvocation(); + + // Create a JsonNode with weather arguments + ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); + argsNode.put("city", "Paris"); + argsNode.put("units", "metric"); + + invocation.setArguments(argsNode); + + // Deserialize to record + WeatherArgs args = invocation.getArgumentsAs(WeatherArgs.class); + assertNotNull(args); + assertEquals("Paris", args.city()); + assertEquals("metric", args.units()); + } + + /** + * Test getArgumentsAs deserializes to a POJO. + */ + @Test + void testGetArgumentsAsWithPojo() { + ToolInvocation invocation = new ToolInvocation(); + + // Create a JsonNode with user data + ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); + argsNode.put("username", "alice"); + argsNode.put("age", 30); + + invocation.setArguments(argsNode); + + // Deserialize to POJO + UserData userData = invocation.getArgumentsAs(UserData.class); + assertNotNull(userData); + assertEquals("alice", userData.getUsername()); + assertEquals(30, userData.getAge()); + } + + /** + * Test getArgumentsAs throws IllegalArgumentException on deserialization failure. + */ + @Test + void testGetArgumentsAsThrowsOnInvalidType() { + ToolInvocation invocation = new ToolInvocation(); + + // Create invalid JSON for the target type (missing required field) + ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); + argsNode.put("invalid_field", "value"); + + invocation.setArguments(argsNode); + + // Try to deserialize to a type that doesn't match + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> invocation.getArgumentsAs(StrictType.class), + "Should throw IllegalArgumentException for invalid deserialization" + ); + + assertTrue(exception.getMessage().contains("Failed to deserialize arguments")); + assertTrue(exception.getMessage().contains("StrictType")); + } + + /** + * Record for testing type-safe argument deserialization. + */ + record WeatherArgs(String city, String units) {} + + /** + * POJO for testing type-safe argument deserialization. + */ + public static class UserData { + private String username; + private int age; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + } + + /** + * Strict type with constructor that throws, for testing error handling. + */ + public static class StrictType { + private final String requiredField; + + public StrictType(String requiredField) { + if (requiredField == null) { + throw new IllegalArgumentException("requiredField cannot be null"); + } + this.requiredField = requiredField; + } + + public String getRequiredField() { + return requiredField; + } + } +} From 1ee5ce7ca9aa9617ac343681ae05c3fd10758e8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:46:48 +0000 Subject: [PATCH 098/147] Remove unused MAPPER field from ToolInvocationTest Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- src/test/java/com/github/copilot/sdk/ToolInvocationTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java b/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java index 445862dcd..2afe39c45 100644 --- a/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java +++ b/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java @@ -8,7 +8,6 @@ import org.junit.jupiter.api.Test; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.copilot.sdk.json.ToolInvocation; @@ -21,8 +20,6 @@ */ public class ToolInvocationTest { - private static final ObjectMapper MAPPER = new ObjectMapper(); - /** * Test all basic getters return values set via setters. */ From c73eab72da219830d29a0eabd7a24b8370f5ceee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:54:58 +0000 Subject: [PATCH 099/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index d8962bc1d..9382d5f37 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 90.4% - 90.4% + 90.3% + 90.3% From 34e5ea133d96ce8fa48cca93deddb2bc94260849 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:57:20 +0000 Subject: [PATCH 100/147] Initial plan From 43221d3941d9354518f8f3c1d54a97b19d922034 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:02:32 +0000 Subject: [PATCH 101/147] Fix Spotless formatting violations in ToolInvocationTest.java Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../copilot/sdk/ToolInvocationTest.java | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java b/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java index 2afe39c45..3dcbf7c9a 100644 --- a/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java +++ b/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java @@ -15,8 +15,8 @@ /** * Unit tests for {@link ToolInvocation}. *

- * Tests getter methods, type-safe deserialization, and null handling - * to improve coverage beyond what E2E tests exercise. + * Tests getter methods, type-safe deserialization, and null handling to improve + * coverage beyond what E2E tests exercise. */ public class ToolInvocationTest { @@ -25,9 +25,7 @@ public class ToolInvocationTest { */ @Test void testGettersReturnSetValues() { - ToolInvocation invocation = new ToolInvocation() - .setSessionId("test-session-123") - .setToolCallId("call_abc123") + ToolInvocation invocation = new ToolInvocation().setSessionId("test-session-123").setToolCallId("call_abc123") .setToolName("test_tool"); assertEquals("test-session-123", invocation.getSessionId()); @@ -50,14 +48,14 @@ void testGetArgumentsWhenNull() { @Test void testGetArgumentsReturnsMap() { ToolInvocation invocation = new ToolInvocation(); - + // Create a JsonNode with some arguments ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); argsNode.put("location", "San Francisco"); argsNode.put("units", "celsius"); - + invocation.setArguments(argsNode); - + var args = invocation.getArguments(); assertNotNull(args); assertEquals("San Francisco", args.get("location")); @@ -70,14 +68,14 @@ void testGetArgumentsReturnsMap() { @Test void testGetArgumentsAsWithRecord() { ToolInvocation invocation = new ToolInvocation(); - + // Create a JsonNode with weather arguments ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); argsNode.put("city", "Paris"); argsNode.put("units", "metric"); - + invocation.setArguments(argsNode); - + // Deserialize to record WeatherArgs args = invocation.getArgumentsAs(WeatherArgs.class); assertNotNull(args); @@ -91,14 +89,14 @@ void testGetArgumentsAsWithRecord() { @Test void testGetArgumentsAsWithPojo() { ToolInvocation invocation = new ToolInvocation(); - + // Create a JsonNode with user data ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); argsNode.put("username", "alice"); argsNode.put("age", 30); - + invocation.setArguments(argsNode); - + // Deserialize to POJO UserData userData = invocation.getArgumentsAs(UserData.class); assertNotNull(userData); @@ -107,25 +105,24 @@ void testGetArgumentsAsWithPojo() { } /** - * Test getArgumentsAs throws IllegalArgumentException on deserialization failure. + * Test getArgumentsAs throws IllegalArgumentException on deserialization + * failure. */ @Test void testGetArgumentsAsThrowsOnInvalidType() { ToolInvocation invocation = new ToolInvocation(); - + // Create invalid JSON for the target type (missing required field) ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); argsNode.put("invalid_field", "value"); - + invocation.setArguments(argsNode); - + // Try to deserialize to a type that doesn't match - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> invocation.getArgumentsAs(StrictType.class), - "Should throw IllegalArgumentException for invalid deserialization" - ); - + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> invocation.getArgumentsAs(StrictType.class), + "Should throw IllegalArgumentException for invalid deserialization"); + assertTrue(exception.getMessage().contains("Failed to deserialize arguments")); assertTrue(exception.getMessage().contains("StrictType")); } @@ -133,7 +130,8 @@ void testGetArgumentsAsThrowsOnInvalidType() { /** * Record for testing type-safe argument deserialization. */ - record WeatherArgs(String city, String units) {} + record WeatherArgs(String city, String units) { + } /** * POJO for testing type-safe argument deserialization. From 972b4a0f290ccfb5345fcb4000fe228322ce3784 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:18:14 +0000 Subject: [PATCH 102/147] Initial plan From 4e903fa3f5605b5e807801a0fce613e63bfc3540 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:20:53 +0000 Subject: [PATCH 103/147] Fix AssistantUsageData.quotaSnapshots() to return empty map instead of null Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../sdk/events/AssistantUsageEvent.java | 2 +- .../copilot/sdk/SessionEventParserTest.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java b/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java index a3c38b03a..6e51064e6 100644 --- a/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AssistantUsageEvent.java @@ -47,7 +47,7 @@ public record AssistantUsageData(@JsonProperty("model") String model, /** Returns a defensive copy of the quota snapshots map. */ @Override public Map quotaSnapshots() { - return quotaSnapshots == null ? null : Collections.unmodifiableMap(quotaSnapshots); + return quotaSnapshots == null ? Collections.emptyMap() : Collections.unmodifiableMap(quotaSnapshots); } } } diff --git a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java index c29e6a403..a102dfe27 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventParserTest.java @@ -1381,6 +1381,30 @@ void testAssistantUsageEventAllFields() throws Exception { assertEquals(2, data.quotaSnapshots().size()); } + @Test + void testAssistantUsageEventWithNullQuotaSnapshots() throws Exception { + String json = """ + { + "type": "assistant.usage", + "data": { + "model": "gpt-4-turbo", + "inputTokens": 500, + "outputTokens": 200 + } + } + """; + + var event = (AssistantUsageEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("gpt-4-turbo", data.model()); + assertEquals(500.0, data.inputTokens()); + assertEquals(200.0, data.outputTokens()); + // quotaSnapshots should return an empty map, not null + assertNotNull(data.quotaSnapshots()); + assertTrue(data.quotaSnapshots().isEmpty()); + } + @Test void testAssistantReasoningDeltaEventAllFields() throws Exception { String json = """ From 39d55faca27104fc2b088da5d05ed81f2952c45b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:21:38 +0000 Subject: [PATCH 104/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 9382d5f37..5ec90e4b6 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 90.3% - 90.3% + 90.6% + 90.6% From 061379d136a411d673c326d16c022b4cc957121b Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 11 Feb 2026 00:40:26 -0500 Subject: [PATCH 105/147] fix: update ToolDefinition Javadoc link to correct path --- src/site/markdown/advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index c540bf53c..0d3e54d11 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -66,7 +66,7 @@ var session = client.createSession( ).get(); ``` -See [ToolDefinition](apidocs/com/github/copilot/sdk/ToolDefinition.html) Javadoc for schema details. +See [ToolDefinition](apidocs/com/github/copilot/sdk/json/ToolDefinition.html) Javadoc for schema details. --- From 49f2dcdcff893d292818ac18eea33fb6e32ffb0a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:44:01 +0000 Subject: [PATCH 106/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 5ec90e4b6..c51329362 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 90.6% - 90.6% + 90.7% + 90.7% From 044a272837935a2453d1cb181761fe15677edcdd Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 11 Feb 2026 00:45:53 -0500 Subject: [PATCH 107/147] feat: add Connection State & Diagnostics and Message Delivery Mode sections to documentation --- src/site/markdown/advanced.md | 60 ++++++++++++++++++++ src/site/markdown/documentation.md | 89 ++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 0d3e54d11..03f5b0db2 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -15,6 +15,7 @@ This guide covers advanced scenarios for extending and customizing your Copilot - [Infinite Sessions](#Infinite_Sessions) - [Compaction Events](#Compaction_Events) - [MCP Servers](#MCP_Servers) +- [Custom Agents](#Custom_Agents) - [Skills Configuration](#Skills_Configuration) - [Loading Skills](#Loading_Skills) - [Disabling Skills](#Disabling_Skills) @@ -236,6 +237,65 @@ var session = client.createSession( --- +## Custom Agents + +Extend the base Copilot assistant with specialized agents that have their own tools, prompts, and behavior. Users can invoke agents using the `@agent-name` mention syntax in messages. + +```java +var reviewer = new CustomAgentConfig() + .setName("code-reviewer") + .setDisplayName("Code Reviewer") + .setDescription("Reviews code for best practices and security") + .setPrompt("You are a code review expert. Focus on security, performance, and maintainability.") + .setTools(List.of("read_file", "search_code")); + +var session = client.createSession( + new SessionConfig() + .setCustomAgents(List.of(reviewer)) +).get(); + +// The user can now mention @code-reviewer in messages +session.send("@code-reviewer Review src/Main.java").get(); +``` + +### Configuration Options + +| Option | Type | Description | +|--------|------|-------------| +| `name` | String | Unique identifier used for `@mentions` (alphanumeric and hyphens) | +| `displayName` | String | Human-readable name shown to users | +| `description` | String | Describes the agent's capabilities | +| `prompt` | String | System prompt that defines the agent's behavior | +| `tools` | List<String> | Tool names available to this agent | +| `mcpServers` | Map | MCP servers available to this agent | +| `infer` | Boolean | Whether the agent can be auto-selected based on context | + +### Multiple Agents + +Register multiple agents for different tasks: + +```java +var agents = List.of( + new CustomAgentConfig() + .setName("reviewer") + .setDescription("Code review") + .setPrompt("You review code for issues."), + new CustomAgentConfig() + .setName("documenter") + .setDescription("Documentation writer") + .setPrompt("You write clear documentation.") + .setInfer(true) // Auto-select when appropriate +); + +var session = client.createSession( + new SessionConfig().setCustomAgents(agents) +).get(); +``` + +See [CustomAgentConfig](apidocs/com/github/copilot/sdk/json/CustomAgentConfig.html) Javadoc for full details. + +--- + ## Skills Configuration Load custom skills from directories to extend the AI's capabilities with domain-specific knowledge. diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index 8d474690f..92cf7b41d 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -12,6 +12,8 @@ This guide covers common use cases for the Copilot SDK for Java. For complete AP - [Streaming Responses](#Streaming_Responses) - [Session Operations](#Session_Operations) - [Choosing a Model](#Choosing_a_Model) +- [Connection State & Diagnostics](#Connection_State__Diagnostics) +- [Message Delivery Mode](#Message_Delivery_Mode) - [Session Management](#Session_Management) --- @@ -310,6 +312,93 @@ var session = client.createSession( --- +## Connection State & Diagnostics + +### Checking Connection State + +Query the client's connection state at any time without making a server call: + +```java +ConnectionState state = client.getState(); +switch (state) { + case CONNECTED -> System.out.println("Ready"); + case CONNECTING -> System.out.println("Starting up..."); + case DISCONNECTED -> System.out.println("Not connected"); + case ERROR -> System.out.println("Connection failed"); +} +``` + +The four states are: + +| State | Description | +|-------|-------------| +| `DISCONNECTED` | Client has not been started yet | +| `CONNECTING` | `start()` was called but hasn't completed | +| `CONNECTED` | Client is connected and ready | +| `ERROR` | Connection failed (check logs for details) | + +### Server Status + +Get CLI version and protocol information: + +```java +var status = client.getStatus().get(); +System.out.println("CLI version: " + status.getVersion()); +System.out.println("Protocol version: " + status.getProtocolVersion()); +``` + +### Authentication Status + +Check whether the current connection is authenticated and how: + +```java +var auth = client.getAuthStatus().get(); +if (auth.isAuthenticated()) { + System.out.println("Logged in as: " + auth.getLogin()); + System.out.println("Auth type: " + auth.getAuthType()); + System.out.println("Host: " + auth.getHost()); +} else { + System.out.println("Not authenticated: " + auth.getStatusMessage()); +} +``` + +### Ping + +Verify server connectivity: + +```java +var pong = client.ping("hello").get(); +System.out.println("Server responded, protocol version: " + pong.protocolVersion()); +``` + +See [ConnectionState](apidocs/com/github/copilot/sdk/ConnectionState.html), [GetStatusResponse](apidocs/com/github/copilot/sdk/json/GetStatusResponse.html), and [GetAuthStatusResponse](apidocs/com/github/copilot/sdk/json/GetAuthStatusResponse.html) Javadoc for details. + +--- + +## Message Delivery Mode + +Control how messages are delivered to the session: + +```java +// Default: message is enqueued for processing +session.send(new MessageOptions() + .setPrompt("Analyze this codebase") +).get(); + +// Immediate: process the message right away +session.send(new MessageOptions() + .setPrompt("Quick question") + .setMode("immediate") +).get(); +``` + +| Mode | Description | +|------|-------------| +| `"enqueue"` | Queue the message for processing (default) | +| `"immediate"` | Process the message immediately | + +--- + ## Session Management ### Multiple Concurrent Sessions From 8c58cecf2113abdb67fc62c3c95a4562f3a9ae68 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 11 Feb 2026 00:50:31 -0500 Subject: [PATCH 108/147] feat: add Reasoning Effort, Tool Filtering, and Working Directory sections to documentation --- src/site/markdown/documentation.md | 87 ++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index 92cf7b41d..a43c4499c 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -12,6 +12,9 @@ This guide covers common use cases for the Copilot SDK for Java. For complete AP - [Streaming Responses](#Streaming_Responses) - [Session Operations](#Session_Operations) - [Choosing a Model](#Choosing_a_Model) +- [Reasoning Effort](#Reasoning_Effort) +- [Tool Filtering](#Tool_Filtering) +- [Working Directory](#Working_Directory) - [Connection State & Diagnostics](#Connection_State__Diagnostics) - [Message Delivery Mode](#Message_Delivery_Mode) - [Session Management](#Session_Management) @@ -312,6 +315,90 @@ var session = client.createSession( --- +## Reasoning Effort + +For models that support it, control how much effort the model spends reasoning before responding: + +```java +var session = client.createSession( + new SessionConfig() + .setModel("o3-mini") + .setReasoningEffort("high") +).get(); +``` + +| Level | Description | +|-------|-------------| +| `"low"` | Fastest responses, less detailed reasoning | +| `"medium"` | Balanced speed and reasoning depth | +| `"high"` | Thorough reasoning, slower responses | +| `"xhigh"` | Maximum reasoning effort | + +> **Note:** Only applies to reasoning models (e.g., `o3-mini`). Non-reasoning models ignore this setting. + +--- + +## Tool Filtering + +Control which built-in tools the assistant can use with allowlists and blocklists. + +### Allowlist (Available Tools) + +Restrict the session to only specific tools: + +```java +var session = client.createSession( + new SessionConfig() + .setAvailableTools(List.of("read_file", "search_code", "list_dir")) +).get(); +``` + +When `availableTools` is set, the assistant can **only** use tools in this list. + +### Blocklist (Excluded Tools) + +Allow all tools except specific ones: + +```java +var session = client.createSession( + new SessionConfig() + .setExcludedTools(List.of("execute_command", "write_file")) +).get(); +``` + +Tools in the `excludedTools` list will not be available to the assistant. + +### Combining with Custom Tools + +Tool filtering applies to built-in tools. Your custom tools (registered via `setTools()`) are always available: + +```java +var lookupTool = ToolDefinition.create("lookup_issue", "Fetch issue", schema, handler); + +var session = client.createSession( + new SessionConfig() + .setTools(List.of(lookupTool)) // Custom tool always available + .setAvailableTools(List.of("read_file")) // Only allow read_file from built-ins +).get(); +``` + +--- + +## Working Directory + +Set the working directory for file operations in the session: + +```java +var session = client.createSession( + new SessionConfig() + .setWorkingDirectory("/path/to/project") +).get(); +``` + +This affects how the assistant resolves relative file paths when using tools like `read_file`, `write_file`, and `search_code`. + +--- + ## Connection State & Diagnostics ### Checking Connection State From ce2a2bd2255a482e2f014619dfcc5adbfac526d5 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 11 Feb 2026 00:52:56 -0500 Subject: [PATCH 109/147] feat: add Next Steps sections to multiple documentation files for improved navigation --- src/site/markdown/advanced.md | 10 +++++++++ src/site/markdown/documentation.md | 31 ++++++++++++++++++++++++++++ src/site/markdown/getting-started.md | 10 +++++++++ src/site/markdown/hooks.md | 9 ++++++++ src/site/markdown/mcp.md | 9 ++++++++ 5 files changed, 69 insertions(+) diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 03f5b0db2..317d83086 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -627,3 +627,13 @@ session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); ``` See [EventErrorPolicy](apidocs/com/github/copilot/sdk/EventErrorPolicy.html) and [EventErrorHandler](apidocs/com/github/copilot/sdk/EventErrorHandler.html) Javadoc for details. + +--- + +## Next Steps + +- 📖 **[Documentation](documentation.html)** - Core concepts, events, streaming, models, tool filtering, reasoning effort +- 📖 **[Session Hooks](hooks.html)** - All 5 hook types with inputs, outputs, and examples +- 📖 **[MCP Servers](mcp.html)** - Local and remote MCP server integration +- 📖 **[Setup & Deployment](setup.html)** - OAuth, backend services, scaling, configuration reference +- 📖 **[API Javadoc](apidocs/index.html)** - Complete API reference diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index a43c4499c..e81e50fb4 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -18,6 +18,7 @@ This guide covers common use cases for the Copilot SDK for Java. For complete AP - [Connection State & Diagnostics](#Connection_State__Diagnostics) - [Message Delivery Mode](#Message_Delivery_Mode) - [Session Management](#Session_Management) +- [SessionConfig Reference](#SessionConfig_Reference) --- @@ -563,6 +564,36 @@ client.deleteSession(sessionId).get(); --- +## SessionConfig Reference + +Complete list of all `SessionConfig` options for `createSession()`: + +| Option | Type | Description | Guide | +|--------|------|-------------|-------| +| `sessionId` | String | Custom session ID (auto-generated if omitted) | — | +| `model` | String | AI model to use | [Choosing a Model](#Choosing_a_Model) | +| `reasoningEffort` | String | Reasoning depth: `"low"`, `"medium"`, `"high"`, `"xhigh"` | [Reasoning Effort](#Reasoning_Effort) | +| `tools` | List<ToolDefinition> | Custom tools the assistant can invoke | [Custom Tools](advanced.html#Custom_Tools) | +| `systemMessage` | SystemMessageConfig | Customize assistant behavior | [System Messages](advanced.html#System_Messages) | +| `availableTools` | List<String> | Allowlist of built-in tool names | [Tool Filtering](#Tool_Filtering) | +| `excludedTools` | List<String> | Blocklist of built-in tool names | [Tool Filtering](#Tool_Filtering) | +| `provider` | ProviderConfig | BYOK API provider configuration | [BYOK](advanced.html#Bring_Your_Own_Key_BYOK) | +| `onPermissionRequest` | PermissionHandler | Handler for permission requests | [Permission Handling](advanced.html#Permission_Handling) | +| `onUserInputRequest` | UserInputHandler | Handler for user input requests | [User Input Handling](advanced.html#User_Input_Handling) | +| `hooks` | SessionHooks | Lifecycle and tool execution hooks | [Session Hooks](hooks.html) | +| `workingDirectory` | String | Working directory for file operations | [Working Directory](#Working_Directory) | +| `streaming` | boolean | Enable streaming response chunks | [Streaming Responses](#Streaming_Responses) | +| `mcpServers` | Map<String, Object> | MCP server configurations | [MCP Servers](mcp.html) | +| `customAgents` | List<CustomAgentConfig> | Custom agent definitions | [Custom Agents](advanced.html#Custom_Agents) | +| `infiniteSessions` | InfiniteSessionConfig | Auto-compaction for long conversations | [Infinite Sessions](advanced.html#Infinite_Sessions) | +| `skillDirectories` | List<String> | Directories to load skills from | [Skills](advanced.html#Skills_Configuration) | +| `disabledSkills` | List<String> | Skills to disable by name | [Skills](advanced.html#Skills_Configuration) | +| `configDir` | String | Custom configuration directory | [Config Dir](advanced.html#Custom_Configuration_Directory) | + +See [SessionConfig](apidocs/com/github/copilot/sdk/json/SessionConfig.html) Javadoc for full details. + +--- + ## Next Steps - 📖 **[Advanced Usage](advanced.html)** - Tools, BYOK, MCP Servers, System Messages, Infinite Sessions, Skills diff --git a/src/site/markdown/getting-started.md b/src/site/markdown/getting-started.md index d1a14a323..9077426f7 100644 --- a/src/site/markdown/getting-started.md +++ b/src/site/markdown/getting-started.md @@ -349,3 +349,13 @@ copilot auth login ### "Connection timeout" Check your internet connection and firewall settings. The SDK communicates with GitHub's Copilot service. + +--- + +## Next Steps + +- 📖 **[Documentation](documentation.html)** - Core concepts, events, streaming, session management +- 📖 **[Advanced Usage](advanced.html)** - Tools, BYOK, MCP Servers, System Messages, Custom Agents +- 📖 **[Session Hooks](hooks.html)** - Intercept tool execution and session lifecycle events +- 📖 **[Setup & Deployment](setup.html)** - OAuth, backend services, scaling +- 📖 **[API Javadoc](apidocs/index.html)** - Complete API reference diff --git a/src/site/markdown/hooks.md b/src/site/markdown/hooks.md index 58b0bc73e..153362c98 100644 --- a/src/site/markdown/hooks.md +++ b/src/site/markdown/hooks.md @@ -393,3 +393,12 @@ To handle errors gracefully in your hooks: - [PreToolUseHookOutput Javadoc](apidocs/com/github/copilot/sdk/json/PreToolUseHookOutput.html) - [PostToolUseHookInput Javadoc](apidocs/com/github/copilot/sdk/json/PostToolUseHookInput.html) - [PostToolUseHookOutput Javadoc](apidocs/com/github/copilot/sdk/json/PostToolUseHookOutput.html) + +--- + +## Next Steps + +- 📖 **[Documentation](documentation.html)** - Core concepts, events, session management +- 📖 **[Advanced Usage](advanced.html)** - Tools, BYOK, MCP Servers, Custom Agents +- 📖 **[MCP Servers](mcp.html)** - Integrate external tools via Model Context Protocol +- 📖 **[API Javadoc](apidocs/index.html)** - Complete API reference diff --git a/src/site/markdown/mcp.md b/src/site/markdown/mcp.md index b575e52a9..262b67372 100644 --- a/src/site/markdown/mcp.md +++ b/src/site/markdown/mcp.md @@ -115,3 +115,12 @@ var session = client.createSession( - [MCP Specification](https://modelcontextprotocol.io/) - [MCP Servers Directory](https://github.com/modelcontextprotocol/servers) - [GitHub MCP Server](https://github.com/github/github-mcp-server) + +--- + +## Next Steps + +- 📖 **[Documentation](documentation.html)** - Core concepts, events, session management +- 📖 **[Advanced Usage](advanced.html)** - Tools, BYOK, System Messages, Custom Agents +- 📖 **[Session Hooks](hooks.html)** - Intercept tool execution and session lifecycle events +- 📖 **[API Javadoc](apidocs/index.html)** - Complete API reference From eb1e4b92a5d386ebe3bf280ee87d3a0d1ebdc9f5 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 11 Feb 2026 01:02:22 -0500 Subject: [PATCH 110/147] fix: refine wording in documentation for clarity on event types --- src/site/markdown/documentation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index e81e50fb4..4b920f080 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -114,13 +114,13 @@ session.on(event -> { | `SessionIdleEvent` | Session finished processing | | `SessionErrorEvent` | An error occurred | -For the complete list of all 33 event types, see [Event Types Reference](#Event_Types_Reference) below. +For the complete list of all event types, see [Event Types Reference](#Event_Types_Reference) below. --- ## Event Types Reference -The SDK supports 33 event types organized by category. All events extend `AbstractSessionEvent`. +The SDK supports event types organized by category. All events extend `AbstractSessionEvent`. ### Session Events From d24e94cabfad033a101a072e9073f21d40c9cca8 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 11 Feb 2026 01:06:35 -0500 Subject: [PATCH 111/147] refactor: remove unused logger field and related methods from CopilotClientOptions --- .../sdk/json/CopilotClientOptions.java | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index 03d6e6726..b51ab7a19 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -5,7 +5,6 @@ package com.github.copilot.sdk.json; import java.util.Map; -import java.util.logging.Logger; import com.fasterxml.jackson.annotation.JsonInclude; @@ -42,7 +41,6 @@ public class CopilotClientOptions { private boolean autoStart = true; private boolean autoRestart = true; private Map environment; - private Logger logger; private String githubToken; private Boolean useLoggedInUser; @@ -276,27 +274,6 @@ public CopilotClientOptions setEnvironment(Map environment) { return this; } - /** - * Gets the custom logger for the client. - * - * @return the logger instance - */ - public Logger getLogger() { - return logger; - } - - /** - * Sets a custom logger for the client. - * - * @param logger - * the logger instance to use - * @return this options instance for method chaining - */ - public CopilotClientOptions setLogger(Logger logger) { - this.logger = logger; - return this; - } - /** * Gets the GitHub token for authentication. * From 25e491394a8677e1408b17c7191feaf73135d53b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 06:09:56 +0000 Subject: [PATCH 112/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index c51329362..c5158bc4f 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 90.7% - 90.7% + 90.8% + 90.8% From df0832c93373e16d62650e01bb61a3e5d881bf6e Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 11 Feb 2026 10:24:30 -0500 Subject: [PATCH 113/147] feat: enhance documentation with detailed file attachment parameters and session shutdown methods --- src/site/markdown/advanced.md | 45 +++++++++++++++++++++++++++++- src/site/markdown/documentation.md | 27 ++++++++++++++++++ src/site/markdown/setup.md | 33 ++++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 317d83086..e3babeb0f 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -110,7 +110,13 @@ var session = client.createSession( ## File Attachments -Include files as context for the AI to analyze. +Include files as context for the AI to analyze. The `Attachment` record takes three parameters: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `type` | String | The attachment type — use `"file"` for filesystem files | +| `path` | String | The absolute path to the file on disk | +| `displayName` | String | A human-readable label shown to the AI (e.g., the filename or a description) | ```java session.send(new MessageOptions() @@ -121,6 +127,18 @@ session.send(new MessageOptions() ).get(); ``` +You can attach multiple files in a single message: + +```java +session.send(new MessageOptions() + .setPrompt("Compare these two implementations") + .setAttachments(List.of( + new Attachment("file", "/src/main/OldImpl.java", "Old Implementation"), + new Attachment("file", "/src/main/NewImpl.java", "New Implementation") + )) +).get(); +``` + --- ## Bring Your Own Key (BYOK) @@ -454,6 +472,31 @@ client.start().get(); // Start manually client.stop().get(); // Stop manually ``` +### Graceful Stop vs Force Stop + +The SDK provides two shutdown methods: + +| Method | Behavior | +|--------|----------| +| `stop()` | Gracefully closes all open sessions, then shuts down the connection | +| `forceStop()` | Immediately clears sessions and shuts down — no graceful session cleanup | + +Use `stop()` for normal shutdown — it ensures each session is properly closed (flushing pending operations) before terminating the connection: + +```java +// Graceful: closes all sessions, then disconnects +client.stop().get(); +``` + +Use `forceStop()` when you need to terminate immediately, such as during error recovery or when the server is unresponsive: + +```java +// Immediate: skips session cleanup, kills connection +client.forceStop().get(); +``` + +> **Tip:** In `try-with-resources` blocks, `close()` delegates to `stop()`, so graceful cleanup happens automatically. + --- ## Session Lifecycle Events diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index 4b920f080..45342988e 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -425,6 +425,33 @@ The four states are: | `CONNECTED` | Client is connected and ready | | `ERROR` | Connection failed (check logs for details) | +### State Transitions + +``` + start() + DISCONNECTED ──────────► CONNECTING + │ + ┌──────────┼──────────┐ + │ success │ │ failure + ▼ │ ▼ + CONNECTED │ ERROR + │ │ + stop() / │ │ + forceStop() │ │ + ▼ │ + DISCONNECTED ◄───┘ + │ + │ start() (retry) + ▼ + CONNECTING +``` + +- **DISCONNECTED → CONNECTING:** `start()` was called +- **CONNECTING → CONNECTED:** Server connection and protocol handshake succeeded +- **CONNECTING → ERROR:** Connection or protocol verification failed +- **CONNECTED → DISCONNECTED:** `stop()` or `forceStop()` was called, or `close()` via try-with-resources +- **DISCONNECTED → CONNECTING:** `start()` can be called again to retry + ### Server Status Get CLI version and protocol information: diff --git a/src/site/markdown/setup.md b/src/site/markdown/setup.md index 660b5e75c..eebec5bcd 100644 --- a/src/site/markdown/setup.md +++ b/src/site/markdown/setup.md @@ -350,6 +350,39 @@ Complete list of `CopilotClientOptions` settings: | `environment` | Map | Environment variables | inherited | | `cwd` | String | Working directory | current dir | +### Extra CLI Arguments + +Pass additional command-line arguments to the CLI process: + +```java +var options = new CopilotClientOptions() + .setCliArgs(new String[]{"--verbose", "--no-telemetry"}); + +try (var client = new CopilotClient(options)) { + client.start().get(); + // CLI process receives the extra flags +} +``` + +### Custom Environment Variables + +Set environment variables for the CLI process (merged with the inherited system environment): + +```java +var options = new CopilotClientOptions() + .setEnvironment(Map.of( + "HTTPS_PROXY", "http://proxy.example.com:8080", + "NO_PROXY", "localhost,127.0.0.1" + )); + +try (var client = new CopilotClient(options)) { + client.start().get(); + // CLI process uses the proxy settings +} +``` + +This is useful for configuring proxy servers, custom CA certificates, or any environment-specific settings the CLI needs. + ## Best Practices ### Development From 241448d3f01df22e80617bddd678ca2804a87f22 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 12 Feb 2026 15:31:00 -0500 Subject: [PATCH 114/147] feat: add AUTOCLOSEABLE_TIMEOUT_SECONDS constant and enhance documentation for graceful shutdown and event handling troubleshooting --- .../com/github/copilot/sdk/CopilotClient.java | 22 +++++++++++- src/site/markdown/advanced.md | 3 +- src/site/markdown/documentation.md | 34 +++++++++++++++++-- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 72305c046..a535e9b12 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -61,6 +61,11 @@ public final class CopilotClient implements AutoCloseable { private static final Logger LOG = Logger.getLogger(CopilotClient.class.getName()); + /** + * Timeout, in seconds, used by {@link #close()} when waiting for graceful + * shutdown via {@link #stop()}. + */ + public static final int AUTOCLOSEABLE_TIMEOUT_SECONDS = 10; private final CopilotClientOptions options; private final CliServerManager serverManager; private final LifecycleEventManager lifecycleManager = new LifecycleEventManager(); @@ -221,6 +226,7 @@ public CompletableFuture stop() { * @return A future that completes when the client is stopped */ public CompletableFuture forceStop() { + disposed = true; sessions.clear(); return cleanupConnection(); } @@ -554,13 +560,27 @@ private CompletableFuture ensureConnected() { return connectionFuture; } + /** + * Closes this client using graceful shutdown semantics. + *

+ * This method is intended for {@code try-with-resources} usage and blocks while + * waiting for {@link #stop()} to complete, up to + * {@link #AUTOCLOSEABLE_TIMEOUT_SECONDS} seconds. If shutdown fails or times + * out, the error is logged at {@link Level#FINE} and the method returns. + *

+ * This method is idempotent. + * + * @see #stop() + * @see #forceStop() + * @see #AUTOCLOSEABLE_TIMEOUT_SECONDS + */ @Override public void close() { if (disposed) return; disposed = true; try { - forceStop().get(5, TimeUnit.SECONDS); + stop().get(AUTOCLOSEABLE_TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (Exception e) { LOG.log(Level.FINE, "Error during close", e); } diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index e3babeb0f..cb3562552 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -495,7 +495,8 @@ Use `forceStop()` when you need to terminate immediately, such as during error r client.forceStop().get(); ``` -> **Tip:** In `try-with-resources` blocks, `close()` delegates to `stop()`, so graceful cleanup happens automatically. +> **Tip:** In `try-with-resources` blocks, `close()` delegates to `stop()`, so graceful session cleanup happens automatically. +> `close()` is blocking and waits up to `CopilotClient.AUTOCLOSEABLE_TIMEOUT_SECONDS` seconds for shutdown to complete. --- diff --git a/src/site/markdown/documentation.md b/src/site/markdown/documentation.md index 45342988e..9cdd5b9a2 100644 --- a/src/site/markdown/documentation.md +++ b/src/site/markdown/documentation.md @@ -8,6 +8,7 @@ This guide covers common use cases for the Copilot SDK for Java. For complete AP - [Basic Usage](#Basic_Usage) - [Handling Responses](#Handling_Responses) +- [Troubleshooting Event Handling](#Troubleshooting_Event_Handling) - [Event Types Reference](#Event_Types_Reference) - [Streaming Responses](#Streaming_Responses) - [Session Operations](#Session_Operations) @@ -64,11 +65,38 @@ System.out.println(response.getData().getContent()); For more control, subscribe to events and use `send()`: > **Exception isolation:** If a handler throws an exception, the SDK logs the -> error and continues dispatching to remaining handlers. One misbehaving handler -> will never prevent others from executing. You can customize error handling with -> `session.setEventErrorHandler()` — see the +> error. By default, dispatch stops after the first handler error +> (`PROPAGATE_AND_LOG_ERRORS`). To continue dispatching to remaining handlers, +> set `EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS`. You can customize error +> handling with `session.setEventErrorHandler()` — see the > [Advanced Usage](advanced.html#Custom_Event_Error_Handler) guide. +## Troubleshooting Event Handling + +### Symptoms of policy misconfiguration + +- You registered multiple `session.on(...)` handlers, but only the first one runs +- A handler throws once and later handlers stop receiving events +- You expected "best effort" fan-out, but dispatch halts on errors + +### Fix + +Set the event error policy to suppress-and-continue: + +```java +session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); +``` + +Optionally add a custom error handler for observability: + +```java +session.setEventErrorHandler((event, exception) -> { + System.err.println("Handler failed for event " + event.getType() + ": " + exception.getMessage()); +}); +``` + +Use `PROPAGATE_AND_LOG_ERRORS` when you want fail-fast behavior. + ```java var done = new CompletableFuture(); From 73927e48e502f95749128bb5a93948cdfabab76f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:34:28 +0000 Subject: [PATCH 115/147] Update JaCoCo coverage badge --- .github/badges/jacoco.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index c5158bc4f..388fa4643 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 90.8% - 90.8% + 91% + 91% From 5edbc1e53f66679e5bdab9977609446c3480697b Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 12 Feb 2026 21:54:54 -0500 Subject: [PATCH 116/147] Redesign docs index page with light theme and external CSS - Replace inline CSS with external styles.css template - New light theme with gradient hero, card-based version list - Add documentation and release notes links for all versions - Compact collapsible older releases list - Add footer attribution - Update deploy workflow to copy styles.css and generate new HTML format --- .github/templates/styles.css | 436 ++++++++++++++++++++++++++++++ .github/templates/versions.html | 325 ++++------------------ .github/workflows/deploy-site.yml | 9 +- 3 files changed, 500 insertions(+), 270 deletions(-) create mode 100644 .github/templates/styles.css diff --git a/.github/templates/styles.css b/.github/templates/styles.css new file mode 100644 index 000000000..415058862 --- /dev/null +++ b/.github/templates/styles.css @@ -0,0 +1,436 @@ +/* ===== Reset & Base ===== */ +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.6; + color: #24292f; + background: #f6f8fa; + -webkit-font-smoothing: antialiased; +} + +.container { + max-width: 860px; + margin: 0 auto; + padding: 0 24px; +} + +a { + color: #0969da; + text-decoration: none; + transition: color 0.2s; +} + +a:hover { + color: #0550ae; +} + +/* ===== Hero Header ===== */ +.hero { + position: relative; + text-align: center; + padding: 80px 24px 48px; + overflow: hidden; +} + +.hero-bg { + position: absolute; + inset: 0; + background: + radial-gradient(ellipse 80% 60% at 50% -20%, rgba(102, 126, 234, 0.12), transparent), + radial-gradient(ellipse 60% 50% at 80% 50%, rgba(118, 75, 162, 0.06), transparent); + z-index: 0; +} + +.hero-content { + position: relative; + z-index: 1; +} + +.hero h1 { + font-size: clamp(2em, 5vw, 2.8em); + font-weight: 800; + line-height: 1.15; + margin-bottom: 16px; + color: #24292f; +} + +.gradient-text { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-subtitle { + font-size: 1.1em; + color: #57606a; + margin-bottom: 32px; + max-width: 520px; + margin-left: auto; + margin-right: auto; +} + +/* ===== Buttons ===== */ +.header-buttons { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + font-size: 0.95em; + text-decoration: none; + transition: all 0.2s; + cursor: pointer; +} + +.btn-github { + background: #24292f; + color: #fff; +} + +.btn-github:hover { + color: #fff; + background: #32383f; + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(36, 41, 47, 0.25); +} + +.btn-github svg { + fill: #fff; +} + +.btn-maven { + background: linear-gradient(135deg, #667eea, #764ba2); + color: #fff; +} + +.btn-maven:hover { + color: #fff; + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.35); +} + +.btn-maven svg { + fill: #fff; +} + +/* ===== Alert Boxes ===== */ +.alert { + padding: 16px 20px; + border-radius: 10px; + margin-bottom: 16px; + font-size: 0.95em; + line-height: 1.6; + border: 1px solid; +} + +.alert-warning { + background: #fff8c5; + border-color: #d4a72c; + color: #6a5300; +} + +.alert-info { + background: rgba(102, 126, 234, 0.06); + border-color: rgba(102, 126, 234, 0.2); + color: #4a5067; +} + +/* ===== Section ===== */ +.section { + padding: 0 0 48px; +} + +.section-title { + font-size: 1.4em; + font-weight: 700; + color: #24292f; + margin-bottom: 20px; +} + +/* ===== Version List ===== */ +.version-list { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + gap: 10px; +} + +.version-list li { + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; + padding: 16px 20px; + display: flex; + align-items: center; + justify-content: space-between; + transition: all 0.2s; +} + +.version-list li:hover { + border-color: #0969da; + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06); +} + +.version-list li.latest-version { + background: linear-gradient(135deg, rgba(102, 126, 234, 0.04), rgba(118, 75, 162, 0.04)); + border-color: #667eea; +} + +.version-list li.latest-version:hover { + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.15); +} + +.version-name { + color: #24292f; + font-size: 1.05em; + font-weight: 700; +} + +.version-links { + display: flex; + align-items: center; + gap: 16px; +} + +.doc-link { + font-size: 0.85em; + font-weight: 500; + color: #0969da; +} + +.doc-link:hover { + color: #0550ae; +} + +/* ===== Badges ===== */ +.badge { + display: inline-block; + padding: 4px 12px; + border-radius: 100px; + font-size: 0.75em; + font-weight: 600; + letter-spacing: 0.3px; + text-transform: uppercase; +} + +.badge.latest { + background: linear-gradient(135deg, #667eea, #764ba2); + color: #fff; +} + +.badge.snapshot { + background: rgba(212, 167, 44, 0.12); + border: 1px solid rgba(212, 167, 44, 0.3); + color: #7a5c00; +} + +/* ===== Collapsible ===== */ +.collapsible { + margin-top: 8px; +} + +.collapsible-toggle { + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; + padding: 14px 20px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + font-size: 0.95em; + font-weight: 600; + color: #57606a; + transition: all 0.2s; +} + +.collapsible-toggle:hover { + border-color: #0969da; + color: #24292f; +} + +.collapsible-toggle::after { + content: '▶'; + font-size: 0.7em; + transition: transform 0.2s; + color: #8b949e; +} + +.collapsible-toggle.open::after { + transform: rotate(90deg); +} + +.collapsible-content { + display: none; + padding-top: 10px; +} + +.collapsible-content.open { + display: block; +} + +/* ===== Older Releases List ===== */ +.older-list { + list-style: none; + padding: 8px 0 0; +} + +.older-list li { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 20px; + border-bottom: 1px solid #eaeef2; + font-size: 0.9em; +} + +.older-list li:last-child { + border-bottom: none; +} + +.older-list > li > span:first-child { + color: #24292f; + font-weight: 500; +} + +.older-links { + display: flex; + gap: 16px; +} + +.release-link { + font-size: 0.85em; + font-weight: 500; + color: #8b949e; +} + +.release-link:hover { + color: #0969da; +} + +/* ===== Legend ===== */ +.legend { + display: flex; + flex-wrap: wrap; + gap: 24px; + justify-content: center; + margin-top: 28px; + padding: 16px; + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; +} + +.legend-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.85em; + color: #57606a; +} + +.legend-color { + width: 14px; + height: 14px; + border-radius: 4px; +} + +.legend-color.latest { + background: linear-gradient(135deg, #667eea, #764ba2); +} + +.legend-color.snapshot { + background: rgba(212, 167, 44, 0.4); + border: 1px solid rgba(212, 167, 44, 0.5); +} + +/* ===== Footer ===== */ +.footer { + margin-top: 48px; + padding: 32px 0; + text-align: center; + border-top: 1px solid #d0d7de; +} + +.footer-links { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; + align-items: center; +} + +.footer-links a { + color: #57606a; + font-size: 0.88em; + font-weight: 500; + transition: color 0.2s; +} + +.footer-links a:hover { + color: #0969da; +} + +.footer-links .sep { + color: #d0d7de; +} + +.built-with { + margin-top: 16px; + font-size: 0.85em; + color: #8b949e; +} + +.built-with a { + color: #57606a; + font-weight: 600; +} + +.built-with a:hover { + color: #0969da; +} + +/* ===== Responsive ===== */ +@media (max-width: 640px) { + .hero { + padding: 60px 16px 36px; + } + + .header-buttons { + flex-direction: column; + align-items: center; + } + + .btn { + width: 100%; + max-width: 280px; + justify-content: center; + } + + .version-list li { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } +} diff --git a/.github/templates/versions.html b/.github/templates/versions.html index 2926d7782..deda683ad 100644 --- a/.github/templates/versions.html +++ b/.github/templates/versions.html @@ -2,294 +2,85 @@ + - Documentation Versions - Copilot SDK for Java - + Copilot SDK for Java — Documentation + -

-
-

Copilot SDK for Java

+ + +
+
+
+

Copilot SDK for Java

+

An unofficial, community-driven SDK for building AI-powered tools with GitHub Copilot's agentic runtime.

- -
+
+ +
+ +
⚠️ Disclaimer: This is an unofficial, community-driven SDK and is not supported or endorsed by GitHub. Use at your own risk.
- -
+ +
💡 Tip: We recommend using the Latest Release documentation unless you need features from an unreleased version.
- -

Available Versions

-
    - -
- -
- -
-
    - -
+ +
+

Available Versions

+
    + +
+ +
+ +
+
    + +
+
-
- -
-
-
- Latest stable release -
-
-
- Development snapshot + +
+
+
+ Latest stable release +
+
+
+ Development snapshot +
- - +
+

Built with ❤️ by Bruno Borges and GitHub Copilot

+ + +
+ diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index a4eb544f2..d2bee9445 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -216,11 +216,14 @@ jobs: - name: Copy version index page run: | cp .github/templates/versions.html site/index.html + cp .github/templates/styles.css site/styles.css - name: Update version list from git tags run: | cd site + REPO_URL="https://github.com/copilot-community-sdk/copilot-sdk-java" + # Get versions from git tags (already sorted by version, descending) VERSIONS=$(git -C .. tag -l | grep -E '^v?[0-9]+\.[0-9]+' | sed 's/^v//' | sort -Vr) HAS_SNAPSHOT=$([ -d "snapshot" ] && echo "true" || echo "false") @@ -232,17 +235,17 @@ jobs: # Add snapshot if exists (goes to current versions) if [ "$HAS_SNAPSHOT" = "true" ]; then - CURRENT_HTML+='
  • Development (main branch)snapshot
  • ' + CURRENT_HTML+='
  • Development (main branch)documentation ↗release notes ↗snapshot
  • ' fi # Add versioned releases from tags (first one is latest, rest go to older) for v in $VERSIONS; do if [ -d "$v" ]; then if [ "$IS_FIRST_VERSION" = "true" ]; then - CURRENT_HTML+="
  • Version $vlatest
  • " + CURRENT_HTML+='
  • Version '"$v"'documentation ↗release notes ↗latest
  • ' IS_FIRST_VERSION="false" else - OLDER_HTML+="
  • Version $v
  • " + OLDER_HTML+='
  • '"$v"'documentation ↗release notes ↗
  • ' fi fi done From cc3eac726970ec7bf9af9ab77375136d1835cef7 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 12 Feb 2026 22:00:08 -0500 Subject: [PATCH 117/147] Rename versions.html template to index.html --- .github/templates/{versions.html => index.html} | 0 .github/workflows/deploy-site.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename .github/templates/{versions.html => index.html} (100%) diff --git a/.github/templates/versions.html b/.github/templates/index.html similarity index 100% rename from .github/templates/versions.html rename to .github/templates/index.html diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index d2bee9445..44ddf54f3 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -215,7 +215,7 @@ jobs: - name: Copy version index page run: | - cp .github/templates/versions.html site/index.html + cp .github/templates/index.html site/index.html cp .github/templates/styles.css site/styles.css - name: Update version list from git tags From 8c92c73e047052d60c1b77ce067f50c53ad73c4d Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 12 Feb 2026 22:21:39 -0500 Subject: [PATCH 118/147] new maven site style --- src/site/resources/css/site.css | 314 +++++++++++++++++++ src/site/resources/images/github-copilot.jpg | Bin 0 -> 13161 bytes 2 files changed, 314 insertions(+) create mode 100644 src/site/resources/css/site.css create mode 100644 src/site/resources/images/github-copilot.jpg diff --git a/src/site/resources/css/site.css b/src/site/resources/css/site.css new file mode 100644 index 000000000..80c717c0a --- /dev/null +++ b/src/site/resources/css/site.css @@ -0,0 +1,314 @@ +/* ===== Custom theme for Copilot SDK for Java ===== */ +/* Layered on top of Maven Fluido Skin (Bootstrap 2.x) */ + +/* ===== Typography & Base ===== */ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + color: #24292f; + background: #f6f8fa; + -webkit-font-smoothing: antialiased; +} + +a { + color: #0969da; +} + +a:hover { + color: #0550ae; +} + +code, pre { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; +} + +/* ===== Navbar ===== */ +.navbar .navbar-inner { + background: #24292f; + background-image: none; + border: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); +} + +.navbar .brand, +.navbar .nav > li > a, +.navbar .nav > li > a:hover, +.navbar .nav .dropdown-toggle { + color: #e6edf3; + text-shadow: none; + font-weight: 600; +} + +.navbar .nav > li > a:hover, +.navbar .nav > .active > a { + color: #fff; + background: rgba(255, 255, 255, 0.08); +} + +.navbar .nav .dropdown-menu { + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + padding: 8px 0; +} + +.navbar .nav .dropdown-menu li > a { + color: #24292f; + padding: 8px 16px; + font-weight: 500; +} + +.navbar .nav .dropdown-menu li > a:hover { + background: rgba(102, 126, 234, 0.06); + color: #0969da; +} + +/* ===== GitHub Ribbon ===== */ +.github-fork-ribbon:before { + background: linear-gradient(135deg, #667eea, #764ba2) !important; +} + +/* ===== Breadcrumbs ===== */ +#breadcrumbs { + background: #fff; + border-bottom: 1px solid #d0d7de; + padding: 10px 20px; +} + +#breadcrumbs .breadcrumb { + background: transparent; + margin: 0; +} + +/* ===== Left Sidebar ===== */ +#leftColumn .well { + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; + box-shadow: none; +} + +#leftColumn .nav-list > li > a { + color: #24292f; + border-radius: 6px; + margin: 2px 0; + font-weight: 500; +} + +#leftColumn .nav-list > li > a:hover { + background: rgba(102, 126, 234, 0.06); + color: #0969da; +} + +#leftColumn .nav-list > .active > a, +#leftColumn .nav-list > .active > a:hover { + background: linear-gradient(135deg, #667eea, #764ba2); + color: #fff; +} + +#leftColumn .nav-header { + color: #57606a; + font-weight: 700; + text-transform: uppercase; + font-size: 0.8em; + letter-spacing: 0.5px; + padding: 8px 14px 4px; +} + +/* ===== Main Content ===== */ +#bodyColumn { + line-height: 1.7; +} + +#bodyColumn h1, +#bodyColumn h2, +#bodyColumn h3, +#bodyColumn h4 { + color: #24292f; + font-weight: 700; +} + +#bodyColumn h2 { + border-bottom: 1px solid #d0d7de; + padding-bottom: 8px; + margin-top: 32px; +} + +/* ===== Code Blocks ===== */ +#bodyColumn pre { + background: #eef1f6; + color: #24292f; + border: 1px solid #d0d7de; + border-radius: 10px; + padding: 16px 20px; + font-size: 0.88em; + line-height: 1.6; + overflow-x: auto; +} + +#bodyColumn code { + background: rgba(102, 126, 234, 0.1); + color: #24292f; + padding: 2px 6px; + border-radius: 4px; + font-size: 0.9em; +} + +#bodyColumn pre code { + background: transparent; + color: inherit; + padding: 0; + border-radius: 0; +} + +/* ===== Alert Boxes (from markdown) ===== */ +#bodyColumn .alert, +#bodyColumn blockquote { + border-radius: 10px; + border-left: 4px solid; + padding: 16px 20px; + margin: 20px 0; +} + +#bodyColumn .alert-info, +#bodyColumn blockquote { + background: rgba(102, 126, 234, 0.06); + border-left-color: #667eea; + color: #4a5067; +} + +#bodyColumn .alert-warning { + background: #fff8c5; + border-left-color: #d4a72c; + color: #6a5300; +} + +#bodyColumn .alert-danger, +#bodyColumn .alert-error { + background: #ffeef0; + border-left-color: #cf222e; + color: #82071e; +} + +#bodyColumn .alert-success { + background: #dafbe1; + border-left-color: #1a7f37; + color: #116329; +} + +/* ===== Tables ===== */ +#bodyColumn table { + border-collapse: separate; + border-spacing: 0; + border: 1px solid #d0d7de; + border-radius: 10px; + overflow: hidden; + width: 100%; + margin: 20px 0; +} + +#bodyColumn table thead th { + background: #f6f8fa; + color: #24292f; + font-weight: 700; + border-bottom: 2px solid #d0d7de; + padding: 12px 16px; + text-align: left; +} + +#bodyColumn table tbody td { + padding: 10px 16px; + border-bottom: 1px solid #eaeef2; +} + +#bodyColumn table tbody tr:last-child td { + border-bottom: none; +} + +#bodyColumn table tbody tr:hover { + background: rgba(102, 126, 234, 0.03); +} + +/* ===== Badges / Labels ===== */ +.label, .badge { + font-weight: 600; + border-radius: 100px; + padding: 3px 10px; + font-size: 0.8em; +} + +.label-info, .badge-info { + background: linear-gradient(135deg, #667eea, #764ba2); +} + +/* ===== Cards (for section-like divs) ===== */ +#bodyColumn .section { + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; + padding: 24px 28px; + margin-bottom: 24px; +} + +#bodyColumn .section .section { + background: transparent; + border: none; + border-radius: 0; + padding: 0; + margin-bottom: 16px; +} + +/* ===== Footer ===== */ +#footer { + background: #fff; + border-top: 1px solid #d0d7de; + color: #57606a; + font-size: 0.88em; + padding: 24px 0; +} + +#footer a { + color: #57606a; + font-weight: 500; +} + +#footer a:hover { + color: #0969da; +} + +/* ===== Powered By (add GitHub Copilot) ===== */ +#poweredBy { + text-align: center; +} + +#poweredBy::after { + content: ''; + display: block; + margin: 8px auto 0; + width: 240px; + height: 120px; + background-image: url('../images/github-copilot.jpg'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + border-radius: 6px; +} + +/* ===== Scrollbar (subtle) ===== */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: #d0d7de; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #8b949e; +} diff --git a/src/site/resources/images/github-copilot.jpg b/src/site/resources/images/github-copilot.jpg new file mode 100644 index 0000000000000000000000000000000000000000..75efcdba2ce4ffa779b0860da7dca78bdce30335 GIT binary patch literal 13161 zcmeHt1yEhhmhQ%b1&0I=9wg|&odChz-GV2$OVHpBfrD#sCrFUs?(TkYg1fwv|K6!P zuWIJb)YPkbuj<{^wN9_STK3m#b)UBVJomf?U`k6!NdPb~0009G!1D$yv6PsYfugd4 zgp{oKUkR{iHYT=?u$TZ~`^m{cSyGf-Q%jp1X$hMDH2?>^1$c~19PLCD6=i|{l&)s} z)D8fXOn>V7PqY2U2`Hv!jwaBg2Po^C*f}^s!RkU~R97dvKe7c>#x=J7OZNIBJ3s+K z^)Y{B^S{Y?f93g`-0)W(Rb??~o*}3#Z2s?L<9{dr{k_mE0GtWipXmQAIhnaZ@dJQ_ zC;ECMqhpqmhgTseHUN3 zsb!n@Wal2s^Yil}3_K=)_V<7ejb8$o(C`w#1@HhIDB}Vo01@yC8i=40BR~dF{GAyA zQD|3Tf%4yeMFBoQ65s_mp)3iNEddok3K~?Pk_Ye+FoLoN5Dx@GLp;y~R6_Y2IERtI z`o9g}xdXs@0Xv2Oz`;-euvjo~STN6B00*>#!@|M*8A<;}h)8ho2r#fOpt}D(0f2#n zhebeqUI5VGprtV3pkoWD6aKey|BC-*fqz-x{}l_soaXX7>3+^AK58UVS~=+DygTvu zpJ8+J+@WXqsQ(Pma`m)j&h&>velrX8MiqCLHAXQ`?py4i?#>Ai2Go69u1aQyT)>b+ z|J&E@jRridiRj6i!fmKv)(_hEpu7tJP}Oyz%$NTbkQ2$8ll#uohMVavhxFfir%%`J z_3ra?{56QZJ&fa@NG!>w^_wN-ZAT1Jn4SB^-XVL z3(C(_i40Jj1?thwzz>oM3I8_$^9$VEMM?6@XY!S-Ce{?+F8848rSqM6+cmxqr`d$=2no(nNdoIy;DDI)PKYPtpVlxDG408qGMboRTO{L4-;9CSRgaonf zaI_n$CvQi8b{r3BCS30h_+YV(CM8^RzEBi+g4L)H1ix&fEH_Xso0jfi^&BVIBP==! zORL$gdTV7NRFP5JOYJ$ya-b)a`V5E=t&{)Pjeu>gEU}q)qOteczHPO4yb$=!<+!Qj zJ#VVSBd9l)(S}o|;Zf6cWv=`1aQejMcAv=`{(14B_vfwCnVAyl6AC1kxf;N`H1JjY zPw|-fIPKiCt_<~b({*N&RmC-ItfXLkH}U{D(o;`6PS@@ zY9Nhc);C}N*DV3gSN(bLRuA@1vjvXf!*^~vJ7erM&N-W!qs=vDYz2O%F6ub&Hr)@# zA2V)s+Prg4>}*_a#RvPe3cBPE9lB=}`}hdlq}`L6{VovyiScdgvW0jFKoW|O*46Ka z4oU!EkkTTz?g;%${`36L39(?spFw`e@q1gxx=gk*b81^r*U@Qiks;hj@ zwD>piq-UV@UN5<>dh+qqa$n+;d@r%QfkEgh8MnaBrNmGM0Zmh4rqW_^3webi zshWdD9ETYvQc-c;(U?07?Y`I%fic5dg+c<3nyJi?pGsQNPBrcMAG;;69gQ*Y64X%-w-g| zRDwF|_CPyJ#XrPb#Pw^nJOaP`0r$@dLjdb;;-ITMJk6?Eflo!!ku&2vH^z`~aU3^3yN+(Rc z=OT}tKYJb|UgCdVA0Io&_(m%u*7TFOaOCBtZ!~v?y{)IQZDBKI)}KmXc5-`jYLecC{$~4YT=V+gsbS{TYVVzft?*frn~OiYuKx+0?w;tsc)u4M^lBz%^U^=<|MG%J>?F*@cMGMr#I(LhlymcNXA)H zlfg5W@yYToN&Lowtjl@8d)n4UP{H1L*^YNbp1dKe|3#9S6%B>4IX@(2lz=$-qPLzZ zIs?{StwwDw+z4rRQ5RWJpnU$b=?;DIwtla*`W;bwSwLOI@96@-klk7}DBUPkFVtBr zGs1;`$_A~fXgG23#`j

    @-pKq?M~&(X5cbK2$I0ogK1(HtF4YW?dq)Xz6j*jL}bpm!ov!KrX_j{==WKCF!NZGBt=O zc-GHwUp9W!|4JdSo7^E~eD2?0^L{;KHcScq{5PZ&lu|e?X=0lBX(TDP8p2^KXf4>* zMZKf8*jb^x@Yn)w&xLV#9M$i10V4|!U}z8bE@_qBkzqXpJtM?E_fS-<#E`4~>*}er zu&~TG^MjhgXxVe*OKYW86qKu@WRzObDSB~@1TPHA^!W!bo(j-uwak5~uv%Wtekkwl zn=y?(=<8G8KKxMbnU)jI_kw7+%ba<3gybTRz*}k3EiuY1J<6hhKwx+$7gBR@<@`%i zq(S|wju|)~o#*n6vg)){9bZPvcGhL-l6N<_bG(*V@);oFw!c4Oe4jPI3(=bYsg^BL z_C6x|Y<4-c*jrL^zMy^QPpc#567`uaA% zfu)tXNa)MGWkVm9j32eLgwnya`)mA5?*ro5zZS5u9ldu4Iqtcdbc2z~Ry_WgEluPy zZL9^Av+c7Pi8T{r+t)YiVDcE`1k$?H=!P0?2WpIeXEt{EYs63b>%+8*t8c8I8oMc| zlB&HE@*<-Wbz9tL!>1{awk=KqC?qOamno#DYT1iDKE@SJ$Mc?-Q20XPAL;;R*;X%G?zhEDT^*NnF>xBv91?%0v2A(jGs`5$+hH#l_T zbW{h-)Hr@-3IN%L71O7;-sni~ycF`z4@r1kmJSeL%vr4AkL_rZh7{KZT%-8L;HzSa z>1M_mpLs3qeD`WgnN4@AP#b7E{{78VP@Su}PVI_5B%WuwTcWhGER7&9Il4QR4c@6* zUw|@(i0*@912-ynRVlqm&i*t1bF`FuWg<(8HsP<1Q>Cn4_^! zn0!Kyp<(T?=?r0|IH{}_BjN9f8c)&E!9i3s+_qvgk@e5R3^y*b?)c6$GE3ECUASa3 zjVG6uf|l+x=##F5xIu19GU}rMN=J|jlk2P`-o3F$RNv>wdMD5RBH#o8A31*tR>LeK zQPC@UeTqrDMY^>xz%OuYIY$WuJGEaN-k)W9QXVAQ6(CmtpB6_4iC7_1A2@CBfT!Od z%Z0h4#$hW6q4(l=C5Hp83{J*C^G&m01z7lkuquf=H#C#1wOVr?D4{HC(*961fG2Nl zG1sH2QBTC`mDnppZc`Ffg{fKr?_Us^7ONAC*1zeIs(h`WDMKZ87pD?WmEW$?JQ;WZk;OnMr2dheIz)B3D^zwz7*=^2}fhKH{NmHGS zcf)Zg~C$EW7jaw3Qc z#&a!d4eW07AhBb*3&YN5L8R52e0hB{OfZ>W?7Pa9XQ+9bvuEDB#erVgj9 zWftM^sOn&3TePkZ4xU`1~>N~(}_xM(rUd0{DWXu*_(6xko7 zYxy+q!|MxwI0UVpTGM_=lCfB#mThs((5L7tvCIR-jNNjD^T3u&?iyF8N0{1jzd`k1 zLf$PaKEzxYq4Z7pKGtnSsX6nbqmnYH3Nwts>fD6oCB900& z66muNc_2sSBJlizwH{kVx7rJit+&XP*IyQ9Sw|r$u#M=ilJlZK_bU}V%i$4&>b%lL z;|3(J&X`8ca>=M27k0!n`J<{69mKm`^eT8`m-}9m1dT)yyV8S5zzt^vuGXxCU7vh)g!fM5M<|@!3ZjozC@9`&vnMb6)`XCtIv0K@o8=DQ*Vd@#BoiI??sNzn zuocSZd^emDl0ZmSG5A)!>bt6ew>>X=bOQWr{ks^-%k z4-pI=$+Rf02}i90UX-}ThE^{ZFPW*Q`r_#xdXi2IW7646{gXM0kO#vWbdCN#&K{_DRG?B+hRiqt8^kI;sH6kT7B^<{#f6ZEy9f>&F z?FrfTPq9i$czA-M7XGiX?jqI|kR#M3>Fc&bS?AHS&KRm#OCnrA_Iw1y4cDgO(L2%a ztrZf{2rY>@b8x~XnFBj zmiO(t=+GW4G~WVf=Y}QaUYHFQYq~AirB(kI(M~$i#$IO>y zNM{9tBeM37c0IPpx^D@0ZvOg6EXW+nww?0ZnO1lp48Q@7X>Nf(*ncm39+at>9c+kAfe_v!x!&I7l z@RopZb4L?29FmVJOrwjcQh-6&QzbO9-}93S9*Oe68p|^)b^|&K??6}LGME4?3>*v` z5+W)B0ulmrB@SJP1Mtvg_!|)f99AVGdq;8#Hf4t@7IslXCqMt#xa?|NO5>b%kg#3+ zI39b2LSzgDm zr=e%ysIW}P7;^d0;>#;!{)6zC=vbS5sVeT{id3mnz1nBKrdFJ^pn9*owJB<`BW6q= z)b#ZF^u9FhkevutyC`0&IH?VnmmeL*Pb06(UMp$6eo2C^r+u2l22&6Q!Iwk|DWqnt zZpZm*PH8Lyx0?t-LI2$P^gbIj)U-7t!p9@tvq8CRK>Q4ZOlMhYMhFg`y*!1xFWJmI zCEPkOb3i-jdO*1TT0MI7u!Bl^yS3yWF_Es^de#%6i1=|s)%?7)HRy<{v4VHkTRNw* z-Zu_RjUp|Cs_yyW2;NRm_`T?mg#8$B1WKWKf2cMJvG$(bs)OjhJ){%L#~BV-&)l^* z?N!kHjTqF$&-EdrdP?Q}JR186_UM}qiJ;U5)o>G^uSJIB{_~sGGi}irS~)sK;sz=u)x^ zZg9SMmv2w=gD(c`W=r*vt|^sXz45~{FhRulv@)V}$9Mn3!8aPJIjU{S<6a@l16 z4Z5T~D!$}vWeIxPW(S@Q(RCS(-u@@8B45+Q6!#ZPOlya$pC;`XV9$r|@ zg5V@gQw<5C6nV)JP8V)9_;}@&{$03`UPWAEBrMtjNzwsGSqvRfjaKY@2`b6wNOYPF z%8^&*l(=U4p_uVUG>?U?Rk=K*UahWoD1+}5EX(%mcGZ=QB?}k3@th$$N)6*v4d-D^ zlk+@}Q>|VGFt{kLqqJK;L9&g(FlE8Z+7dae8GOu7QTIGty?96yNU@$59@}IXX0_LB z7NO2&m2#h2pCn8ACQb<4^i(ap(=s;;23M=9J3GqdPsz;RdWW^1ToJRre#N~IrkQH_ zlEj*F2Q!eEWBB`iR*2m%<@&+oom0h9^nz02Q@8eeF+6|QqMi z#H5IEzKKMipK9ZxlQO}-3(XZG{Mri(R2rwqnB$wd<_DM_EOm4CKREkUiu<-M`6f?4 z-P=EzsFc=YH(+u?9IO)r|ALQOxzs&EpBB`Qhs#-IA@+g~{@SH|RVQMf<4k*T*X0#o zf4TC>L%G7~q}lJed874NTDRz$gw}2YH&609SNiC*Sl#hT`3S^YXGbe}_xdFlt-i%Y ziKM$$`xiZ@C@^9T_r=59Fvr@T?tbl+#@ENUZ=9-v6VlrB85Bd?@N_NV;oyAKtd>1l zT0qT*iM-uftWRXaR+`a+(ryZ}YG_a@P-o$G7Mz(l^>_X-YzquGCp6e~kP`_?h@Jm#fbY zZR&perfI}G_s!+~TJxSZmJ}J?%bksXA74vxQ3ZHAqr42salH>tETCB$aQx{`y5H&W z5y?}Tgb=S^6a(EvPKR={Ya|@I66HZ)Y@yGtB$iu;|L^pEU>?qR6zw4D|bwMx#mB zGstm^TVY|9D?zE>1j5h?mUjZM1NpZyP7!vrIl6*g# z-SXRR^)A=MH#^NE+@a5n(=3K=M6-&5(|zMnhBZR(5d%?WJF;=)C~7hZ3B4?Qu%SFMYj1PDTqXPd%8Pag~x`L5upcy{l>oH zV@1cHmSbNMF@8CoIL;}ydqN&eUY2$wH;VMN#yFViFB^4B%Tjd2z_^@66+-A#IerUY zbg%N$km(}?WX-c%WD6MyMSO|w;VKlBUmJ?rwUvZzR9IJgCNdu>TDLTGe&s=tNGV*L z3&4C4wDTE6bwB1mpQ59CMJ%V&)r?N-Oo+C&8~>e*me|59_A6|s>(n$vJoe$;%|qm; zI^D~}kB{$X-W^ZlOfv!x4B9puGfzJ;?6Mu5x-wp8SH%X4;I_**NUeN#E7Oi_Zjl_n z12^~Mn;vx~`dLStHa!E{j~nDy^KC_0=d9!H!jpxUFrq3G_al9e;E0Mm1gfg^cVC^6 zagFX?Is1Mo-`OhSIQt1ZB*g*ra$h1oT!0nKO(|~(Yf$m}UK2BHmwbmG zvVoR9U|p6;nhilXoE4fjC9h3QRQnq`7}xB(JOnyC@_;ePj=KC>2yQ6O_@1%lR^uwY zw2DEQMm^T1s^ZUg9={5Y%vmkyb{rn|e+nqJH8shJ&ll&8B5<&HAoh8&i}evEGrM=2 zVoI9F?(jyPYZ3LvEXU=GU$xv*D!NXfP03_LfqI2q?~F5Di8No2}^_8g+ut*Uk| z9hU|UrYAGg2wLGY_E)7E&?&ynaeOB_t;R6)d{_ zlt0}mUZrB#IjMdlKuspUC9N4#RV?NOUg3@@U^-KPIgHUvrBJ8KI=-M4C6!l9*+CbtENfwIotP&LZVP4QNDm)6u@=>RB}(LPh4dUU$MXuk-o9OK z!jlr{XxhrqdplQK^*-QcHJq@A;W5|Bk88uM=vH844b-jdNz=&JOU7OP2FWXy?Qs)# zyrHwG^X;0%FX0i)F~Z8Z3;8~Qphtc2jlzCbLQ$@OHc`onQWA$omb0HdKD(zLY1 ziFN!GT)bTblJRfazRU%MX{nV(ER_+^<>8k?UZcUrv(k5NxUArevDk9jzAK&ntyN=b z-@MjEh1}`-dTIw>|BDQcltqm3cr`8WrkXpDwrl_0LBdlcwb;6pZVmx&n|b`QI?2(U z%j&mhAW>?q{|{=eUV}7SULVNjOfLGCI0oV^ZNnJll;EjwLnp%0V8fUY*(-j5H|lpH zyNbR9aT6krRD@B|=m)a9`ck?^`QAHy?pYZ!4!w*eEtdJ}?jI=;CC}dROJyntKAE(#Pj%E;zU5#F78?_%p~jNy zp_wylYWy%|N9%r!Yn%Ty)vr%8Bk4mtv(3W(hj%*o=coHtXc4a!M3BfQWgOXhUwu!+ zgh(J`tJUTBb_ydOLEXGBBa_sHLma$TA(H5alobHBET>i#7JO<~pi?O~MPI;N<`Ae5p&(bGWM z=gEqPgKy1CZA0D_LeZL|kBXB=0M%8q?;EAC2K`!X3MBs`;cQ|oVvYLYF&(aXV%X>X z#9b!2A_@SPh4E!5;QauXG+lBo&iB#;MIDDPhp5!BGvz6_4rz*DW7^DH4U^ISuD7-j z9C2)--3KOvC%V_)!rl6?%tNk6@+Kxv9VuU5hnVW2vFE#Ad zWSoy~SH*t0k2?0!<%wKUg{8$=aeF{PS#}0n9PF*iLf7;i1%B+BU2bWtlANPCqf*GK zI}!hRjwz|Be{wf!Yepz}xn=G)0f|C+(kCSfx`wWoT16C*qB&@EMAS`{(Yg*lWOYR< z=5ythkoVVr<)ZJl@1T;8Xu_SxzXMZU&f4efqMbUIpNqklplE)IVM7?%A5J`=edIi! zJqF$1%NcqVzO2~h<{-#GdCI+wye4C;sZ^SgXTY*fyL=Cw00qNn-@EJ`u+FR|t;^D; zACt9lKlz7ALg=W_J{!|XqJ5^!Kw-0x8o;(NxRny+b5jQ$k}bOnb$A4H#qYGUiQ+5h zxf1T!>dI!h3Rr--BEKTSD-!QaS&Mf!aqsgzUfvhPVW7_=Eite<44M=RM?VGE>n+qj z1312t714Fi0Mj#YR>7CRw0Ru<4A7L04xF_aCmM_~C zWqCeJMn31#*XP&WBo;sXOh_`9kCyZtDX18Bt@7&F&i%APxO*JWck{*>qH^exwX#av z*`$S#b4RpfR$6`cN_lxR#=|a3Cd$EGcG6D+IAZt_>E)s`pDNTmDRD6lJ0!{I#fmG7 zq2D?C1`tgVo|mpOEo9B6m>~K1>6EDVeg@W}RsR@fZc-u|FhzBu!tav9XPdXD~` zBFv3NxmJ|xeZ!vA_z-&pd`wj=&DRz-v^=z|7+UHvUOg>Qa)EhL;j-ce*u^JoF`xhz zYL%{bLj!zF6_p7*x|v<4#F=6H>_ZzpeL0HBk8RuxIx6@HxDZCotT4IYntS}G6J`Ij zt}<|1Q95(LTow-JFOI#O`{8dYf%G?6tg2<{?U4~8yUMauHTZ?*JUR1|y4(Cxvgw~8 z-g+2pE&3+*+P~1U2nsrQ@eZX8nF^MDXr%&&6W;e+IJdQDotVBwMit#^3Gz@hLOZr_ggRI9Paiq`x`!-+YQKVo0HcfQiFq zWbc=SOHL`KY#jTSTcMwTL*1GpXUx2`CRRKdef<3~+?%O8%+~;I%4$E$BkE~V=~O^G zFYqGdw+Wd_N@iELm)*xqEeAV@<>mg(OBO@&8;KgV3ypkN69P%^owD^Cvp}1Dn(rds ze3gCIID0Ab;6;OcL|KKv6&`0yi;9Yf*Am&qZwx|Sdc9uEmrA6asynjps#op}K#x+O zvFva9F1f;)nI8hy4#MTzoO;jMM%~Z)xt2JXwXZd9?(*fw;EBsb{)%(+U!c$9s$%JU zDgMT4DWXonk=poclDbp-LOlqq^h4r%GGFh8DV_E|9Co(bR9U}fSNl9A&i$LNUG#gy zSLwB*hEOFqFXR|boIKGvmWjM=!?LfIpRisWk7LMPrs!wuZU(G<)HBf?a#JEZELW+@ zEbZ~^Ruq(POfB8_>CpWS#5ea|GCZuKktDmYh5M8l-r#r_@O0uGnuVYgJRobZ2A{&C z--x`fT4ebmVF+u*t%-}TtbnNNVPoS87Dn)acVpcfOy6GfPfm28d{K!t!{x7obCi;F zjE{GzaXVzz-#E@U~rZ zM8QAiMLX8T+9?XJhl2=o_6zFE}5!1}Gz)Cpu~)1Dk4pdwh213t^l H&-4EahMJ0v literal 0 HcmV?d00001 From 0441d9fe01d450f6d5e7dfe4a30e3e6a6930ecc9 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 12 Feb 2026 22:38:23 -0500 Subject: [PATCH 119/147] Add custom theme for Maven site and JaCoCo report - Add site.css with light theme matching docs index design - Add GitHub Copilot logo to poweredBy section - Add custom JaCoCo report.css with matching theme - Add Maven copy-resources overlay for JaCoCo CSS --- pom.xml | 17 ++ src/site/jacoco-resources/report.css | 299 +++++++++++++++++++++++++++ 2 files changed, 316 insertions(+) create mode 100644 src/site/jacoco-resources/report.css diff --git a/pom.xml b/pom.xml index 38d337ab3..eccc5ce8e 100644 --- a/pom.xml +++ b/pom.xml @@ -368,6 +368,23 @@ + + overlay-jacoco-css + site + + copy-resources + + + ${project.reporting.outputDirectory}/jacoco-coverage/jacoco-resources + + + src/site/jacoco-resources + false + + + true + + diff --git a/src/site/jacoco-resources/report.css b/src/site/jacoco-resources/report.css new file mode 100644 index 000000000..585ab6821 --- /dev/null +++ b/src/site/jacoco-resources/report.css @@ -0,0 +1,299 @@ +/* ===== Custom JaCoCo Report Theme ===== */ +/* Matches the Copilot SDK site design */ + +body, td { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + font-size: 10pt; + color: #24292f; + -webkit-font-smoothing: antialiased; +} + +body { + background: #f6f8fa; + margin: 0; + padding: 20px; +} + +h1 { + font-weight: 800; + font-size: 18pt; + color: #24292f; + margin-bottom: 16px; +} + +a { + color: #0969da; + text-decoration: none; +} + +a:hover { + color: #0550ae; + text-decoration: underline; +} + +/* ===== Breadcrumb ===== */ +.breadcrumb { + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; + padding: 10px 16px; + margin-bottom: 20px; +} + +.breadcrumb .info { + float: right; +} + +.breadcrumb .info a { + margin-left: 8px; + color: #57606a; + font-size: 0.9em; +} + +.breadcrumb .info a:hover { + color: #0969da; +} + +/* ===== Element Icons ===== */ +.el_report { + padding-left: 18px; + background-image: url(report.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_group { + padding-left: 18px; + background-image: url(group.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_bundle { + padding-left: 18px; + background-image: url(bundle.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_package { + padding-left: 18px; + background-image: url(package.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_class { + padding-left: 18px; + background-image: url(class.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_source { + padding-left: 18px; + background-image: url(source.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_method { + padding-left: 18px; + background-image: url(method.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_session { + padding-left: 18px; + background-image: url(session.gif); + background-position: left center; + background-repeat: no-repeat; +} + +/* ===== Source Code ===== */ +pre.source { + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + overflow-x: auto; +} + +pre.source ol { + margin-bottom: 0; + margin-top: 0; +} + +pre.source li { + border-left: 1px solid #d0d7de; + color: #8b949e; + padding-left: 0; +} + +pre.source span.fc { + background-color: #dafbe1; +} + +pre.source span.nc { + background-color: #ffeef0; +} + +pre.source span.pc { + background-color: #fff8c5; +} + +pre.source span.bfc { + background-image: url(branchfc.gif); + background-repeat: no-repeat; + background-position: 2px center; +} + +pre.source span.bfc:hover { + background-color: #aff5b4; +} + +pre.source span.bnc { + background-image: url(branchnc.gif); + background-repeat: no-repeat; + background-position: 2px center; +} + +pre.source span.bnc:hover { + background-color: #ffcecb; +} + +pre.source span.bpc { + background-image: url(branchpc.gif); + background-repeat: no-repeat; + background-position: 2px center; +} + +pre.source span.bpc:hover { + background-color: #fff2b2; +} + +/* ===== Coverage Table ===== */ +table.coverage { + empty-cells: show; + border-collapse: separate; + border-spacing: 0; + border: 1px solid #d0d7de; + border-radius: 10px; + overflow: hidden; + width: 100%; + background: #fff; +} + +table.coverage thead { + background: #f6f8fa; +} + +table.coverage thead td { + white-space: nowrap; + padding: 10px 14px 10px 10px; + border-bottom: 2px solid #d0d7de; + font-weight: 700; + color: #24292f; + font-size: 0.92em; +} + +table.coverage thead td.bar { + border-left: 1px solid #eaeef2; +} + +table.coverage thead td.ctr1 { + text-align: right; + border-left: 1px solid #eaeef2; +} + +table.coverage thead td.ctr2 { + text-align: right; + padding-left: 2px; +} + +table.coverage thead td.sortable { + cursor: pointer; + background-image: url(sort.gif); + background-position: right center; + background-repeat: no-repeat; +} + +table.coverage thead td.up { + background-image: url(up.gif); +} + +table.coverage thead td.down { + background-image: url(down.gif); +} + +table.coverage tbody td { + white-space: nowrap; + padding: 8px 10px; + border-bottom: 1px solid #eaeef2; +} + +table.coverage tbody tr:hover { + background: rgba(102, 126, 234, 0.04) !important; +} + +table.coverage tbody td.bar { + border-left: 1px solid #eaeef2; +} + +table.coverage tbody td.ctr1 { + text-align: right; + padding-right: 14px; + border-left: 1px solid #eaeef2; +} + +table.coverage tbody td.ctr2 { + text-align: right; + padding-right: 14px; + padding-left: 2px; +} + +table.coverage tfoot td { + white-space: nowrap; + padding: 8px 10px; + font-weight: 700; + background: #f6f8fa; + border-top: 2px solid #d0d7de; +} + +table.coverage tfoot td.bar { + border-left: 1px solid #eaeef2; +} + +table.coverage tfoot td.ctr1 { + text-align: right; + padding-right: 14px; + border-left: 1px solid #eaeef2; +} + +table.coverage tfoot td.ctr2 { + text-align: right; + padding-right: 14px; + padding-left: 2px; +} + +/* ===== Footer ===== */ +.footer { + margin-top: 24px; + border-top: 1px solid #d0d7de; + padding-top: 8px; + font-size: 8pt; + color: #8b949e; +} + +.footer a { + color: #8b949e; +} + +.footer a:hover { + color: #0969da; +} + +.right { + float: right; +} From 593ba44f39568c9f85301e149d6428da82460684 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 12 Feb 2026 22:45:35 -0500 Subject: [PATCH 120/147] Change GitHub ribbon text to 'View on GitHub' with purple gradient --- src/site/resources/css/site.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/site/resources/css/site.css b/src/site/resources/css/site.css index 80c717c0a..9da11e93a 100644 --- a/src/site/resources/css/site.css +++ b/src/site/resources/css/site.css @@ -65,7 +65,12 @@ code, pre { /* ===== GitHub Ribbon ===== */ .github-fork-ribbon:before { - background: linear-gradient(135deg, #667eea, #764ba2) !important; + background-color: #667eea !important; + background-image: linear-gradient(135deg, #667eea, #764ba2) !important; +} + +.github-fork-ribbon:after { + content: 'View on GitHub' !important; } /* ===== Breadcrumbs ===== */ From 3350d4c0822a905794017e53fb87944a37b17978 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Fri, 13 Feb 2026 06:29:47 -0500 Subject: [PATCH 121/147] Overlay custom JaCoCo CSS in deploy workflow for all versions --- .github/workflows/deploy-site.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 44ddf54f3..a86d5e17e 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -254,6 +254,16 @@ jobs: sed -i "s||$CURRENT_HTML|" index.html sed -i "s||$OLDER_HTML|" index.html + - name: Overlay custom JaCoCo CSS + run: | + cd site + for dir in */jacoco-coverage/jacoco-resources; do + if [ -d "$dir" ]; then + cp ../src/site/jacoco-resources/report.css "$dir/report.css" + echo "Overlaid JaCoCo CSS in $dir" + fi + done + - name: Deploy to GitHub Pages run: | cd site From a36fa1c4501229906139342bb65bb19e416076f2 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Fri, 13 Feb 2026 07:14:19 -0500 Subject: [PATCH 122/147] Fix JaCoCo CSS overlay path from jacoco-coverage/ to jacoco/ --- .github/workflows/deploy-site.yml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index a86d5e17e..b168ac45c 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -257,7 +257,7 @@ jobs: - name: Overlay custom JaCoCo CSS run: | cd site - for dir in */jacoco-coverage/jacoco-resources; do + for dir in */jacoco/jacoco-resources; do if [ -d "$dir" ]; then cp ../src/site/jacoco-resources/report.css "$dir/report.css" echo "Overlaid JaCoCo CSS in $dir" diff --git a/pom.xml b/pom.xml index eccc5ce8e..247b0e485 100644 --- a/pom.xml +++ b/pom.xml @@ -375,7 +375,7 @@ copy-resources - ${project.reporting.outputDirectory}/jacoco-coverage/jacoco-resources + ${project.reporting.outputDirectory}/jacoco/jacoco-resources src/site/jacoco-resources From 5b61dfc0f9878245098f2d84eea63943b14944f5 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Fri, 13 Feb 2026 07:23:03 -0500 Subject: [PATCH 123/147] Add documentation for custom site and JaCoCo CSS theming --- docs/CUSTOM_SITE_CSS.md | 187 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 docs/CUSTOM_SITE_CSS.md diff --git a/docs/CUSTOM_SITE_CSS.md b/docs/CUSTOM_SITE_CSS.md new file mode 100644 index 000000000..9181118f1 --- /dev/null +++ b/docs/CUSTOM_SITE_CSS.md @@ -0,0 +1,187 @@ +# Custom Site CSS + +This project applies custom CSS themes to both the **Maven-generated documentation site** and the **JaCoCo coverage reports** so they share a consistent visual identity: light backgrounds, GitHub-inspired colors, purple gradient accents, and rounded card-style containers. + +## Architecture Overview + +``` +src/site/ +├── resources/ +│ ├── css/ +│ │ └── site.css ← Maven site theme (loaded automatically by Fluido) +│ └── images/ +│ └── github-copilot.jpg ← Copilot logo for "Powered By" sidebar +├── jacoco-resources/ +│ └── report.css ← JaCoCo report theme (overlaid after generation) +└── site.xml ← Fluido skin configuration + +.github/ +├── templates/ +│ ├── index.html ← Version picker page template +│ └── styles.css ← Version picker CSS +└── workflows/ + └── deploy-site.yml ← Deploys site + overlays JaCoCo CSS +``` + +## Maven Site Theme (`src/site/resources/css/site.css`) + +### How It Works + +The Maven Fluido Skin 2.1.0 (configured in `src/site/site.xml`) automatically loads `css/site.css` from the site resources directory. No additional configuration is needed — placing the file at `src/site/resources/css/site.css` is sufficient. + +### Design Choices + +| Element | Style | +|---------|-------| +| **Navbar** | Dark (`#24292f`) with no gradient or border, subtle box shadow | +| **Sidebar** | White card with rounded corners (`border-radius: 10px`), 1px border | +| **Active nav item** | Purple gradient (`#667eea → #764ba2`) | +| **Links** | GitHub-blue (`#0969da`), hover darkens to `#0550ae` | +| **Code blocks** | Light gray background (`#eef1f6`) to distinguish from the white page | +| **Inline code** | Tinted purple background (`rgba(102, 126, 234, 0.1)`) | +| **Tables** | Rounded borders, light header, subtle row hover | +| **Sections** | White card with border and padding (nested sections are transparent) | +| **Alerts** | Rounded with 4px left accent border, color-coded (info/warning/danger/success) | +| **Typography** | System font stack (`-apple-system, BlinkMacSystemFont, 'Segoe UI', ...`) | + +### GitHub Ribbon Override + +The Fluido skin renders a "Fork me on GitHub" ribbon using a `::after` pseudo-element with `content: attr(data-ribbon)`. We override this to say "View on GitHub" with a purple gradient background: + +```css +.github-fork-ribbon:before { + background-color: #667eea !important; + background-image: linear-gradient(135deg, #667eea, #764ba2) !important; +} + +.github-fork-ribbon:after { + content: 'View on GitHub' !important; +} +``` + +> **Note:** Both `background-color` and `background-image` with `!important` are required because Fluido injects an inline `