diff --git a/.changeset/patch-fetch-origin-base-for-push-patch.md b/.changeset/patch-fetch-origin-base-for-push-patch.md new file mode 100644 index 0000000000..3da199001b --- /dev/null +++ b/.changeset/patch-fetch-origin-base-for-push-patch.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Fetches the missing `origin/(branch)` ref before generating the `push_to_pull_request_branch` patch so follow-up `issue_comment` runs only include new commits. diff --git a/.changeset/patch-fetch-push-patch-base.md b/.changeset/patch-fetch-push-patch-base.md new file mode 100644 index 0000000000..3da199001b --- /dev/null +++ b/.changeset/patch-fetch-push-patch-base.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Fetches the missing `origin/(branch)` ref before generating the `push_to_pull_request_branch` patch so follow-up `issue_comment` runs only include new commits. diff --git a/.changeset/smoke-test-push-22232216256.md b/.changeset/smoke-test-push-22232216256.md new file mode 100644 index 0000000000..ccf3db874e --- /dev/null +++ b/.changeset/smoke-test-push-22232216256.md @@ -0,0 +1,3 @@ +# Smoke Test Push + +Smoke test file for PR push - run 22232216256 diff --git a/actions/setup/README.md b/actions/setup/README.md index 238cc7dd7b..d127bb2752 100644 --- a/actions/setup/README.md +++ b/actions/setup/README.md @@ -62,11 +62,10 @@ This action copies files from `actions/setup/`, including: - Safe output scripts (safe_outputs_*, safe_inputs_*, messages, etc.) - Utility scripts (sanitize_*, validate_*, generate_*, etc.) -### Shell Scripts (7 files from `sh/`) +### Shell Scripts (6 files from `sh/`) - `create_gh_aw_tmp_dir.sh` - Creates temporary directory structure - `start_safe_inputs_server.sh` - Starts safe-inputs HTTP server - `print_prompt_summary.sh` - Prints prompt summary to logs -- `generate_git_patch.sh` - Generates git patches - `create_cache_memory_dir.sh` - Creates cache-memory directory - `create_prompt_first.sh` - Creates prompt directory - `validate_multi_secret.sh` - Validates that at least one secret from a list is configured diff --git a/actions/setup/js/generate_git_patch.cjs b/actions/setup/js/generate_git_patch.cjs index d7ec46a190..adec71b32e 100644 --- a/actions/setup/js/generate_git_patch.cjs +++ b/actions/setup/js/generate_git_patch.cjs @@ -8,19 +8,157 @@ const { getBaseBranch } = require("./get_base_branch.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { execGitSync } = require("./git_helpers.cjs"); +const PATCH_PATH = "/tmp/gh-aw/aw.patch"; + +/** + * Resolves the base ref to use for patch generation against a named branch. + * Preference order: + * 1. Remote tracking ref refs/remotes/origin/ (already fetched) + * 2. Fresh fetch of origin/ (gh pr checkout path) + * 3. merge-base with origin/ (brand-new branch) + * @param {string} branchName + * @param {string} defaultBranch + * @param {string} cwd + * @returns {string} baseRef + */ +function resolveBaseRef(branchName, defaultBranch, cwd) { + try { + execGitSync(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branchName}`], { cwd }); + const baseRef = `origin/${branchName}`; + core.info(`[generate_git_patch] using remote tracking ref as baseRef="${baseRef}"`); + return baseRef; + } catch { + // Remote tracking ref not found (e.g. after gh pr checkout which doesn't set tracking refs). + // Try fetching the branch from origin so we use only the NEW commits as the patch base. + core.info(`[generate_git_patch] refs/remotes/origin/${branchName} not found; fetching from origin`); + } + + try { + execGitSync(["fetch", "origin", branchName], { cwd }); + const baseRef = `origin/${branchName}`; + core.info(`[generate_git_patch] fetch succeeded, baseRef="${baseRef}"`); + return baseRef; + } catch (fetchErr) { + // Distinguish between "branch doesn't exist on origin" and other fetch failures. + let branchExistsOnOrigin = false; + try { + // git ls-remote --exit-code origin refs/heads/ succeeds only if the ref exists. + execGitSync(["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd }); + branchExistsOnOrigin = true; + } catch (lsRemoteErr) { + core.info(`[generate_git_patch] ls-remote did not find refs/heads/${branchName} on origin (${getErrorMessage(lsRemoteErr)}); treating as new branch`); + } + + if (branchExistsOnOrigin) { + // The branch exists on origin, so this is a real fetch failure (auth/network/etc). + const message = `[generate_git_patch] fetch of origin/${branchName} failed, but branch exists on origin; cannot safely determine baseRef: ${getErrorMessage(fetchErr)}`; + core.error(message); + throw fetchErr; + } + + // Branch doesn't exist on origin yet (new branch) – fall back to merge-base. + core.warning(`[generate_git_patch] origin does not have branch ${branchName}; falling back to merge-base with "${defaultBranch}"`); + } + + execGitSync(["fetch", "origin", defaultBranch], { cwd }); + const baseRef = execGitSync(["merge-base", `origin/${defaultBranch}`, branchName], { cwd }).trim(); + core.info(`[generate_git_patch] merge-base baseRef="${baseRef}"`); + return baseRef; +} + /** - * Generates a git patch file for the current changes + * Writes a patch file for the range base..tip and returns whether it succeeded. + * @param {string} base - commit-ish for the base (exclusive) + * @param {string} tip - commit-ish for the tip (inclusive) + * @param {string} cwd + * @returns {boolean} true if the patch was written with content + */ +function writePatch(base, tip, cwd) { + const commitCount = parseInt(execGitSync(["rev-list", "--count", `${base}..${tip}`], { cwd }).trim(), 10); + core.info(`[generate_git_patch] ${commitCount} commit(s) between ${base} and ${tip}`); + + if (commitCount === 0) { + return false; + } + + const patchContent = execGitSync(["format-patch", `${base}..${tip}`, "--stdout"], { cwd }); + if (!patchContent || !patchContent.trim()) { + core.warning(`[generate_git_patch] format-patch produced empty output for ${base}..${tip}`); + return false; + } + + fs.writeFileSync(PATCH_PATH, patchContent, "utf8"); + core.info(`[generate_git_patch] patch written: ${patchContent.split("\n").length} lines, ${Math.ceil(Buffer.byteLength(patchContent, "utf8") / 1024)} KB`); + return true; +} + +/** + * Strategy 1: generate a patch from the known remote state of branchName to + * its local tip, capturing only commits not yet on origin. + * @param {string} branchName + * @param {string} defaultBranch + * @param {string} cwd + * @returns {boolean} true if a patch was written + */ +function tryBranchStrategy(branchName, defaultBranch, cwd) { + core.info(`[generate_git_patch] Strategy 1: branch-based patch for "${branchName}"`); + try { + execGitSync(["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd }); + } catch (err) { + core.info(`[generate_git_patch] local branch "${branchName}" not found: ${getErrorMessage(err)}`); + return false; + } + + const baseRef = resolveBaseRef(branchName, defaultBranch, cwd); + return writePatch(baseRef, branchName, cwd); +} + +/** + * Strategy 2: generate a patch from GITHUB_SHA to the current HEAD, capturing + * commits made by the agent after checkout. + * @param {string|undefined} githubSha + * @param {string} cwd + * @returns {{ generated: boolean, errorMessage: string|null }} + */ +function tryHeadStrategy(githubSha, cwd) { + const currentHead = execGitSync(["rev-parse", "HEAD"], { cwd }).trim(); + core.info(`[generate_git_patch] Strategy 2: HEAD="${currentHead}" GITHUB_SHA="${githubSha || ""}"`); + + if (!githubSha) { + const msg = "GITHUB_SHA environment variable is not set"; + core.warning(`[generate_git_patch] ${msg}`); + return { generated: false, errorMessage: msg }; + } + + if (currentHead === githubSha) { + core.info(`[generate_git_patch] HEAD matches GITHUB_SHA – no new commits`); + return { generated: false, errorMessage: null }; + } + + try { + execGitSync(["merge-base", "--is-ancestor", githubSha, "HEAD"], { cwd }); + } catch { + core.warning(`[generate_git_patch] GITHUB_SHA is not an ancestor of HEAD – repository state has diverged`); + return { generated: false, errorMessage: null }; + } + + const generated = writePatch(githubSha, "HEAD", cwd); + return { generated, errorMessage: null }; +} + +/** + * Generates a git patch file for the current changes. * @param {string} branchName - The branch name to generate patch for * @returns {Object} Object with patch info or error */ function generateGitPatch(branchName) { - const patchPath = "/tmp/gh-aw/aw.patch"; const cwd = process.env.GITHUB_WORKSPACE || process.cwd(); const defaultBranch = process.env.DEFAULT_BRANCH || getBaseBranch(); const githubSha = process.env.GITHUB_SHA; - // Ensure /tmp/gh-aw directory exists - const patchDir = path.dirname(patchPath); + core.info(`[generate_git_patch] branchName="${branchName || ""}" GITHUB_SHA="${githubSha || ""}" defaultBranch="${defaultBranch}"`); + + const patchDir = path.dirname(PATCH_PATH); if (!fs.existsSync(patchDir)) { fs.mkdirSync(patchDir, { recursive: true }); } @@ -29,87 +167,32 @@ function generateGitPatch(branchName) { let errorMessage = null; try { - // Strategy 1: If we have a branch name, check if that branch exists and get its diff if (branchName) { - // Check if the branch exists locally - try { - execGitSync(["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd }); - - // Determine base ref for patch generation - let baseRef; - try { - // Check if origin/branchName exists - execGitSync(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branchName}`], { cwd }); - baseRef = `origin/${branchName}`; - } catch { - // Use merge-base with default branch - execGitSync(["fetch", "origin", defaultBranch], { cwd }); - baseRef = execGitSync(["merge-base", `origin/${defaultBranch}`, branchName], { cwd }).trim(); - } - - // Count commits to be included - const commitCount = parseInt(execGitSync(["rev-list", "--count", `${baseRef}..${branchName}`], { cwd }).trim(), 10); - - if (commitCount > 0) { - // Generate patch from the determined base to the branch - const patchContent = execGitSync(["format-patch", `${baseRef}..${branchName}`, "--stdout"], { cwd }); - - if (patchContent && patchContent.trim()) { - fs.writeFileSync(patchPath, patchContent, "utf8"); - patchGenerated = true; - } - } - } catch (branchError) { - // Branch does not exist locally - } + patchGenerated = tryBranchStrategy(branchName, defaultBranch, cwd); + } else { + core.info(`[generate_git_patch] Strategy 1: skipped (no branchName)`); } - // Strategy 2: Check if commits were made to current HEAD since checkout if (!patchGenerated) { - const currentHead = execGitSync(["rev-parse", "HEAD"], { cwd }).trim(); - - if (!githubSha) { - errorMessage = "GITHUB_SHA environment variable is not set"; - } else if (currentHead === githubSha) { - // No commits have been made since checkout - } else { - // Check if GITHUB_SHA is an ancestor of current HEAD - try { - execGitSync(["merge-base", "--is-ancestor", githubSha, "HEAD"], { cwd }); - - // Count commits between GITHUB_SHA and HEAD - const commitCount = parseInt(execGitSync(["rev-list", "--count", `${githubSha}..HEAD`], { cwd }).trim(), 10); - - if (commitCount > 0) { - // Generate patch from GITHUB_SHA to HEAD - const patchContent = execGitSync(["format-patch", `${githubSha}..HEAD`, "--stdout"], { cwd }); - - if (patchContent && patchContent.trim()) { - fs.writeFileSync(patchPath, patchContent, "utf8"); - patchGenerated = true; - } - } - } catch { - // GITHUB_SHA is not an ancestor of HEAD - repository state has diverged - } - } + const result = tryHeadStrategy(githubSha, cwd); + patchGenerated = result.generated; + errorMessage = result.errorMessage; } } catch (error) { errorMessage = `Failed to generate patch: ${getErrorMessage(error)}`; + core.warning(`[generate_git_patch] ${errorMessage}`); } - // Check if patch was generated and has content - if (patchGenerated && fs.existsSync(patchPath)) { - const patchContent = fs.readFileSync(patchPath, "utf8"); + if (patchGenerated && fs.existsSync(PATCH_PATH)) { + const patchContent = fs.readFileSync(PATCH_PATH, "utf8"); const patchSize = Buffer.byteLength(patchContent, "utf8"); const patchLines = patchContent.split("\n").length; if (!patchContent.trim()) { - // Empty patch return { success: false, error: "No changes to commit - patch is empty", - patchPath: patchPath, + patchPath: PATCH_PATH, patchSize: 0, patchLines: 0, }; @@ -117,17 +200,16 @@ function generateGitPatch(branchName) { return { success: true, - patchPath: patchPath, + patchPath: PATCH_PATH, patchSize: patchSize, patchLines: patchLines, }; } - // No patch generated return { success: false, error: errorMessage || "No changes to commit - no commits found", - patchPath: patchPath, + patchPath: PATCH_PATH, }; } diff --git a/actions/setup/js/generate_git_patch.test.cjs b/actions/setup/js/generate_git_patch.test.cjs index 9595bf92a4..b4b18d3cff 100644 --- a/actions/setup/js/generate_git_patch.test.cjs +++ b/actions/setup/js/generate_git_patch.test.cjs @@ -1,9 +1,26 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; + +// Mock the global objects that GitHub Actions provides +const mockCore = { + debug: vi.fn(), + info: vi.fn(), + notice: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + setFailed: vi.fn(), + setOutput: vi.fn(), +}; + +// Set up global mocks before importing the module +global.core = mockCore; describe("generateGitPatch", () => { let originalEnv; beforeEach(() => { + // Reset mocks before each test + vi.clearAllMocks(); + // Save original environment originalEnv = { GITHUB_SHA: process.env.GITHUB_SHA, diff --git a/actions/setup/js/safe-outputs-mcp-server.cjs b/actions/setup/js/safe-outputs-mcp-server.cjs index decf5dbb13..975af2ed3f 100644 --- a/actions/setup/js/safe-outputs-mcp-server.cjs +++ b/actions/setup/js/safe-outputs-mcp-server.cjs @@ -5,6 +5,10 @@ // This is the main entry point script for the safe-outputs MCP server // It starts the HTTP server on the configured port +// Load core shim before any other modules so that global.core is available +// for modules that rely on it (e.g. generate_git_patch.cjs). +require("./shim.cjs"); + const { createLogger } = require("./mcp_logger.cjs"); const logger = createLogger("safe-outputs-entry"); diff --git a/actions/setup/js/safe_inputs_mcp_server_http.cjs b/actions/setup/js/safe_inputs_mcp_server_http.cjs index 36aacbe35e..85029ee4e8 100644 --- a/actions/setup/js/safe_inputs_mcp_server_http.cjs +++ b/actions/setup/js/safe_inputs_mcp_server_http.cjs @@ -18,6 +18,10 @@ * --log-dir Directory for log files */ +// Load core shim before any other modules so that global.core is available +// for modules that rely on it. +require("./shim.cjs"); + const http = require("http"); const { randomUUID } = require("crypto"); const { MCPServer, MCPHTTPTransport } = require("./mcp_http_transport.cjs"); diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index d0a3032391..a4a2667cce 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -4,6 +4,20 @@ import fs from "fs"; import path from "path"; import { createHandlers } from "./safe_outputs_handlers.cjs"; +// Mock the global objects that GitHub Actions provides +const mockCore = { + debug: vi.fn(), + info: vi.fn(), + notice: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + setFailed: vi.fn(), + setOutput: vi.fn(), +}; + +// Set up global mocks before importing the module +global.core = mockCore; + describe("safe_outputs_handlers", () => { let mockServer; let mockAppendSafeOutput; @@ -11,6 +25,8 @@ describe("safe_outputs_handlers", () => { let testWorkspaceDir; beforeEach(() => { + vi.clearAllMocks(); + mockServer = { debug: vi.fn(), }; diff --git a/actions/setup/js/shim.cjs b/actions/setup/js/shim.cjs new file mode 100644 index 0000000000..d3b964e7b1 --- /dev/null +++ b/actions/setup/js/shim.cjs @@ -0,0 +1,36 @@ +// @ts-check + +/** + * shim.cjs + * + * Provides a minimal `global.core` shim so that modules written for the + * GitHub Actions `github-script` context (which rely on the built-in `core` + * global) work correctly when executed as plain Node.js processes, such as + * inside the safe-outputs and safe-inputs MCP servers. + * + * When `global.core` is already set (i.e. running inside `github-script`) + * this module is a no-op. + */ + +// @ts-expect-error - global.core is not declared in TypeScript but is provided by github-script +if (!global.core) { + // @ts-expect-error - Assigning to global properties that are declared as const + global.core = { + debug: /** @param {string} message */ message => console.debug(`[debug] ${message}`), + info: /** @param {string} message */ message => console.info(`[info] ${message}`), + notice: /** @param {string} message */ message => console.info(`[notice] ${message}`), + warning: /** @param {string} message */ message => console.warn(`[warning] ${message}`), + error: /** @param {string} message */ message => console.error(`[error] ${message}`), + setFailed: /** @param {string} message */ message => { + console.error(`[error] ${message}`); + if (typeof process !== "undefined") { + if (process.exitCode == null || process.exitCode === 0) { + process.exitCode = 1; + } + } + }, + setOutput: /** @param {string} name @param {unknown} value */ (name, value) => { + console.info(`[output] ${name}=${value}`); + }, + }; +} diff --git a/actions/setup/setup.sh b/actions/setup/setup.sh index 9f50df0928..a3cf8eb5f8 100755 --- a/actions/setup/setup.sh +++ b/actions/setup/setup.sh @@ -158,6 +158,7 @@ SAFE_INPUTS_FILES=( "setup_globals.cjs" "error_helpers.cjs" "mcp_enhanced_errors.cjs" + "shim.cjs" ) SAFE_INPUTS_COUNT=0 @@ -225,6 +226,7 @@ SAFE_OUTPUTS_FILES=( "git_helpers.cjs" "mcp_enhanced_errors.cjs" "comment_limit_helpers.cjs" + "shim.cjs" ) SAFE_OUTPUTS_COUNT=0 diff --git a/actions/setup/sh/generate_git_patch.sh b/actions/setup/sh/generate_git_patch.sh deleted file mode 100755 index 02edcb04e2..0000000000 --- a/actions/setup/sh/generate_git_patch.sh +++ /dev/null @@ -1,217 +0,0 @@ -# Diagnostic logging: Show environment information -echo "=== Diagnostic: Environment Information ===" -echo "GITHUB_SHA: ${GITHUB_SHA@Q}" -echo "DEFAULT_BRANCH: ${DEFAULT_BRANCH@Q}" -echo "Current HEAD: $(git rev-parse HEAD 2>/dev/null || echo 'unknown')" -echo "Current branch: $(git branch --show-current 2>/dev/null || echo 'detached HEAD')" - -# Diagnostic logging: Show recent commits before patch generation -echo "" -echo "=== Diagnostic: Recent commits (last 10) ===" -git log --oneline -10 || echo "Failed to show git log" - -# Check current git status -echo "" -echo "=== Diagnostic: Current git status ===" -git status - -# Extract branch name from JSONL output -BRANCH_NAME="" -if [ -f "$GH_AW_SAFE_OUTPUTS" ]; then - echo "" - echo "Checking for branch name in JSONL output..." - echo "JSONL file path: $GH_AW_SAFE_OUTPUTS" - while IFS= read -r line; do - if [ -n "$line" ]; then - # Extract branch from create-pull-request line using simple grep and sed - # Note: types use underscores (normalized by safe-outputs MCP server) - if echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"create_pull_request"'; then - echo "Found create_pull_request line: $line" - # Extract branch value using sed - BRANCH_NAME="$(echo "$line" | sed -n 's/.*"branch"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" - if [ -n "$BRANCH_NAME" ]; then - echo "Extracted branch name from create_pull_request: ${BRANCH_NAME@Q}" - break - fi - # Extract branch from push_to_pull_request_branch line using simple grep and sed - # Note: types use underscores (normalized by safe-outputs MCP server) - elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push_to_pull_request_branch"'; then - echo "Found push_to_pull_request_branch line: $line" - # Extract branch value using sed - BRANCH_NAME="$(echo "$line" | sed -n 's/.*"branch"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" - if [ -n "$BRANCH_NAME" ]; then - echo "Extracted branch name from push_to_pull_request_branch: ${BRANCH_NAME@Q}" - break - fi - fi - fi - done < "$GH_AW_SAFE_OUTPUTS" -else - echo "" - echo "GH_AW_SAFE_OUTPUTS file not found at: ${GH_AW_SAFE_OUTPUTS@Q}" -fi - -# If no branch found in JSONL, log it but don't give up yet -if [ -z "$BRANCH_NAME" ]; then - echo "" - echo "No branch name found in JSONL output" - echo "Will check for commits made to current HEAD instead" -fi - -# Strategy 1: If we have a branch name, check if that branch exists and get its diff -PATCH_GENERATED=false -if [ -n "$BRANCH_NAME" ]; then - echo "" - echo "=== Strategy 1: Using named branch from JSONL ===" - echo "Looking for branch: ${BRANCH_NAME@Q}" - # Check if the branch exists - if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then - echo "Branch ${BRANCH_NAME@Q} exists, generating patch from branch changes" - - # Check if origin/$BRANCH_NAME exists to use as base - if git show-ref --verify --quiet "refs/remotes/origin/$BRANCH_NAME"; then - echo "Using origin/${BRANCH_NAME@Q} as base for patch generation" - BASE_REF="origin/$BRANCH_NAME" - else - echo "origin/${BRANCH_NAME@Q} does not exist, using merge-base with default branch" - # Use the default branch name from environment variable - echo "Default branch: ${DEFAULT_BRANCH@Q}" - # Fetch the default branch to ensure it's available locally - git fetch origin "$DEFAULT_BRANCH" - # Find merge base between default branch and current branch - BASE_REF="$(git merge-base "origin/$DEFAULT_BRANCH" "$BRANCH_NAME")" - echo "Using merge-base as base: ${BASE_REF@Q}" - fi - - # Diagnostic logging: Show diff stats before generating patch - echo "" - echo "=== Diagnostic: Diff stats for patch generation ===" - echo "Command: git diff --stat ${BASE_REF@Q}..${BRANCH_NAME@Q}" - git diff --stat "$BASE_REF".."$BRANCH_NAME" || echo "Failed to show diff stats" - - # Diagnostic logging: Count commits to be included - echo "" - echo "=== Diagnostic: Commits to be included in patch ===" - COMMIT_COUNT="$(git rev-list --count "$BASE_REF".."$BRANCH_NAME" 2>/dev/null || echo "0")" - echo "Number of commits: $COMMIT_COUNT" - if [ "$COMMIT_COUNT" -gt 0 ]; then - echo "Commit SHAs:" - git log --oneline "$BASE_REF".."$BRANCH_NAME" || echo "Failed to list commits" - fi - - # Diagnostic logging: Show the exact command being used - echo "" - echo "=== Diagnostic: Generating patch ===" - echo "Command: git format-patch ${BASE_REF@Q}..${BRANCH_NAME@Q} --stdout > /tmp/gh-aw/aw.patch" - - # Generate patch from the determined base to the branch - git format-patch "$BASE_REF".."$BRANCH_NAME" --stdout > /tmp/gh-aw/aw.patch || echo "Failed to generate patch from branch" > /tmp/gh-aw/aw.patch - echo "Patch file created from branch: ${BRANCH_NAME@Q} (base: ${BASE_REF@Q})" - PATCH_GENERATED=true - else - echo "Branch ${BRANCH_NAME@Q} does not exist locally" - fi -fi - -# Strategy 2: Check if commits were made to current HEAD since checkout -if [ "$PATCH_GENERATED" = false ]; then - echo "" - echo "=== Strategy 2: Checking for commits on current HEAD ===" - - # Get current HEAD SHA - CURRENT_HEAD="$(git rev-parse HEAD 2>/dev/null || echo '')" - echo "Current HEAD: ${CURRENT_HEAD@Q}" - echo "Checkout SHA (GITHUB_SHA): ${GITHUB_SHA@Q}" - - if [ -z "$CURRENT_HEAD" ]; then - echo "ERROR: Could not determine current HEAD SHA" - elif [ -z "$GITHUB_SHA" ]; then - echo "ERROR: GITHUB_SHA environment variable is not set" - elif [ "$CURRENT_HEAD" = "$GITHUB_SHA" ]; then - echo "No commits have been made since checkout (HEAD == GITHUB_SHA)" - echo "No patch will be generated" - else - echo "HEAD has moved since checkout - checking if commits were added" - - # Check if GITHUB_SHA is an ancestor of current HEAD - if git merge-base --is-ancestor "$GITHUB_SHA" HEAD 2>/dev/null; then - echo "GITHUB_SHA is an ancestor of HEAD - commits were added" - - # Count commits between GITHUB_SHA and HEAD - COMMIT_COUNT="$(git rev-list --count "${GITHUB_SHA}..HEAD" 2>/dev/null || echo "0")" - echo "" - echo "=== Diagnostic: Commits added since checkout ===" - echo "Number of commits: $COMMIT_COUNT" - - if [ "$COMMIT_COUNT" -gt 0 ]; then - echo "Commit SHAs:" - git log --oneline "${GITHUB_SHA}..HEAD" || echo "Failed to list commits" - - # Show diff stats - echo "" - echo "=== Diagnostic: Diff stats for patch generation ===" - echo "Command: git diff --stat ${GITHUB_SHA@Q}..HEAD" - git diff --stat "${GITHUB_SHA}..HEAD" || echo "Failed to show diff stats" - - # Generate patch from GITHUB_SHA to HEAD - echo "" - echo "=== Diagnostic: Generating patch ===" - echo "Command: git format-patch ${GITHUB_SHA@Q}..HEAD --stdout > /tmp/gh-aw/aw.patch" - git format-patch "${GITHUB_SHA}..HEAD" --stdout > /tmp/gh-aw/aw.patch || echo "Failed to generate patch from HEAD" > /tmp/gh-aw/aw.patch - echo "Patch file created from commits on HEAD (base: ${GITHUB_SHA@Q})" - PATCH_GENERATED=true - else - echo "No commits found between GITHUB_SHA and HEAD" - fi - else - echo "GITHUB_SHA is not an ancestor of HEAD - repository state has diverged" - echo "This may indicate a rebase or other history rewriting operation" - echo "Will not generate patch due to ambiguous history" - fi - fi -fi - -# Final status -echo "" -if [ "$PATCH_GENERATED" = true ]; then - echo "=== Patch generation completed successfully ===" -else - echo "=== No patch generated ===" - echo "Reason: No commits found via branch name or HEAD analysis" -fi - -# Show patch info if it exists -if [ -f /tmp/gh-aw/aw.patch ]; then - echo "" - echo "=== Diagnostic: Patch file information ===" - ls -lh /tmp/gh-aw/aw.patch - - # Get patch file size in KB - PATCH_SIZE="$(du -k /tmp/gh-aw/aw.patch | cut -f1)" - echo "Patch file size: ${PATCH_SIZE} KB" - - # Count lines in patch - PATCH_LINES="$(wc -l < /tmp/gh-aw/aw.patch)" - echo "Patch file lines: $PATCH_LINES" - - # Extract and count commits from patch file (each commit starts with "From ") - PATCH_COMMITS="$(grep -c "^From [0-9a-f]\{40\}" /tmp/gh-aw/aw.patch 2>/dev/null || echo "0")" - echo "Commits included in patch: $PATCH_COMMITS" - - # List commit SHAs in the patch - if [ "$PATCH_COMMITS" -gt 0 ]; then - echo "Commit SHAs in patch:" - grep "^From [0-9a-f]\{40\}" /tmp/gh-aw/aw.patch | sed 's/^From \([0-9a-f]\{40\}\).*/ \1/' || echo "Failed to extract commit SHAs" - fi - - # Show the first 50 lines of the patch for review - { - echo '## Git Patch' - echo '' - echo '```diff' - head -500 /tmp/gh-aw/aw.patch || echo "Could not display patch contents" - echo '...' - echo '```' - echo '' - } >> "$GITHUB_STEP_SUMMARY" -fi diff --git a/pkg/workflow/git_patch_head_test.go b/pkg/workflow/git_patch_head_test.go deleted file mode 100644 index 1545064669..0000000000 --- a/pkg/workflow/git_patch_head_test.go +++ /dev/null @@ -1,410 +0,0 @@ -//go:build !integration - -package workflow - -import ( - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "github.com/github/gh-aw/pkg/testutil" -) - -// TestGitPatchFromHEADCommits tests that the patch generation script can detect -// and create patches from commits made directly to HEAD (without a named branch) -func TestGitPatchFromHEADCommits(t *testing.T) { - // Create a temporary directory for the test - tmpDir := testutil.TempDir(t, "test-patch-head-*") - - // Initialize a git repository - cmd := exec.Command("git", "init") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to init git repo: %v\nOutput: %s", err, output) - } - - // Configure git user for commits - cmd = exec.Command("git", "config", "user.email", "test@example.com") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to config git email: %v\nOutput: %s", err, output) - } - - cmd = exec.Command("git", "config", "user.name", "Test User") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to config git name: %v\nOutput: %s", err, output) - } - - // Create an initial commit (this will be our GITHUB_SHA) - testFile1 := filepath.Join(tmpDir, "initial.txt") - if err := os.WriteFile(testFile1, []byte("initial content\n"), 0644); err != nil { - t.Fatalf("Failed to write initial file: %v", err) - } - - cmd = exec.Command("git", "add", "initial.txt") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to add initial file: %v\nOutput: %s", err, output) - } - - cmd = exec.Command("git", "commit", "-m", "Initial commit") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to create initial commit: %v\nOutput: %s", err, output) - } - - // Get the initial commit SHA (this simulates GITHUB_SHA) - cmd = exec.Command("git", "rev-parse", "HEAD") - cmd.Dir = tmpDir - output, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("Failed to get initial SHA: %v\nOutput: %s", err, output) - } - initialSHA := strings.TrimSpace(string(output)) - - // Now simulate the LLM making commits directly to HEAD - // Commit 1: Add a new file - testFile2 := filepath.Join(tmpDir, "new-feature.txt") - if err := os.WriteFile(testFile2, []byte("new feature content\n"), 0644); err != nil { - t.Fatalf("Failed to write new file: %v", err) - } - - cmd = exec.Command("git", "add", "new-feature.txt") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to add new file: %v\nOutput: %s", err, output) - } - - cmd = exec.Command("git", "commit", "-m", "Add new feature") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to create first commit: %v\nOutput: %s", err, output) - } - - // Commit 2: Modify existing file - if err := os.WriteFile(testFile1, []byte("initial content\nupdated by LLM\n"), 0644); err != nil { - t.Fatalf("Failed to update initial file: %v", err) - } - - cmd = exec.Command("git", "add", "initial.txt") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to add updated file: %v\nOutput: %s", err, output) - } - - cmd = exec.Command("git", "commit", "-m", "Update initial file") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to create second commit: %v\nOutput: %s", err, output) - } - - // Now run the patch generation script - // The script creates the patch at /tmp/gh-aw/aw.patch - patchFile := "/tmp/gh-aw/aw.patch" - - // Ensure the /tmp/gh-aw directory exists - if err := os.MkdirAll("/tmp/gh-aw", 0755); err != nil { - t.Fatalf("Failed to create /tmp/gh-aw directory: %v", err) - } - - // Remove any existing patch file - os.Remove(patchFile) - - // Create a minimal safe-outputs file (empty - no branch name) - safeOutputsFile := filepath.Join(tmpDir, "safe-outputs.jsonl") - if err := os.WriteFile(safeOutputsFile, []byte(""), 0644); err != nil { - t.Fatalf("Failed to write safe-outputs file: %v", err) - } - - // Run the patch generation script from actions/setup/sh - scriptPath := filepath.Join("..", "..", "actions", "setup", "sh", "generate_git_patch.sh") - scriptContent, err := os.ReadFile(scriptPath) - if err != nil { - t.Fatalf("Failed to read script file: %v", err) - } - scriptFile := filepath.Join(tmpDir, "generate_patch.sh") - if err := os.WriteFile(scriptFile, scriptContent, 0755); err != nil { - t.Fatalf("Failed to write script file: %v", err) - } - - cmd = exec.Command("bash", scriptFile) - cmd.Dir = tmpDir - cmd.Env = append(os.Environ(), - "GH_AW_SAFE_OUTPUTS="+safeOutputsFile, - "GITHUB_SHA="+initialSHA, - "DEFAULT_BRANCH=main", - "GITHUB_STEP_SUMMARY=/dev/null", - ) - - // Capture the output for debugging - scriptOutput, err := cmd.CombinedOutput() - t.Logf("Script output:\n%s", scriptOutput) - - if err != nil { - t.Fatalf("Failed to run patch generation script: %v\nOutput: %s", err, scriptOutput) - } - - // Verify the patch file was created - if _, err := os.Stat(patchFile); os.IsNotExist(err) { - t.Fatal("Patch file was not created") - } - - // Read and verify the patch content - patchContent, err := os.ReadFile(patchFile) - if err != nil { - t.Fatalf("Failed to read patch file: %v", err) - } - - patchStr := string(patchContent) - - // Verify the patch contains both commits - if !strings.Contains(patchStr, "Add new feature") { - t.Error("Patch does not contain first commit message") - } - - if !strings.Contains(patchStr, "Update initial file") { - t.Error("Patch does not contain second commit message") - } - - // Verify the patch contains file changes - if !strings.Contains(patchStr, "new-feature.txt") { - t.Error("Patch does not contain new file") - } - - if !strings.Contains(patchStr, "initial.txt") { - t.Error("Patch does not contain modified file") - } - - // Verify patch format (should start with "From ") - if !strings.HasPrefix(patchStr, "From ") { - t.Error("Patch does not have correct format (should start with 'From ')") - } - - // Count commits in patch (each commit starts with "From ") - commitCount := strings.Count(patchStr, "\nFrom ") - if strings.HasPrefix(patchStr, "From ") { - commitCount++ // Count the first commit - } - - if commitCount != 2 { - t.Errorf("Expected 2 commits in patch, got %d", commitCount) - } - - // Verify script logged the strategy being used - if !strings.Contains(string(scriptOutput), "Strategy 2: Checking for commits on current HEAD") { - t.Error("Script output does not indicate Strategy 2 was used") - } - - if !strings.Contains(string(scriptOutput), "GITHUB_SHA is an ancestor of HEAD - commits were added") { - t.Error("Script output does not confirm commits were detected") - } - - t.Log("Successfully generated patch from HEAD commits without named branch") -} - -// TestGitPatchPrefersBranchOverHEAD tests that when both a branch name and HEAD commits exist, -// the script prefers the branch-based approach (Strategy 1) -func TestGitPatchPrefersBranchOverHEAD(t *testing.T) { - // Create a temporary directory for the test - tmpDir := testutil.TempDir(t, "test-patch-priority-*") - - // Initialize a git repository - cmd := exec.Command("git", "init") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to init git repo: %v\nOutput: %s", err, output) - } - - // Configure git user - cmd = exec.Command("git", "config", "user.email", "test@example.com") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to config git: %v\nOutput: %s", err, output) - } - - cmd = exec.Command("git", "config", "user.name", "Test User") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to config git: %v\nOutput: %s", err, output) - } - - // Create initial commit - testFile := filepath.Join(tmpDir, "file.txt") - if err := os.WriteFile(testFile, []byte("content\n"), 0644); err != nil { - t.Fatalf("Failed to write file: %v", err) - } - - cmd = exec.Command("git", "add", ".") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to add files: %v\nOutput: %s", err, output) - } - - cmd = exec.Command("git", "commit", "-m", "Initial") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to commit: %v\nOutput: %s", err, output) - } - - // Get initial SHA - cmd = exec.Command("git", "rev-parse", "HEAD") - cmd.Dir = tmpDir - output, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("Failed to get SHA: %v", err) - } - initialSHA := strings.TrimSpace(string(output)) - - // Create a named branch - cmd = exec.Command("git", "checkout", "-b", "feature-branch") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to create branch: %v\nOutput: %s", err, output) - } - - // Make a commit on the branch - if err := os.WriteFile(testFile, []byte("content\nupdated\n"), 0644); err != nil { - t.Fatalf("Failed to update file: %v", err) - } - - cmd = exec.Command("git", "add", ".") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to add files: %v\nOutput: %s", err, output) - } - - cmd = exec.Command("git", "commit", "-m", "Branch commit") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to commit: %v\nOutput: %s", err, output) - } - - // Create safe-outputs file with branch name - safeOutputsFile := filepath.Join(tmpDir, "safe-outputs.jsonl") - safeOutputsContent := "{\"type\":\"create_pull_request\",\"branch\":\"feature-branch\",\"title\":\"Test\",\"body\":\"Test\"}\n" - if err := os.WriteFile(safeOutputsFile, []byte(safeOutputsContent), 0644); err != nil { - t.Fatalf("Failed to write safe-outputs: %v", err) - } - - // Run the script from actions/setup/sh - scriptPath := filepath.Join("..", "..", "actions", "setup", "sh", "generate_git_patch.sh") - scriptContent, err := os.ReadFile(scriptPath) - if err != nil { - t.Fatalf("Failed to read script file: %v", err) - } - scriptFile := filepath.Join(tmpDir, "generate_patch.sh") - if err := os.WriteFile(scriptFile, scriptContent, 0755); err != nil { - t.Fatalf("Failed to write script: %v", err) - } - - // Ensure /tmp/gh-aw exists and is clean - patchFile := "/tmp/gh-aw/aw.patch" - if err := os.MkdirAll("/tmp/gh-aw", 0755); err != nil { - t.Fatalf("Failed to create /tmp/gh-aw: %v", err) - } - os.Remove(patchFile) - - cmd = exec.Command("bash", scriptFile) - cmd.Dir = tmpDir - cmd.Env = append(os.Environ(), - "GH_AW_SAFE_OUTPUTS="+safeOutputsFile, - "GITHUB_SHA="+initialSHA, - "DEFAULT_BRANCH=main", - "GITHUB_STEP_SUMMARY=/dev/null", - ) - - scriptOutput, err := cmd.CombinedOutput() - t.Logf("Script output:\n%s", scriptOutput) - - if err != nil { - t.Fatalf("Script failed: %v\nOutput: %s", err, scriptOutput) - } - - // Verify Strategy 1 was used (branch-based) - if !strings.Contains(string(scriptOutput), "Strategy 1: Using named branch from JSONL") { - t.Error("Expected Strategy 1 to be used when branch name is provided") - } - - if strings.Contains(string(scriptOutput), "Strategy 2: Checking for commits on current HEAD") { - t.Error("Strategy 2 should not run when Strategy 1 succeeds") - } -} - -// TestGitPatchNoCommits tests that no patch is generated when there are no commits -func TestGitPatchNoCommits(t *testing.T) { - tmpDir := testutil.TempDir(t, "test-patch-no-commits-*") - - // Initialize git repo - cmd := exec.Command("git", "init") - cmd.Dir = tmpDir - if output, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to init git: %v\nOutput: %s", err, output) - } - - // Configure git - cmd = exec.Command("git", "config", "user.email", "test@example.com") - cmd.Dir = tmpDir - cmd.Run() - - cmd = exec.Command("git", "config", "user.name", "Test User") - cmd.Dir = tmpDir - cmd.Run() - - // Create and commit a file - testFile := filepath.Join(tmpDir, "file.txt") - os.WriteFile(testFile, []byte("content\n"), 0644) - - cmd = exec.Command("git", "add", ".") - cmd.Dir = tmpDir - cmd.Run() - - cmd = exec.Command("git", "commit", "-m", "Initial") - cmd.Dir = tmpDir - cmd.Run() - - // Get SHA - cmd = exec.Command("git", "rev-parse", "HEAD") - cmd.Dir = tmpDir - output, _ := cmd.CombinedOutput() - currentSHA := strings.TrimSpace(string(output)) - - // Create empty safe-outputs - safeOutputsFile := filepath.Join(tmpDir, "safe-outputs.jsonl") - os.WriteFile(safeOutputsFile, []byte(""), 0644) - - // Run script with GITHUB_SHA = current HEAD (no new commits) from actions/setup/sh - scriptPath := filepath.Join("..", "..", "actions", "setup", "sh", "generate_git_patch.sh") - scriptContent, _ := os.ReadFile(scriptPath) - scriptFile := filepath.Join(tmpDir, "generate_patch.sh") - os.WriteFile(scriptFile, scriptContent, 0755) - - // Ensure /tmp/gh-aw exists and is clean - patchFile := "/tmp/gh-aw/aw.patch" - os.MkdirAll("/tmp/gh-aw", 0755) - os.Remove(patchFile) - - cmd = exec.Command("bash", scriptFile) - cmd.Dir = tmpDir - cmd.Env = append(os.Environ(), - "GH_AW_SAFE_OUTPUTS="+safeOutputsFile, - "GITHUB_SHA="+currentSHA, - "DEFAULT_BRANCH=main", - "GITHUB_STEP_SUMMARY=/dev/null", - ) - - scriptOutput, _ := cmd.CombinedOutput() - t.Logf("Script output:\n%s", scriptOutput) - - // Verify no patch was generated (patchFile was already defined above) - if _, err := os.Stat(patchFile); err == nil { - t.Error("Patch file should not be created when there are no commits") - } - - // Verify the script logged that no commits were found - if !strings.Contains(string(scriptOutput), "No commits have been made since checkout") { - t.Error("Script should log that no commits were made") - } -} diff --git a/test-pr-push-22233329042.txt b/test-pr-push-22233329042.txt new file mode 100644 index 0000000000..c61ea84c3e --- /dev/null +++ b/test-pr-push-22233329042.txt @@ -0,0 +1 @@ +Test file for PR push - smoke test run 22233329042 \ No newline at end of file