diff --git a/.gitattributes b/.gitattributes index 3982c9ad9a59..988d7a1f8843 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,5 @@ *.cmd text eol=crlf licenses/** linguist-generated + +.github/workflows/*.lock.yml linguist-generated=true merge=ours diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json new file mode 100644 index 000000000000..415337d210ea --- /dev/null +++ b/.github/aw/actions-lock.json @@ -0,0 +1,14 @@ +{ + "entries": { + "actions/github-script@v9.0.0": { + "repo": "actions/github-script", + "version": "v9.0.0", + "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3" + }, + "github/gh-aw-actions/setup@v0.71.5": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.71.5", + "sha": "b8068426813005612b960b5ab0b8bd2c27142323" + } + } +} diff --git a/.github/scripts/module-cleanup/build-cleanup-matrix.py b/.github/scripts/module-cleanup/build-cleanup-matrix.py index 90a22fea1e30..909cb68e73c8 100644 --- a/.github/scripts/module-cleanup/build-cleanup-matrix.py +++ b/.github/scripts/module-cleanup/build-cleanup-matrix.py @@ -1,63 +1,60 @@ #!/usr/bin/env python3 -"""Build the ordered list of instrumentation modules for this review run. +"""Pick the next instrumentation module for this cleanup run. -Reads module list from settings.gradle.kts, filters out already-reviewed -modules (read from the otelbot/module-cleanup-progress branch by the workflow -and passed via REVIEW_PROGRESS), respects the open-PR cap, and writes a -`modules` JSON array + `has_work` flag to $GITHUB_OUTPUT. +Reads the module list from settings.gradle.kts, filters out already-processed +modules (passed via REVIEW_PROGRESS), and emits a single module to walk this +run plus a count of how many unprocessed modules remain after it. -The review job processes modules sequentially on a single branch, stopping -after it accumulates at least `FILE_THRESHOLD` modified files, so the list -emitted here is an upper-bound slice the job is allowed to walk through. +The workflow chains itself one module at a time. The finalize step uses +`queue_remaining` to decide whether to self-dispatch or flush the pending +queue into a PR. Environment variables: - GITHUB_OUTPUT - path to the GitHub Actions output file - GH_TOKEN - token for `gh` CLI (set automatically by the workflow) - REVIEW_PROGRESS - newline-separated list of reviewed module names - (contents of reviewed.txt on the progress branch) + GITHUB_OUTPUT - path to the GitHub Actions output file + GH_TOKEN - token for `gh` CLI (set automatically by the workflow) + REVIEW_PROGRESS - newline-separated list of processed module names + (contents of processed.txt on the memory branch, plus + shorts already in inflight module-cleanup PR bodies) + +Outputs (to $GITHUB_OUTPUT): + has_work - "true" if a module was picked, "false" otherwise + short_name - picked module's gradle short name (e.g. "akka-actor:javaagent") + module_dir - picked module's repo-relative directory + queue_remaining - count of unprocessed modules left AFTER this one """ -import json import os import re import subprocess from pathlib import Path SETTINGS_FILE = "settings.gradle.kts" -# Skip the run entirely if at least this many automated review PRs are already open. +# Skip the run entirely if at least this many module-cleanup PRs are already open. MAX_OPEN_PRS = 5 -# Upper bound on modules the review job will walk through in a single run, -# even if the file-count threshold is never reached. Keeps one run bounded. -MODULE_LIMIT_PER_RUN = 50 def parse_modules() -> list[tuple[str, str]]: """Return list of (gradle_name, module_dir) from settings.gradle.kts.""" text = Path(SETTINGS_FILE).read_text(encoding="utf-8") - # Match include(":instrumentation:activej-http:6.0:javaagent") raw = re.findall(r'include\(":instrumentation:([^"]+)"\)', text) pairs = [] for entry in sorted(raw): parts = entry.split(":") - # Skip shared/helper modules (e.g. "cdi-testing") that don't follow the - # : layout used for real instrumentation modules. if len(parts) < 2: continue module_dir = "instrumentation/" + "/".join(parts) - # Gradle module name: second-to-last:last gradle_name = f"{parts[-2]}:{parts[-1]}" pairs.append((gradle_name, module_dir)) return pairs -def load_reviewed() -> set[str]: - """Load already-reviewed module names from the REVIEW_PROGRESS env var.""" +def load_processed() -> set[str]: + """Load already-processed module names from the REVIEW_PROGRESS env var.""" progress = os.environ.get("REVIEW_PROGRESS", "") return {line.strip() for line in progress.splitlines() if line.strip()} def count_open_prs() -> int: - """Count open PRs with the module cleanup label.""" result = subprocess.run( ["gh", "pr", "list", "--label", "module cleanup", "--state", "open", "--json", "number", "--jq", "length"], @@ -67,46 +64,49 @@ def count_open_prs() -> int: def write_output(key: str, value: str) -> None: - """Append a key=value to $GITHUB_OUTPUT. Values must not contain newlines.""" assert "\n" not in value, f"multi-line $GITHUB_OUTPUT value not supported: {value!r}" with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f: f.write(f"{key}={value}\n") +def emit_no_work() -> None: + write_output("has_work", "false") + write_output("short_name", "") + write_output("module_dir", "") + write_output("queue_remaining", "0") + + def main() -> None: all_modules = parse_modules() print(f"Total instrumentation modules: {len(all_modules)}") - reviewed = load_reviewed() - print(f"Already reviewed: {len(reviewed)}") + processed = load_processed() + print(f"Already processed: {len(processed)}") - remaining = [(name, d) for name, d in all_modules if name not in reviewed] + remaining = [(n, d) for n, d in all_modules if n not in processed] print(f"Remaining modules: {len(remaining)}") if not remaining: - print("All modules have been reviewed!") - write_output("has_work", "false") - write_output("modules", "[]") + print("All modules have been processed!") + emit_no_work() return open_prs = count_open_prs() - print(f"Open review PRs: {open_prs}") - + print(f"Open module-cleanup PRs: {open_prs}") if open_prs >= MAX_OPEN_PRS: print(f"PR cap reached ({open_prs} open >= {MAX_OPEN_PRS}). Skipping this cycle.") - write_output("has_work", "false") - write_output("modules", "[]") + emit_no_work() return - batch = remaining[:MODULE_LIMIT_PER_RUN] - print(f"Dispatching {len(batch)} modules (upper bound for this run)") - - modules = [{"short_name": name, "module_dir": d} for name, d in batch] - modules_json = json.dumps(modules) - print(json.dumps(modules, indent=2)) + short_name, module_dir = remaining[0] + queue_remaining = len(remaining) - 1 + print(f"Picked: {short_name} ({module_dir})") + print(f"Queue remaining after this run: {queue_remaining}") write_output("has_work", "true") - write_output("modules", modules_json) + write_output("short_name", short_name) + write_output("module_dir", module_dir) + write_output("queue_remaining", str(queue_remaining)) if __name__ == "__main__": diff --git a/.github/scripts/module-cleanup/cleanup-loop.py b/.github/scripts/module-cleanup/cleanup-loop.py deleted file mode 100644 index eee335da2349..000000000000 --- a/.github/scripts/module-cleanup/cleanup-loop.py +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/env python3 -"""Walk instrumentation modules sequentially, applying Copilot cleanup fixes. - -Invoked by the `module-cleanup` workflow's `Run Copilot cleanup loop` step. -Reads the dispatch list from MODULES_JSON, invokes the `copilot` CLI per -module, squashes each module's edits into a single commit on the current -branch, and stops once the total number of files modified versus `origin/main` -reaches FILE_THRESHOLD. - -Per-module diagnostics (raw JSONL, extracted report, final assistant message, -JSONL diagnostics, and a PR body fragment) are written under COPILOT_ROOT, -and the ordered list of modules actually processed (including no-op ones) is -written to PROCESSED_MODULES so the workflow can append them to `reviewed.txt`. - -Environment variables (all required): - MODULES_JSON - JSON array of {short_name, module_dir} - FILE_THRESHOLD - integer; stop after this many modified files - MODEL - Copilot model name to pass via `--model` - COPILOT_ROOT - directory for per-module diagnostics - FRAGMENTS_DIR - directory for per-module PR body fragments - PROCESSED_MODULES - path to file receiving processed short_names - COPILOT_REVIEW_PROMPT_TEMPLATE - prompt with `__MODULE_DIR__` placeholder - COPILOT_GITHUB_TOKEN - Copilot CLI auth token - GITHUB_OUTPUT - GitHub Actions output file -""" - -from __future__ import annotations - -import json -import os -import subprocess -import sys -from pathlib import Path - -SCRIPTS_DIR = Path(__file__).resolve().parent -EXTRACT_REPORT_SCRIPT = SCRIPTS_DIR / "extract-report.py" -JSONL_DIAGNOSTICS_SCRIPT = SCRIPTS_DIR / "jsonl-diagnostics.py" -PR_BODY_SCRIPT = SCRIPTS_DIR / "pr-body.py" - - -def require_env(name: str) -> str: - value = os.environ.get(name) - if value is None or value == "": - raise SystemExit(f"Missing required environment variable: {name}") - return value - - -def write_github_output(key: str, value: str) -> None: - with open(require_env("GITHUB_OUTPUT"), "a", encoding="utf-8") as f: - f.write(f"{key}={value}\n") - - -def run_git(*args: str, capture: bool = False) -> subprocess.CompletedProcess: - return subprocess.run( - ["git", *args], - check=True, - text=True, - capture_output=capture, - ) - - -def current_head_sha() -> str: - return run_git("rev-parse", "HEAD", capture=True).stdout.strip() - - -def count_modified_files_vs_main() -> int: - result = run_git("diff", "--name-only", "origin/main", capture=True) - return sum(1 for line in result.stdout.splitlines() if line.strip()) - - -def count_commits_since_main() -> int: - result = run_git("rev-list", "--count", "origin/main..HEAD", capture=True) - return int(result.stdout) - - -def staged_changes_present() -> bool: - # `git diff --cached --quiet` exits 0 if no staged changes, 1 if there are. - return subprocess.run(["git", "diff", "--cached", "--quiet"], check=False).returncode != 0 - - -def run_copilot(prompt: str, model: str, output_path: Path) -> int: - """Invoke the Copilot CLI, streaming JSONL to `output_path`. Returns exit code.""" - with output_path.open("wb") as out: - # `--yolo` disables Copilot CLI confirmation prompts for tool calls. - # This is intentional for autonomous CI and must not be copied into - # interactive/local scripts without review. - proc = subprocess.run( - [ - "copilot", - "-p", prompt, - "--agent", "module-cleanup", - "--model", model, - "--output-format", "json", - "--silent", - "--stream", "off", - "--yolo", - ], - stdout=out, - check=False, - # stderr inherits the workflow's stderr so Actions logs show live output. - ) - return proc.returncode - - -def run_extract_report( - *, - copilot_output: Path, - final_message: Path, - review_report: Path, -) -> int: - return subprocess.run( - [ - sys.executable, str(EXTRACT_REPORT_SCRIPT), - "--input", str(copilot_output), - "--final-message-output", str(final_message), - "--output", str(review_report), - ], - check=False, - ).returncode - - -def write_diagnostics(copilot_output: Path, diagnostics: Path) -> None: - with diagnostics.open("wb") as out: - subprocess.run( - [sys.executable, str(JSONL_DIAGNOSTICS_SCRIPT), "--input", str(copilot_output)], - stdout=out, - check=False, - ) - - -def render_pr_body_fragment( - *, - fragment_path: Path, - short_name: str, - module_dir: str, - review_report: Path, -) -> None: - body_file = fragment_path.with_suffix(fragment_path.suffix + ".body") - subprocess.run( - [ - sys.executable, str(PR_BODY_SCRIPT), - "--input", str(review_report), - "--output", str(body_file), - ], - check=True, - ) - header = ( - f"## Module: `{short_name}`\n" - f"\n" - f"_Module path: `{module_dir}`_\n" - f"\n" - ) - fragment_path.write_text(header + body_file.read_text(encoding="utf-8"), encoding="utf-8") - body_file.unlink() - - -def process_module( - *, - short_name: str, - module_dir: str, - fragment_index: int, - copilot_root: Path, - fragments_dir: Path, - processed_modules_file: Path, - model: str, - prompt_template: str, -) -> bool: - """Process one module. - - Returns True if the module ran to completion (even with zero edits), - False if Copilot or report extraction failed and the module should be - retried on a future run. - """ - slug = short_name.replace(":", "-") - work_dir = copilot_root / slug - work_dir.mkdir(parents=True, exist_ok=True) - - copilot_output = work_dir / "copilot-output.jsonl" - final_message = work_dir / "final-assistant-message.txt" - review_report = work_dir / "review-report.json" - diagnostics = work_dir / "diagnostics.txt" - - print(f"::group::Copilot review ({model}) for {module_dir}", flush=True) - try: - prompt = prompt_template.replace("__MODULE_DIR__", module_dir) - pre_run_sha = current_head_sha() - - copilot_rc = run_copilot(prompt, model, copilot_output) - - # Diagnostics are best-effort and should be produced even if Copilot failed. - write_diagnostics(copilot_output, diagnostics) - - if copilot_rc != 0: - print( - f"Copilot invocation for {short_name} failed with exit {copilot_rc};" - " skipping (module will be retried next run)." - ) - run_git("reset", "--hard", pre_run_sha) - return False - - extract_rc = run_extract_report( - copilot_output=copilot_output, - final_message=final_message, - review_report=review_report, - ) - if extract_rc != 0: - print( - f"Report extraction failed for {short_name} (exit {extract_rc});" - " discarding edits and skipping." - ) - run_git("reset", "--hard", pre_run_sha) - return False - - # Squash whatever Copilot committed for this module into one commit on our branch. - run_git("reset", "--soft", pre_run_sha) - run_git("add", "-A") - - # Record this module as processed regardless of whether it produced edits, - # so no-op modules aren't re-walked on the next run. - with processed_modules_file.open("a", encoding="utf-8") as f: - f.write(short_name + "\n") - - fragment_path = fragments_dir / f"{fragment_index:03d}-{slug}.md" - render_pr_body_fragment( - fragment_path=fragment_path, - short_name=short_name, - module_dir=module_dir, - review_report=review_report, - ) - - if staged_changes_present(): - run_git( - "commit", - "-m", f"Cleanup for {short_name}", - "-m", f"Automated module cleanup of {module_dir}.", - ) - else: - print(f"No edits from {short_name}; not committing.") - - total_changed = count_modified_files_vs_main() - print(f"Total modified files so far: {total_changed}") - return True - finally: - print("::endgroup::", flush=True) - - -def main() -> None: - modules_json = require_env("MODULES_JSON") - file_threshold = int(require_env("FILE_THRESHOLD")) - model = require_env("MODEL") - copilot_root = Path(require_env("COPILOT_ROOT")) - fragments_dir = Path(require_env("FRAGMENTS_DIR")) - processed_modules_file = Path(require_env("PROCESSED_MODULES")) - prompt_template = require_env("COPILOT_REVIEW_PROMPT_TEMPLATE") - - modules: list[dict[str, str]] = json.loads(modules_json) - - copilot_root.mkdir(parents=True, exist_ok=True) - fragments_dir.mkdir(parents=True, exist_ok=True) - processed_modules_file.write_text("", encoding="utf-8") - - print(f"Modules to walk (upper bound for this run): {len(modules)}") - - fragment_index = 0 - for module in modules: - processed = process_module( - short_name=module["short_name"], - module_dir=module["module_dir"], - fragment_index=fragment_index, - copilot_root=copilot_root, - fragments_dir=fragments_dir, - processed_modules_file=processed_modules_file, - model=model, - prompt_template=prompt_template, - ) - if processed: - fragment_index += 1 - - total_changed = count_modified_files_vs_main() - if total_changed >= file_threshold: - print( - f"Reached file threshold ({total_changed} >= {file_threshold});" - " stopping loop." - ) - break - - processed_lines = processed_modules_file.read_text(encoding="utf-8").splitlines() - processed_count = sum(1 for line in processed_lines if line.strip()) - commits_on_branch = count_commits_since_main() - print(f"Processed modules: {processed_count}") - print(f"Commits on cleanup branch: {commits_on_branch}") - write_github_output("processed_count", str(processed_count)) - write_github_output("commits_on_branch", str(commits_on_branch)) - - -if __name__ == "__main__": - main() diff --git a/.github/scripts/module-cleanup/export-cleanup-patch.sh b/.github/scripts/module-cleanup/export-cleanup-patch.sh new file mode 100644 index 000000000000..1a036b7bab59 --- /dev/null +++ b/.github/scripts/module-cleanup/export-cleanup-patch.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Final action invoked by the LLM agent: format-patch the cleanup commit +# range into /tmp/gh-aw/agent/cleanup.patch so gh-aw's auto-uploader +# includes it in the `agent` workflow artifact. The finalize job then +# downloads that artifact and applies the patch onto module-cleanup-wip. +# +# Idempotent and write-only to /tmp. Does NOT push anything. +# +# Args: +# $1 - module short_name (used for logging only) + +set -euo pipefail + +SHORT="${1:?short_name argument required}" +OUT_DIR="${OUT_DIR:-/tmp/gh-aw/agent}" +mkdir -p "$OUT_DIR" + +if ! git rev-parse --verify origin/main >/dev/null 2>&1; then + git fetch origin main --depth=1 || true +fi + +if [ -z "$(git log origin/main..HEAD --oneline 2>/dev/null || true)" ]; then + echo "No commit produced by agent for $SHORT; nothing to export." + exit 0 +fi + +# Capture every commit the persona made on top of main. The persona is +# expected to produce exactly one commit per its Phase 5 contract, but +# format-patch range-form is robust if it makes more than one. +git format-patch origin/main..HEAD --stdout > "$OUT_DIR/cleanup.patch" +echo "Wrote cleanup patch for $SHORT to $OUT_DIR/cleanup.patch" diff --git a/.github/scripts/module-cleanup/extract-report.py b/.github/scripts/module-cleanup/extract-report.py deleted file mode 100644 index 32c65b1c0cb3..000000000000 --- a/.github/scripts/module-cleanup/extract-report.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 -"""Extract the final assistant message from review CLI JSONL output.""" - -from __future__ import annotations - -import argparse -import json -from pathlib import Path - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - parser.add_argument("--input", required=True) - parser.add_argument("--output", required=True) - parser.add_argument("--final-message-output") - return parser.parse_args() - - -def strip_json_fence(value: str) -> str: - stripped = value.strip() - lines = stripped.splitlines() - if ( - len(lines) >= 3 - and lines[0].strip().lower() in {"```", "```json"} - and lines[-1].strip().startswith("```") - ): - return "\n".join(lines[1:-1]).strip() - return stripped - - -def extract_final_message(path: Path) -> str: - final_message: str | None = None - - with path.open(encoding="utf-8") as handle: - for raw_line in handle: - line = raw_line.strip() - if not line: - continue - event = json.loads(line) - if event.get("type") != "assistant.message": - continue - content = event["data"]["content"] - if isinstance(content, str): - final_message = content - - if not final_message or not final_message.strip(): - raise ValueError("Review output did not contain a non-empty assistant.message") - - return final_message.strip() - - -def validate_report_json(report: str) -> dict[str, object]: - parsed = json.loads(strip_json_fence(report)) - if not isinstance(parsed, dict): - raise ValueError(f"Review report must be a JSON object, got {type(parsed).__name__}") - return parsed - - -def main() -> None: - args = parse_args() - report = extract_final_message(Path(args.input)) - - # Write the raw final message *before* JSON validation so that, on validation - # failure, the diagnostic artifact is preserved for post-mortem. In that case - # `--output` is intentionally not produced and the caller treats the run as failed. - if args.final_message_output: - Path(args.final_message_output).write_text(report + "\n", encoding="utf-8") - - parsed = validate_report_json(report) - Path(args.output).write_text(json.dumps(parsed, indent=2) + "\n", encoding="utf-8") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/.github/scripts/module-cleanup/finalize.sh b/.github/scripts/module-cleanup/finalize.sh new file mode 100644 index 000000000000..df7872b88e3a --- /dev/null +++ b/.github/scripts/module-cleanup/finalize.sh @@ -0,0 +1,222 @@ +#!/bin/bash +# Finalize: single writer for both module-cleanup-wip and the +# memory/module-cleanup branch. Runs after the agent job (regardless of +# whether the agent succeeded, no-oped, or failed). +# +# Steps: +# 1. Append to memory/module-cleanup:processed.txt; if the +# agent failed, also append to failed.txt. This guarantees a failing +# module is recorded as "processed" (so it isn't retried in a loop) +# AND logged as a failure for diagnostics. +# 2. If the agent produced a cleanup patch, apply it onto the fixed +# module-cleanup-wip branch and push. +# 3. If wip diff vs origin/main has reached FLUSH_THRESHOLD files OR +# the queue is empty, atomically rename wip to a +# module-cleanup-batch- branch and open the PR. The wip +# branch ceases to exist on remote until the next run recreates +# it from main. +# 4. Self-dispatch the workflow unless the queue is empty. The chain +# stops on its own once MAX_OPEN_PRS is reached (matrix script +# returns has_work=false and finalize is skipped). +# +# No rebase-retry loops on push: the workflow uses +# concurrency.group=module-cleanup with cancel-in-progress=false, so this +# job is the only writer of either branch and runs serialized across +# workflow runs. +# +# Required env: +# GH_TOKEN - token with contents:write, pull-requests:write, +# and actions:write +# GITHUB_REPOSITORY - owner/repo +# SHORT_NAME - the module short_name processed this run +# AGENT_RESULT - github.needs.agent.result ('success'|'failure'|...) +# ARTIFACT_DIR - directory of the downloaded `agent` artifact +# (may or may not contain cleanup.patch) +# QUEUE_REMAINING - count of unprocessed modules left after this one +# +# Optional env: +# FLUSH_THRESHOLD - file count that triggers a PR (default 10) +# WORKFLOW_FILE - workflow file name for self-dispatch +# MEMORY_BRANCH - default: memory/module-cleanup +# WIP_BRANCH - default: module-cleanup-wip + +set -euo pipefail + +MEMORY_BRANCH="${MEMORY_BRANCH:-memory/module-cleanup}" +WIP_BRANCH="${WIP_BRANCH:-module-cleanup-wip}" +THRESHOLD="${FLUSH_THRESHOLD:-10}" +QUEUE_REMAINING="${QUEUE_REMAINING:-0}" +REPO="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY required}" +WORKFLOW_FILE="${WORKFLOW_FILE:-module-cleanup.lock.yml}" +SHORT="${SHORT_NAME:?SHORT_NAME required}" +AGENT_RESULT="${AGENT_RESULT:-failure}" +ARTIFACT_DIR="${ARTIFACT_DIR:-./agent-artifact}" + +git fetch origin main --depth=1 +git fetch origin "$MEMORY_BRANCH" --depth=1 2>/dev/null || true +git fetch origin "$WIP_BRANCH" --depth=1 2>/dev/null || true + +# ---- 1. Update processed.txt (and failed.txt on failure) ---- + +MEM_WT=/tmp/memory-wt +rm -rf "$MEM_WT" +if git rev-parse --verify "origin/$MEMORY_BRANCH" >/dev/null 2>&1; then + git worktree add -B "$MEMORY_BRANCH" "$MEM_WT" "origin/$MEMORY_BRANCH" +else + git worktree add --orphan -B "$MEMORY_BRANCH" "$MEM_WT" + rm -rf "$MEM_WT"/* +fi + +PROCESSED="$MEM_WT/processed.txt" +FAILED="$MEM_WT/failed.txt" + +touch "$PROCESSED" +if ! grep -Fxq "$SHORT" "$PROCESSED"; then + echo "$SHORT" >> "$PROCESSED" +fi + +if [ "$AGENT_RESULT" != "success" ]; then + ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) + echo -e "$SHORT\t$ts\tagent_result=$AGENT_RESULT" >> "$FAILED" +fi + +( + cd "$MEM_WT" + git add -A + if ! git diff --cached --quiet; then + git commit -m "Mark $SHORT processed (agent_result=$AGENT_RESULT)" + git push origin "$MEMORY_BRANCH" + fi +) + +# ---- 2. Apply cleanup patch (if any) onto wip ---- + +PATCH_SRC="" +for candidate in \ + "$ARTIFACT_DIR/agent/cleanup.patch" \ + "$ARTIFACT_DIR/tmp/gh-aw/agent/cleanup.patch" \ + "$ARTIFACT_DIR/cleanup.patch"; do + if [ -f "$candidate" ]; then + # Absolute path so the value survives the cd into $WIP_WT below. + PATCH_SRC="$(realpath "$candidate")" + echo "Found cleanup patch at $PATCH_SRC" + break + fi +done +if [ -z "$PATCH_SRC" ]; then + echo "No cleanup.patch (no-op or agent failed before commit)." +fi + +WIP_WT=/tmp/wip-wt +rm -rf "$WIP_WT" +if git rev-parse --verify "origin/$WIP_BRANCH" >/dev/null 2>&1; then + git worktree add -B "$WIP_BRANCH" "$WIP_WT" "origin/$WIP_BRANCH" +else + git worktree add -B "$WIP_BRANCH" "$WIP_WT" origin/main +fi + +if [ -n "$PATCH_SRC" ]; then + ( + cd "$WIP_WT" + if git am --3way "$PATCH_SRC"; then + echo "Applied cleanup for $SHORT to $WIP_BRANCH" + git push origin "$WIP_BRANCH" + else + git am --abort 2>/dev/null || true + echo "FAILED to apply cleanup for $SHORT (rebase conflict)." + ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) + ( + cd "$MEM_WT" + echo -e "$SHORT\t$ts\tgit am failed (rebase conflict)" >> "$FAILED" + git add -A + git commit -m "Record $SHORT as patch-conflict failure" + git push origin "$MEMORY_BRANCH" || true + ) + fi + ) +fi + +git fetch origin "$WIP_BRANCH" --depth=50 2>/dev/null || true + +# ---- 3. Decide flush ---- + +if git rev-parse --verify "origin/$WIP_BRANCH" >/dev/null 2>&1; then + FILE_COUNT=$(git diff --name-only origin/main "origin/$WIP_BRANCH" | wc -l) + AHEAD=$(git rev-list --count "origin/main..origin/$WIP_BRANCH") +else + FILE_COUNT=0 + AHEAD=0 +fi + +echo "wip ahead of main: $AHEAD commit(s), $FILE_COUNT file(s)" +echo "queue remaining: $QUEUE_REMAINING" +echo "threshold: $THRESHOLD" + +SHOULD_FLUSH=false +if [ "$AHEAD" -gt 0 ]; then + if [ "$FILE_COUNT" -ge "$THRESHOLD" ]; then + SHOULD_FLUSH=true + echo "Flushing: file count >= threshold." + elif [ "$QUEUE_REMAINING" -eq 0 ]; then + SHOULD_FLUSH=true + echo "Flushing: queue exhausted." + fi +fi + +OPENED_PR=false +if [ "$SHOULD_FLUSH" = "true" ]; then + RUN_ID="${GITHUB_RUN_ID:-$(date -u +%Y%m%d%H%M%S)}" + BATCH_BRANCH="module-cleanup-batch-$RUN_ID" + + BODY_FILE=$(mktemp) + { + echo "Automated module-cleanup batch." + echo + echo "## Modules in this batch" + echo + git -C "$WIP_WT" log "origin/main..origin/$WIP_BRANCH" \ + --reverse --format='- `%s`' \ + | sed 's|^- `Cleanup for |- `|' + echo + echo "---" + echo + git -C "$WIP_WT" log "origin/main..origin/$WIP_BRANCH" \ + --reverse --format='## %s%n%n%b%n' + } > "$BODY_FILE" + + # Atomic rename: in one push, create the batch branch at wip's tip + # and delete the wip branch. Either both succeed or both fail, so we + # never leave wip and batch pointing at the same commits. + git push --atomic origin \ + "refs/remotes/origin/$WIP_BRANCH:refs/heads/$BATCH_BRANCH" \ + ":refs/heads/$WIP_BRANCH" + + gh pr create \ + --repo "$REPO" \ + --base main \ + --head "$BATCH_BRANCH" \ + --title "Module cleanup: batch (run $RUN_ID)" \ + --body-file "$BODY_FILE" \ + --label "module cleanup" + + OPENED_PR=true +fi + +# ---- 4. Self-dispatch ---- + +# Always self-dispatch when there's more queued work. The next run will +# pick up wherever wip is: if we just flushed, wip is gone and the run +# starts a fresh wip from main; otherwise it appends to the same wip. +# The chain stops on its own when build-cleanup-matrix.py sees +# MAX_OPEN_PRS reached and returns has_work=false (no agent, no +# finalize, no self-dispatch). Cron picks back up later. + +if [ "$QUEUE_REMAINING" -le 0 ]; then + echo "Queue empty; nothing to dispatch." +else + echo "Self-dispatching workflow for next module." + gh workflow run "$WORKFLOW_FILE" --repo "$REPO" --ref main +fi + +git worktree remove --force "$MEM_WT" 2>/dev/null || true +git worktree remove --force "$WIP_WT" 2>/dev/null || true diff --git a/.github/scripts/module-cleanup/jsonl-diagnostics.py b/.github/scripts/module-cleanup/jsonl-diagnostics.py deleted file mode 100644 index 400fa75d491d..000000000000 --- a/.github/scripts/module-cleanup/jsonl-diagnostics.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python3 -"""Print concise diagnostics for Copilot review CLI JSONL output.""" - -from __future__ import annotations - -import argparse -import json -from collections import Counter, deque -from pathlib import Path - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - parser.add_argument("--input", required=True) - parser.add_argument("--tail", type=int, default=15) - return parser.parse_args() - - -def collapse(value: str, limit: int = 240) -> str: - collapsed = " ".join(value.split()) - if len(collapsed) <= limit: - return collapsed - return collapsed[: limit - 3] + "..." - - -def summarize_field(value: object) -> str: - if isinstance(value, str): - return collapse(value) - if isinstance(value, bool): - return str(value).lower() - if isinstance(value, (int, float)): - return str(value) - if value is None: - return "null" - if isinstance(value, list): - return f"list[{len(value)}]" - if isinstance(value, dict): - return f"object({', '.join(sorted(value.keys())[:6])})" - return type(value).__name__ - - -def summarize_event(event: dict[str, object]) -> str: - parts: list[str] = [] - data = event.get("data") - - for key in ("type", "id"): - if key in event: - parts.append(f"{key}={summarize_field(event[key])}") - - if isinstance(data, dict): - for key in ( - "role", - "name", - "tool", - "toolName", - "command", - "status", - "exitCode", - "exit_code", - "content", - "error", - "message", - ): - if key in data: - parts.append(f"{key}={summarize_field(data[key])}") - if len(parts) <= 2: - parts.append(f"dataKeys={','.join(sorted(data.keys())[:8])}") - - return "; ".join(parts) - - -def main() -> None: - args = parse_args() - path = Path(args.input) - - if not path.exists(): - print(f"Diagnostics input missing: {path}") - return - - raw_lines = path.read_text(encoding="utf-8").splitlines() - print(f"Diagnostics for {path}") - print(f"Line count: {len(raw_lines)}") - - event_types: Counter[str] = Counter() - assistant_messages: list[str] = [] - tail_events: deque[tuple[int, str]] = deque(maxlen=args.tail) - - for line_number, raw_line in enumerate(raw_lines, start=1): - line = raw_line.strip() - if not line: - continue - event = json.loads(line) - - event_type = str(event.get("type", "")) - event_types[event_type] += 1 - - data = event.get("data") - if event_type == "assistant.message" and isinstance(data, dict): - content = data.get("content") - if isinstance(content, str) and content.strip(): - assistant_messages.append(collapse(content, limit=500)) - - tail_events.append((line_number, summarize_event(event))) - - print("Event types:") - for event_type, count in event_types.most_common(): - print(f" {event_type}: {count}") - - if assistant_messages: - print("Last assistant message:") - print(f" {assistant_messages[-1]}") - else: - print("Last assistant message:") - print(" ") - - print(f"Last {len(tail_events)} events:") - for line_number, summary in tail_events: - print(f" line {line_number}: {summary}") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/.github/scripts/module-cleanup/pr-body.py b/.github/scripts/module-cleanup/pr-body.py deleted file mode 100644 index 1156954d966b..000000000000 --- a/.github/scripts/module-cleanup/pr-body.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -"""Render a PR body from a structured review report.""" - -from __future__ import annotations - -import argparse -import json -from pathlib import Path - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - parser.add_argument("--input", required=True) - parser.add_argument("--output", required=True) - return parser.parse_args() - - -def strip_code_fences(raw: str) -> str: - lines = raw.strip().splitlines() - if len(lines) >= 3 and lines[0].startswith("```") and lines[-1].startswith("```"): - return "\n".join(lines[1:-1]).strip() - return raw.strip() - - -def load_report(path: Path) -> dict: - return json.loads(strip_code_fences(path.read_text(encoding="utf-8"))) - - -# Characters that can inject Markdown/HTML structure when LLM-supplied text -# ends up as PR body prose. Escape them so headings/links/emphasis/HTML can't -# be forged. -_MARKDOWN_METACHARS = r"\`*_{}[]()#+-.!|<>" -_MARKDOWN_TRANSLATION = str.maketrans({c: "\\" + c for c in _MARKDOWN_METACHARS}) - - -def escape_markdown(value: str) -> str: - return " ".join(value.translate(_MARKDOWN_TRANSLATION).split()).strip() - - -def render_path(path: str, line_hint: object) -> str: - file_name = Path(path).name - if isinstance(line_hint, int): - return f"`{file_name}:{line_hint}`" - return f"`{file_name}`" - - -def group_changes_by_category(changes: list[dict]) -> list[tuple[str, list[dict]]]: - grouped: dict[str, list[dict]] = {} - ordered_categories: list[str] = [] - for change in changes: - category = escape_markdown(change["category"]) - if category not in grouped: - grouped[category] = [] - ordered_categories.append(category) - grouped[category].append(change) - return [(category, grouped[category]) for category in ordered_categories] - - -def render_change(change: dict) -> list[str]: - return [ - f"**File:** {render_path(change['path'], change.get('line_hint'))} ", - f"**Change:** {escape_markdown(change['change'])} ", - f"**Reason:** {escape_markdown(change['reason'])}", - "", - ] - - -def render_unresolved_item(item: dict) -> list[str]: - return [ - f"**File:** {render_path(item['path'], None)} ", - f"**Reason:** {escape_markdown(item['reason'])}", - "", - ] - - -def render_body(report: dict) -> str: - lines = [ - "### Summary", - "", - report["summary"], - "", - "### Applied Changes", - "", - ] - - changes = report.get("changes") or [] - if changes: - for category, category_changes in group_changes_by_category(changes): - lines.extend([f"#### {category}", ""]) - for change in category_changes: - lines.extend(render_change(change)) - else: - lines.extend(["No safe automated changes were applied.", ""]) - - unresolved = report.get("unresolved") or [] - if unresolved: - lines.extend(["### Unresolved Items", ""]) - for item in unresolved: - lines.extend(render_unresolved_item(item)) - - return "\n".join(lines) - - -def main() -> None: - args = parse_args() - report = load_report(Path(args.input)) - Path(args.output).write_text(render_body(report), encoding="utf-8") - - -if __name__ == "__main__": - main() diff --git a/.github/workflows/module-cleanup.lock.yml b/.github/workflows/module-cleanup.lock.yml new file mode 100644 index 000000000000..f7c127a45c74 --- /dev/null +++ b/.github/workflows/module-cleanup.lock.yml @@ -0,0 +1,1072 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"d5d15125c0301304e52468b6e595abe8bd4aef5f97417e1f9c194dc9c2c4d9f0","compiler_version":"v0.71.5","agent_id":"copilot","agent_model":"${{ vars.MODULE_CLEANUP_MODEL || 'gpt-5' }}"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"de0fac2e4500dabe0009e67214ff5f5447ce83dd"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/setup-java","sha":"be666c2fcd27ec809703dec50e508c2fdc7f6654","version":"be666c2fcd27ec809703dec50e508c2fdc7f6654"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"b8068426813005612b960b5ab0b8bd2c27142323","version":"v0.71.5"},{"repo":"gradle/actions/setup-gradle","sha":"50e97c2cd7a37755bbfafc9c5b7cafaece252f6e","version":"50e97c2cd7a37755bbfafc9c5b7cafaece252f6e"}],"containers":[{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.71.5). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Walks instrumentation modules one-at-a-time, processing exactly one +# module per run. Each successful run's commit is appended to the fixed +# `module-cleanup-wip` branch. When that branch reaches FILE_THRESHOLD +# modified files (or when the unprocessed-module queue empties), the +# finalize job atomically renames wip to `module-cleanup-batch-` +# and opens a PR against main. The next run, finding no wip on remote, +# starts a fresh wip from main. +# +# After each successful run, the workflow self-dispatches so chains keep +# moving without waiting for cron. The chain stops on its own once +# MAX_OPEN_PRS is reached (matrix returns has_work=false; finalize +# doesn't run; no self-dispatch). Cron (every 1h) restarts work after +# a PR merges and the open-PR count drops below MAX_OPEN_PRS. +# +# Because state lives on a fixed branch (not in any one run's identity), +# GitHub Actions concurrency cancellations of queued runs are harmless: +# the wip branch survives, and the next run picks up where it was. +# +# State: +# - `memory/module-cleanup` branch holds `processed.txt` (modules already +# attempted; never re-picked automatically) and `failed.txt` (a +# diagnostic log of timeouts and patch-conflict failures). +# - `module-cleanup-wip` branch holds not-yet-PR'd commits. Exists only +# while there is uncommitted work; deleted when promoted to a batch. +# - Open PRs labeled `module cleanup` count toward MAX_OPEN_PRS; while at +# cap, dispatch exits and waits for cron to retry. +# +# Resolved workflow manifest: +# Imports: +# - .github/agents/module-cleanup.agent.md +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 +# - gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e +# +# Container images used: +# - ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 +# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + +name: "Module Cleanup — Single Module" +"on": + schedule: + - cron: "9 */1 * * *" + # Friendly format: every 1h (scattered) + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + +permissions: {} + +concurrency: + cancel-in-progress: false + group: module-cleanup + +run-name: "Module Cleanup — Single Module" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Module Cleanup — Single Module" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/module-cleanup.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: "${{ vars.MODULE_CLEANUP_MODEL || 'gpt-5' }}" + GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_AGENT_VERSION: "1.0.40" + GH_AW_INFO_CLI_VERSION: "v0.71.5" + GH_AW_INFO_WORKFLOW_NAME: "Module Cleanup — Single Module" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","java"]' + GH_AW_INFO_FIREWALL_ENABLED: "false" + GH_AW_INFO_AWF_VERSION: "" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "" + GH_AW_COMPILED_STRICT: "false" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/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: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "module-cleanup.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.71.5" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_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 }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_10c522436a23fc31_EOF' + + GH_AW_PROMPT_10c522436a23fc31_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_10c522436a23fc31_EOF' + + Tools: create_issue + GH_AW_PROMPT_10c522436a23fc31_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_auto_create_issue.md" + cat << 'GH_AW_PROMPT_10c522436a23fc31_EOF' + + GH_AW_PROMPT_10c522436a23fc31_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_10c522436a23fc31_EOF' + + 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}} + + + GH_AW_PROMPT_10c522436a23fc31_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_10c522436a23fc31_EOF' + + {{#runtime-import .github/agents/module-cleanup.agent.md}} + {{#runtime-import .github/workflows/module-cleanup.md}} + GH_AW_PROMPT_10c522436a23fc31_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_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 }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_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, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: + - activation + - dispatch + if: needs.dispatch.outputs.has_work == 'true' + runs-on: ubuntu-latest + permissions: read-all + 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_WORKFLOW_ID_SANITIZED: modulecleanup + outputs: + agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-copilot-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-copilot-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-copilot-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Module Cleanup — Single Module" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/module-cleanup.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + - env: + GH_AW_NEEDS_DISPATCH_OUTPUTS_MODULE_DIR: ${{ needs.dispatch.outputs.module_dir }} + GH_AW_NEEDS_DISPATCH_OUTPUTS_SHORT_NAME: ${{ needs.dispatch.outputs.short_name }} + name: Export module identifiers to env + run: | + echo "MODULE_SHORT_NAME=$GH_AW_NEEDS_DISPATCH_OUTPUTS_SHORT_NAME" >> "$GITHUB_ENV" + echo "MODULE_DIR=$GH_AW_NEEDS_DISPATCH_OUTPUTS_MODULE_DIR" >> "$GITHUB_ENV" + - name: Set up JDK for running Gradle + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 + with: + distribution: temurin + java-version-file: .java-version + - name: Setup Gradle + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e + with: + cache-read-only: true + - name: Use CLA approved bot + run: .github/scripts/use-cla-approved-bot.sh + + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # 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 || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 + env: + GH_HOST: github.com + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_b706b838d5749a4f_EOF' + {"create_issue":{"labels":["module-cleanup"],"max":1,"title_prefix":"[module-cleanup]"}} + GH_AW_SAFE_OUTPUTS_CONFIG_b706b838d5749a4f_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[module-cleanup]\". Labels [\"module-cleanup\"] will be automatically added." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "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 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # 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: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + 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: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/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 + 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 "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.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_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + 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 "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="localhost" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -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 MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -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_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e 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.3.6' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_aa50bf905b4175e6_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v1.0.3", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://localhost:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_aa50bf905b4175e6_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 30 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + mkdir -p /tmp/ + mkdir -p /tmp/gh-aw/ + mkdir -p /tmp/gh-aw/agent/ + mkdir -p /tmp/gh-aw/sandbox/agent/logs/ + GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs 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 --no-ask-user --allow-all-tools --allow-all-paths --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt 2>&1 | tee /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.MODULE_CLEANUP_MODEL || 'gpt-5' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.71.5 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Detect Copilot errors + id: detect-copilot-errors + if: always() + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_copilot_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # 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 + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_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: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,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.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.azul.com,central.sonatype.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,develocity.apache.org,dl.google.com,dlcdn.apache.org,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,ge.spockframework.org,github.com,gradle.org,host.docker.internal,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.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,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,security.ubuntu.com,services.gradle.org,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - dispatch + - finalize + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + concurrency: + group: "gh-aw-conclusion-module-cleanup" + cancel-in-progress: false + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Module Cleanup — Single Module" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/module-cleanup.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Module Cleanup — Single Module" + 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: "module-cleanup" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "30" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + dispatch: + needs: activation + if: github.repository == 'trask/opentelemetry-java-instrumentation' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + + outputs: + has_work: ${{ steps.pick.outputs.has_work }} + module_dir: ${{ steps.pick.outputs.module_dir }} + queue_remaining: ${{ steps.pick.outputs.queue_remaining }} + short_name: ${{ steps.pick.outputs.short_name }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + - name: Pick next module + id: pick + run: | + set -euo pipefail + # processed.txt lives at the root of the memory branch. + processed="" + if git fetch origin "$MEMORY_BRANCH" --depth=1 2>/dev/null; then + processed=$(git show "origin/$MEMORY_BRANCH:processed.txt" 2>/dev/null || true) + fi + # Also exclude shorts already in inflight module-cleanup PRs (their + # bodies list the modules under "## Modules in this batch" as + # `- ` + backticks + short + backticks). Once a PR merges, those + # shorts also exist in processed.txt so they won't be re-picked. + inflight=$(gh pr list --repo "$GITHUB_REPOSITORY" \ + --label "module cleanup" --state open \ + --json body --jq '.[].body' \ + | sed -n 's/^- `\([^`]*\)`$/\1/p' || true) + export REVIEW_PROGRESS="$(printf '%s\n%s\n' "$processed" "$inflight" \ + | grep -v '^$' | sort -u)" + python .github/scripts/module-cleanup/build-cleanup-matrix.py + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MEMORY_BRANCH: memory/module-cleanup + + finalize: + needs: + - agent + - dispatch + if: always() && needs.dispatch.outputs.has_work == 'true' + runs-on: ubuntu-latest + permissions: + actions: write + contents: write + pull-requests: write + + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: true + - name: Configure git author + run: .github/scripts/use-cla-approved-bot.sh + - name: Download agent artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: ./agent-artifact + continue-on-error: true + - name: Finalize + run: bash .github/scripts/module-cleanup/finalize.sh + env: + AGENT_RESULT: ${{ needs.agent.result }} + ARTIFACT_DIR: ./agent-artifact + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + QUEUE_REMAINING: ${{ needs.dispatch.outputs.queue_remaining }} + SHORT_NAME: ${{ needs.dispatch.outputs.short_name }} + WORKFLOW_FILE: module-cleanup.lock.yml + + safe_outputs: + needs: + - activation + - agent + if: (!cancelled()) && needs.agent.result != 'skipped' + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/module-cleanup" + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: "${{ vars.MODULE_CLEANUP_MODEL || 'gpt-5' }}" + GH_AW_ENGINE_VERSION: "1.0.40" + GH_AW_WORKFLOW_ID: "module-cleanup" + GH_AW_WORKFLOW_NAME: "Module Cleanup — Single Module" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Module Cleanup — Single Module" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/module-cleanup.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,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.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.azul.com,central.sonatype.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,develocity.apache.org,dl.google.com,dlcdn.apache.org,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,ge.spockframework.org,github.com,gradle.org,host.docker.internal,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.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,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,security.ubuntu.com,services.gradle.org,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"labels\":[\"module-cleanup\"],\"max\":1,\"title_prefix\":\"[module-cleanup]\"}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + diff --git a/.github/workflows/module-cleanup.md b/.github/workflows/module-cleanup.md new file mode 100644 index 000000000000..d99f2bbb15eb --- /dev/null +++ b/.github/workflows/module-cleanup.md @@ -0,0 +1,225 @@ +--- +description: | + Walks instrumentation modules one-at-a-time, processing exactly one + module per run. Each successful run's commit is appended to the fixed + `module-cleanup-wip` branch. When that branch reaches FILE_THRESHOLD + modified files (or when the unprocessed-module queue empties), the + finalize job atomically renames wip to `module-cleanup-batch-` + and opens a PR against main. The next run, finding no wip on remote, + starts a fresh wip from main. + + After each successful run, the workflow self-dispatches so chains keep + moving without waiting for cron. The chain stops on its own once + MAX_OPEN_PRS is reached (matrix returns has_work=false; finalize + doesn't run; no self-dispatch). Cron (every 1h) restarts work after + a PR merges and the open-PR count drops below MAX_OPEN_PRS. + + Because state lives on a fixed branch (not in any one run's identity), + GitHub Actions concurrency cancellations of queued runs are harmless: + the wip branch survives, and the next run picks up where it was. + + State: + - `memory/module-cleanup` branch holds `processed.txt` (modules already + attempted; never re-picked automatically) and `failed.txt` (a + diagnostic log of timeouts and patch-conflict failures). + - `module-cleanup-wip` branch holds not-yet-PR'd commits. Exists only + while there is uncommitted work; deleted when promoted to a batch. + - Open PRs labeled `module cleanup` count toward MAX_OPEN_PRS; while at + cap, dispatch exits and waits for cron to retry. + +on: + workflow_dispatch: + schedule: + - cron: "every 1h" + +permissions: read-all + +concurrency: + group: module-cleanup + cancel-in-progress: false + +timeout-minutes: 30 + +# Disable strict mode so we can opt out of the AWF agent sandbox below. +strict: false + +engine: + id: copilot + model: ${{ vars.MODULE_CLEANUP_MODEL || 'gpt-5' }} + +# Disable the AWF sandbox so copilot-cli connects directly to +# api.githubcopilot.com. The AWF api-proxy in v0.25.40 collapses Copilot +# traffic to a single endpoint, which prevents the CLI from negotiating +# /responses-API routing and blocks newer models like gpt-5.5. +sandbox: + agent: false + +network: + allowed: + - defaults + - java + +tools: + edit: + bash: [":*"] + +# No safe-outputs: the finalize job owns PR creation directly via `gh`, +# and memory-branch state is managed by plain git pushes from the finalize +# script. This keeps all post-LLM logic in shell where it can run reliably +# regardless of how the agent session ends. + +imports: + - .github/agents/module-cleanup.agent.md + +jobs: + dispatch: + if: github.repository == 'trask/opentelemetry-java-instrumentation' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + has_work: ${{ steps.pick.outputs.has_work }} + short_name: ${{ steps.pick.outputs.short_name }} + module_dir: ${{ steps.pick.outputs.module_dir }} + queue_remaining: ${{ steps.pick.outputs.queue_remaining }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + - name: Pick next module + id: pick + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MEMORY_BRANCH: memory/module-cleanup + run: | + set -euo pipefail + # processed.txt lives at the root of the memory branch. + processed="" + if git fetch origin "$MEMORY_BRANCH" --depth=1 2>/dev/null; then + processed=$(git show "origin/$MEMORY_BRANCH:processed.txt" 2>/dev/null || true) + fi + # Also exclude shorts already in inflight module-cleanup PRs (their + # bodies list the modules under "## Modules in this batch" as + # `- ` + backticks + short + backticks). Once a PR merges, those + # shorts also exist in processed.txt so they won't be re-picked. + inflight=$(gh pr list --repo "$GITHUB_REPOSITORY" \ + --label "module cleanup" --state open \ + --json body --jq '.[].body' \ + | sed -n 's/^- `\([^`]*\)`$/\1/p' || true) + export REVIEW_PROGRESS="$(printf '%s\n%s\n' "$processed" "$inflight" \ + | grep -v '^$' | sort -u)" + python .github/scripts/module-cleanup/build-cleanup-matrix.py + + finalize: + needs: + - dispatch + - agent + if: always() && needs.dispatch.outputs.has_work == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + actions: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: true + - name: Configure git author + run: .github/scripts/use-cla-approved-bot.sh + - name: Download agent artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: ./agent-artifact + continue-on-error: true + - name: Finalize + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SHORT_NAME: ${{ needs.dispatch.outputs.short_name }} + AGENT_RESULT: ${{ needs.agent.result }} + QUEUE_REMAINING: ${{ needs.dispatch.outputs.queue_remaining }} + ARTIFACT_DIR: ./agent-artifact + WORKFLOW_FILE: module-cleanup.lock.yml + run: bash .github/scripts/module-cleanup/finalize.sh + +if: ${{ needs.dispatch.outputs.has_work == 'true' }} + +steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Export module identifiers to env + # Top-level frontmatter `env:` would land at workflow scope where + # `needs.*` is not available. Export via GITHUB_ENV instead so the + # LLM step (and its bash tool) sees MODULE_SHORT_NAME / MODULE_DIR. + run: | + echo "MODULE_SHORT_NAME=${{ needs.dispatch.outputs.short_name }}" >> "$GITHUB_ENV" + echo "MODULE_DIR=${{ needs.dispatch.outputs.module_dir }}" >> "$GITHUB_ENV" + - name: Set up JDK for running Gradle + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version-file: .java-version + - name: Setup Gradle + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + with: + cache-read-only: true + - name: Use CLA approved bot + run: .github/scripts/use-cla-approved-bot.sh +--- + +# Module Cleanup — Single Module + +You clean up exactly **one** instrumentation module this run, then export +your commit so the finalize job can roll it into a batched PR. + +## Inputs + +This run targets a single module. Read its identifiers from the workflow +environment, **not** from a JSON list: + +- `` is in the `MODULE_SHORT_NAME` environment variable. +- `` is in the `MODULE_DIR` environment variable. + +Use these directly via `$MODULE_SHORT_NAME` / `$MODULE_DIR` in any shell +command. Do **not** invent module names or guess directories. + +## Per-run workflow + +Run **inline in this session**. Do **not** spawn background agents, +sub-sessions, or use `Module-cleanup` as a callable tool. The persona +instructions imported into this prompt are yours; execute them yourself. + +1. Confirm the module directory exists: + `test -d "$MODULE_DIR" || { echo "Module directory missing: $MODULE_DIR"; exit 1; }` +2. Apply the imported `module-cleanup` persona's full checklist to + `$MODULE_DIR`. Reach the persona's commit step. The commit subject must + match the persona's format: `Cleanup for $MODULE_SHORT_NAME`. If the + persona reports it had to revert all of its changes (no substantive + diff remained), proceed to step 3 anyway — "no commit" is a valid + outcome and finalize handles it. +3. **Final mandatory action** (do not skip even on no-op): + ``` + bash .github/scripts/module-cleanup/export-cleanup-patch.sh "$MODULE_SHORT_NAME" + ``` + This writes `/tmp/gh-aw/agent/cleanup.patch` (a `git format-patch` of + your commit range) so gh-aw's auto-uploader includes it in the + workflow's `agent` artifact. The finalize job downloads that artifact + and applies the patch to the `module-cleanup-wip` branch. The script + is idempotent and exits cleanly with no patch if you produced no + commit. **Run it exactly once as your last action.** If you do not run + it, your work is lost. + +## What you must NOT do + +- Do not run `git push`. The finalize job handles all remote writes. +- Do not call `gh pr create`. The finalize job opens the PR. +- Do not modify `processed.txt` or `failed.txt`. The finalize job owns + the memory branch. +- Do not spawn background agents, child sessions, or sub-tasks. The + persona is loaded into this session; execute it inline. +- Do not modify files outside `$MODULE_DIR` unless the persona's + out-of-module-edit allowance applies to your specific change. diff --git a/.github/workflows/module-cleanup.yml b/.github/workflows/module-cleanup.yml deleted file mode 100644 index 8cde69a89e0b..000000000000 --- a/.github/workflows/module-cleanup.yml +++ /dev/null @@ -1,226 +0,0 @@ -name: Module Cleanup - -on: - schedule: - # Every 15 minutes - - cron: "*/15 * * * *" - workflow_dispatch: - -permissions: - contents: read - -# Prevent overlapping cleanup runs -concurrency: - group: module-cleanup - cancel-in-progress: false - -jobs: - # --------------------------------------------------------------------------- - # Job 1: Determine which modules to clean up - # --------------------------------------------------------------------------- - dispatch: - # Only run on official repo, not forks - if: github.repository == 'open-telemetry/opentelemetry-java-instrumentation' - runs-on: ubuntu-latest - outputs: - modules: ${{ steps.build-matrix.outputs.modules }} - has_work: ${{ steps.build-matrix.outputs.has_work }} - model: ${{ steps.model.outputs.model }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - - - name: Fetch progress branch - run: git fetch origin otelbot/module-cleanup-progress || true - - - name: Resolve Copilot model - id: model - run: | - model=$(git show origin/otelbot/module-cleanup-progress:model.txt | xargs) - echo "model=$model" >> "$GITHUB_OUTPUT" - - - name: Build cleanup matrix - id: build-matrix - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Read progress from the dedicated orphan branch (if it exists) - progress=$(git show origin/otelbot/module-cleanup-progress:reviewed.txt 2>/dev/null || true) - if [[ -n "$progress" ]]; then - export REVIEW_PROGRESS="$progress" - fi - python .github/scripts/module-cleanup/build-cleanup-matrix.py - - # --------------------------------------------------------------------------- - # Job 2: Walk modules sequentially on a single branch, stopping once the - # accumulated change set reaches FILE_THRESHOLD modified files. One PR per run. - # --------------------------------------------------------------------------- - cleanup: - needs: dispatch - if: needs.dispatch.outputs.has_work == 'true' - runs-on: ubuntu-latest - environment: protected - permissions: - contents: write # for git push - env: - MODULES_JSON: ${{ needs.dispatch.outputs.modules }} - MODEL: ${{ needs.dispatch.outputs.model }} - # Stop processing further modules once at least this many files have been - # modified (vs origin/main) at the end of a module. - FILE_THRESHOLD: 10 - COPILOT_ROOT: /tmp/copilot - FRAGMENTS_DIR: /tmp/pr-body-fragments - PROCESSED_MODULES: /tmp/processed-modules.txt - PR_BODY: /tmp/pr-body.md - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Fetch progress branch - run: git fetch origin otelbot/module-cleanup-progress || true - - - name: Free disk space - run: .github/scripts/gha-free-disk-space.sh - - - name: Set up JDK for running Gradle - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - distribution: temurin - java-version-file: .java-version - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: true - - - name: Install Copilot CLI - run: | - curl -fsSL https://gh.io/copilot-install | bash - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - - - name: Use CLA approved bot - run: .github/scripts/use-cla-approved-bot.sh - - - name: Check out cleanup branch - id: branch - run: | - branch="otelbot/module-cleanup-${GITHUB_RUN_ID}" - git checkout -B "$branch" origin/main - echo "name=$branch" >> "$GITHUB_OUTPUT" - - - name: Run Copilot cleanup loop - id: cleanup-loop - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_REVIEW_PROMPT_TEMPLATE: >- - Review all files under __MODULE_DIR__. Apply safe repository-guideline fixes directly. - Return ONLY a valid JSON object as your final answer with this exact schema: - {"summary": string, "changes": [{"path": string, "category": string, "change": string, "reason": string, "line_hint": number|null}], "unresolved": [{"path": string, "reason": string}]} - Include one changes entry for every file you changed. - Use concise factual reasons that cite the review guideline or repository rule behind each change. - In `summary`, `change`, and `reason`, use Markdown inline code backticks around code-like constructs when helpful, - including annotations, class names, method names, field names, file names, Gradle tasks, commands, flags, and config keys. - If no safe fixes were applied, still return valid JSON with an empty changes array and a brief summary. - Do not write markdown and do not wrap the JSON in code fences. - run: python .github/scripts/module-cleanup/cleanup-loop.py - - - name: Upload cleanup diagnostics artifact - if: always() - id: upload-cleanup-diagnostics - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: module-cleanup-diagnostics-${{ github.run_id }} - path: | - /tmp/copilot/** - /tmp/processed-modules.txt - if-no-files-found: ignore - - - name: Assemble PR body - if: steps.cleanup-loop.outputs.commits_on_branch != '0' - env: - ARTIFACT_URL: ${{ steps.upload-cleanup-diagnostics.outputs.artifact-url }} - run: | - set -euo pipefail - { - echo "Automated module cleanup walked the following modules in order" - echo "and stopped after accumulating at least ${FILE_THRESHOLD} modified files:" - echo - while IFS= read -r m; do - echo "- \`$m\`" - done < "$PROCESSED_MODULES" - echo - echo "---" - echo - for f in "$FRAGMENTS_DIR"/*.md; do - [[ -f "$f" ]] || continue - cat "$f" - echo - done - echo "---" - echo - echo "[Download module cleanup diagnostics]($ARTIFACT_URL)" - echo - } > "$PR_BODY" - - - name: Commit summary - if: steps.cleanup-loop.outputs.commits_on_branch != '0' - id: commit - run: | - branch="${{ steps.branch.outputs.name }}" - git push -f origin "$branch" - echo "pushed=true" >> "$GITHUB_OUTPUT" - - - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 - id: otelbot-token - if: steps.commit.outputs.pushed == 'true' - with: - app-id: ${{ vars.OTELBOT_APP_ID }} - private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - - - name: Create PR - if: steps.commit.outputs.pushed == 'true' - env: - GH_TOKEN: ${{ steps.otelbot-token.outputs.token }} - run: | - branch="${{ steps.branch.outputs.name }}" - title="Module cleanup (run ${GITHUB_RUN_ID})" - gh pr create \ - --title "$title" \ - --body-file "$PR_BODY" \ - --base main \ - --head "$branch" \ - --label "module cleanup" - - - name: Ensure progress branch exists - if: steps.cleanup-loop.outputs.processed_count != '0' - run: | - if ! git rev-parse --verify origin/otelbot/module-cleanup-progress >/dev/null 2>&1; then - git checkout --orphan otelbot/module-cleanup-progress - git reset --hard - git commit --allow-empty -m "Initialize progress tracking" - git push origin HEAD:otelbot/module-cleanup-progress || true - fi - - - name: Check out progress branch - if: steps.cleanup-loop.outputs.processed_count != '0' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: otelbot/module-cleanup-progress - path: progress - - - name: Mark processed modules as reviewed - if: steps.cleanup-loop.outputs.processed_count != '0' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - cd progress - - git config user.name otelbot - git config user.email 197425009+otelbot@users.noreply.github.com - - cat "$PROCESSED_MODULES" >> reviewed.txt - - git add reviewed.txt - git commit -m "Mark $(wc -l < "$PROCESSED_MODULES" | tr -d ' ') module(s) as reviewed" - git push origin HEAD:otelbot/module-cleanup-progress diff --git a/instrumentation/executors/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/AbstractExecutorServiceTest.java b/instrumentation/executors/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/AbstractExecutorServiceTest.java index 9286713afe0c..90a112f4fad8 100644 --- a/instrumentation/executors/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/AbstractExecutorServiceTest.java +++ b/instrumentation/executors/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/AbstractExecutorServiceTest.java @@ -32,9 +32,9 @@ protected AbstractExecutorServiceTest(T executor, InstrumentationExtension testi this.testing = testing; } - protected abstract U newTask(boolean doTraceableWork, boolean blockThread); + abstract U newTask(boolean doTraceableWork, boolean blockThread); - protected T executor() { + T executor() { return executor; } @@ -110,7 +110,7 @@ void submitCallableAndCancel() { executeAndCancelTasks(task -> executor.submit((Callable) task)); } - protected void executeTwoTasks(ThrowingConsumer task) { + void executeTwoTasks(ThrowingConsumer task) { testing.runWithSpan( "parent", () -> { @@ -137,7 +137,7 @@ protected void executeTwoTasks(ThrowingConsumer task) { .hasParent(trace.getSpan(0)))); } - protected void executeAndCancelTasks(Function> task) { + void executeAndCancelTasks(Function> task) { List children = new ArrayList<>(); List> jobFutures = new ArrayList<>(); diff --git a/instrumentation/executors/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/TestTask.java b/instrumentation/executors/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/TestTask.java index 82d51ebd85d6..fb46e60dbb6c 100644 --- a/instrumentation/executors/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/TestTask.java +++ b/instrumentation/executors/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/TestTask.java @@ -7,7 +7,7 @@ import java.util.concurrent.Callable; -public interface TestTask extends Runnable, Callable { +interface TestTask extends Runnable, Callable { void unblock(); diff --git a/instrumentation/finagle-http-23.11/javaagent/build.gradle.kts b/instrumentation/finagle-http-23.11/javaagent/build.gradle.kts index 156c95f0cc7e..b7295a4a7202 100644 --- a/instrumentation/finagle-http-23.11/javaagent/build.gradle.kts +++ b/instrumentation/finagle-http-23.11/javaagent/build.gradle.kts @@ -51,11 +51,6 @@ tasks { systemProperty("collectMetadata", otelProps.collectMetadata) jvmArgs("-Dotel.instrumentation.http.client.emit-experimental-telemetry=true") jvmArgs("-Dotel.instrumentation.http.server.emit-experimental-telemetry=true") - } - - test { - jvmArgs("-Dotel.instrumentation.http.client.emit-experimental-telemetry=true") - jvmArgs("-Dotel.instrumentation.http.server.emit-experimental-telemetry=true") jvmArgs("-Dio.opentelemetry.context.enableStrictContext=true") // force the netty event loop into constrained territory @@ -64,7 +59,9 @@ tasks { systemProperty("com.twitter.finagle.netty4.numWorkers", "2") // ensure concurrent tests are competing for offload pool workers systemProperty("com.twitter.finagle.offload.numWorkers", "2") + } + test { systemProperty( "metadataConfig", "otel.instrumentation.http.client.emit-experimental-telemetry=true," + @@ -76,14 +73,6 @@ tasks { testClassesDirs = sourceSets.test.get().output.classesDirs classpath = sourceSets.test.get().runtimeClasspath jvmArgs("-Dotel.semconv-stability.opt-in=service.peer") - jvmArgs("-Dio.opentelemetry.context.enableStrictContext=true") - - // force the netty event loop into constrained territory - systemProperty("io.netty.eventLoopThreads", "2") - // ensure concurrent tests are competing for netty workers - systemProperty("com.twitter.finagle.netty4.numWorkers", "2") - // ensure concurrent tests are competing for offload pool workers - systemProperty("com.twitter.finagle.offload.numWorkers", "2") systemProperty( "metadataConfig", diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/BijectionsNettyInstrumentation.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/BijectionsNettyInstrumentation.java index 38f7126c7ae7..d5fad4a910c1 100644 --- a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/BijectionsNettyInstrumentation.java +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/BijectionsNettyInstrumentation.java @@ -5,7 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; -import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.named; import com.twitter.finagle.http.Request; @@ -28,10 +27,9 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( - isMethod().and(named("fullRequestToFinagle")), getClass().getName() + "$FullRequestAdvice"); + named("fullRequestToFinagle"), getClass().getName() + "$FullRequestAdvice"); transformer.applyAdviceToMethod( - isMethod().and(named("chunkedRequestToFinagle")), - getClass().getName() + "$ChunkedRequestAdvice"); + named("chunkedRequestToFinagle"), getClass().getName() + "$ChunkedRequestAdvice"); } @SuppressWarnings("unused") diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FutureInstrumentation.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FutureInstrumentation.java index 68afcf42e23a..257ae4da8bda 100644 --- a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FutureInstrumentation.java +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FutureInstrumentation.java @@ -5,7 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; -import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.named; import com.twitter.util.Future; @@ -30,12 +29,10 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { - transformer.applyAdviceToMethod( - isMethod().and(named("respond")), getClass().getName() + "$RespondAdvice"); + transformer.applyAdviceToMethod(named("respond"), getClass().getName() + "$RespondAdvice"); // transformTry is documented as not being run in the scheduler, so it's not handled - transformer.applyAdviceToMethod( - isMethod().and(named("transform")), getClass().getName() + "$TransformAdvice"); + transformer.applyAdviceToMethod(named("transform"), getClass().getName() + "$TransformAdvice"); } @SuppressWarnings("unused") diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FuturePoolInstrumentation.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FuturePoolInstrumentation.java index 9116cbcfd04d..bb5f4854efc7 100644 --- a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FuturePoolInstrumentation.java +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FuturePoolInstrumentation.java @@ -5,7 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; -import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.named; import io.opentelemetry.context.Context; @@ -30,8 +29,7 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { - transformer.applyAdviceToMethod( - isMethod().and(named("apply")), getClass().getName() + "$ApplyAdvice"); + transformer.applyAdviceToMethod(named("apply"), getClass().getName() + "$ApplyAdvice"); } @SuppressWarnings("unused") diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/PromiseKInstrumentation.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/PromiseKInstrumentation.java index eba5e9befe4e..216f3da1a8c3 100644 --- a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/PromiseKInstrumentation.java +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/PromiseKInstrumentation.java @@ -6,7 +6,6 @@ package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; import static net.bytebuddy.matcher.ElementMatchers.isConstructor; -import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; @@ -39,7 +38,7 @@ public void transform(TypeTransformer transformer) { isConstructor().and(takesArgument(0, named("com.twitter.util.Local$Context"))), getClass().getName() + "$TrapContextAdvice"); transformer.applyAdviceToMethod( - isMethod().and(named("apply").and(takesArgument(0, named("com.twitter.util.Try")))), + named("apply").and(takesArgument(0, named("com.twitter.util.Try"))), getClass().getName() + "$ApplyAdvice"); } diff --git a/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FinagleClientExtension.java b/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FinagleClientExtension.java index 7c1bd76db347..23a4322d1ae3 100644 --- a/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FinagleClientExtension.java +++ b/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FinagleClientExtension.java @@ -32,14 +32,14 @@ * the underlying {@link EventLoopGroup} for the duration of the test class. Clients and services * are created lazily on first use and torn down once in {@code afterAll}. */ -public class FinagleClientExtension implements AfterAllCallback { +class FinagleClientExtension implements AfterAllCallback { private final UnaryOperator configurer; private final Map clients = new ConcurrentHashMap<>(); private final Map> services = new ConcurrentHashMap<>(); - public FinagleClientExtension(UnaryOperator configurer) { + FinagleClientExtension(UnaryOperator configurer) { this.configurer = configurer; } @@ -52,11 +52,11 @@ public void afterAll(ExtensionContext context) throws Exception { clients.clear(); } - public Service getService(URI uri) { + Service getService(URI uri) { return getService(uri, "https".equals(uri.getScheme()) ? ClientType.TLS : ClientType.DEFAULT); } - public Service getService(URI uri, ClientType type) { + Service getService(URI uri, ClientType type) { String dest = uri.getHost() + ":" + Utils.safePort(uri); ServiceKey key = new ServiceKey(dest, type); // Build the client and bind the service under the root OTel context so no test-trace context diff --git a/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/v2_9/FinatraRouteBuilderInstrumentation.java b/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/v2_9/FinatraRouteBuilderInstrumentation.java index bfb4e39c6099..a9fc70828abd 100644 --- a/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/v2_9/FinatraRouteBuilderInstrumentation.java +++ b/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/v2_9/FinatraRouteBuilderInstrumentation.java @@ -5,7 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.finatra.v2_9; -import static io.opentelemetry.javaagent.instrumentation.finatra.v2_9.FinatraSingletons.setCallbackClass; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.returns; @@ -36,7 +35,7 @@ public static class BuildAdvice { @Advice.OnMethodExit(suppress = Throwable.class, inline = false) public static void onExit( @Advice.Return Route route, @Advice.FieldValue("callback") Function1 callback) { - setCallbackClass(route, callback.getClass()); + FinatraSingletons.setCallbackClass(route, callback.getClass()); } } } diff --git a/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/v2_9/FinatraRouteInstrumentation.java b/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/v2_9/FinatraRouteInstrumentation.java index 4272d9a41240..7edeef0e4204 100644 --- a/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/v2_9/FinatraRouteInstrumentation.java +++ b/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/v2_9/FinatraRouteInstrumentation.java @@ -5,10 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.finatra.v2_9; -import static io.opentelemetry.javaagent.instrumentation.finatra.v2_9.FinatraSingletons.getCallbackClass; import static io.opentelemetry.javaagent.instrumentation.finatra.v2_9.FinatraSingletons.instrumenter; -import static io.opentelemetry.javaagent.instrumentation.finatra.v2_9.FinatraSingletons.setCallbackClass; -import static io.opentelemetry.javaagent.instrumentation.finatra.v2_9.FinatraSingletons.updateServerSpanName; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.returns; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; @@ -65,9 +62,9 @@ private AdviceScope(FinatraRequest request, Context context, Scope scope) { public static AdviceScope start(Route route, RouteInfo routeInfo, Class controllerClass) { Context parentContext = Context.current(); - updateServerSpanName(parentContext, routeInfo); + FinatraSingletons.updateServerSpanName(parentContext, routeInfo); - Class callbackClass = getCallbackClass(route); + Class callbackClass = FinatraSingletons.getCallbackClass(route); // We expect callback to be an inner class of the controller class. If it is not we are not // going to record it at all. FinatraRequest request; @@ -120,7 +117,7 @@ public static class CopyAdvice { @Advice.OnMethodExit(suppress = Throwable.class, inline = false) public static void onExit(@Advice.This Route route, @Advice.Return Route result) { - setCallbackClass(result, getCallbackClass(route)); + FinatraSingletons.setCallbackClass(result, FinatraSingletons.getCallbackClass(route)); } } }