diff --git a/.github/workflows/test-pr-head-sha.yml b/.github/workflows/test-pr-head-sha.yml new file mode 100644 index 0000000000..02e61b3761 --- /dev/null +++ b/.github/workflows/test-pr-head-sha.yml @@ -0,0 +1,125 @@ +name: Test PR Head SHA Detection + +on: + pull_request: + paths: + - 'src/utils/vcs.rs' + - '.github/workflows/test-pr-head-sha.yml' + +jobs: + test-head-sha-detection: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + run: | + rustup set profile minimal + rustup component add clippy rustfmt + + - name: Build sentry-cli + run: | + echo "=== Building sentry-cli ===" + cargo build --release + echo "✅ Built sentry-cli at: target/release/sentry-cli" + + - name: Show GitHub Actions Environment + run: | + echo "=== GitHub Actions Environment ===" + echo "GITHUB_EVENT_NAME: ${{ github.event_name }}" + echo "GITHUB_SHA (merge commit): ${{ github.sha }}" + echo "GITHUB_HEAD_REF: ${{ github.head_ref }}" + echo "GITHUB_BASE_REF: ${{ github.base_ref }}" + echo "PR Head SHA (actual commit): ${{ github.event.pull_request.head.sha }}" + echo "PR Base SHA: ${{ github.event.pull_request.base.sha }}" + echo "GITHUB_EVENT_PATH: $GITHUB_EVENT_PATH" + + if [ -f "$GITHUB_EVENT_PATH" ]; then + echo "" + echo "=== GitHub Event Payload Preview ===" + echo "Event file exists at: $GITHUB_EVENT_PATH" + echo "Looking for pull_request.head.sha in payload:" + grep -A 5 -B 2 '"head"' "$GITHUB_EVENT_PATH" | head -15 || echo "No head section found" + else + echo "No event file found at $GITHUB_EVENT_PATH" + fi + + - name: Test Built CLI Head SHA Detection + run: | + echo "=== Testing Real sentry-cli Head SHA Detection ===" + echo "" + + # Show what git thinks HEAD is (this will be the merge commit) + echo "Git HEAD (merge commit): $(git rev-parse HEAD)" + echo "PR Head SHA (from event): ${{ github.event.pull_request.head.sha }}" + echo "" + + if [ "$(git rev-parse HEAD)" != "${{ github.event.pull_request.head.sha }}" ]; then + echo "✅ PERFECT TEST SCENARIO: Git HEAD differs from PR head SHA!" + echo " This is exactly the problem our fix solves." + echo "" + else + echo "â„šī¸ Git HEAD matches PR head SHA (unusual scenario)" + echo "" + fi + + # Use the real APK file that should already be in the repo + ls -la test_artifacts/ || echo "No test_artifacts directory found" + + echo "=== Running sentry-cli build upload with debug logging ===" + echo "Command: ./target/release/sentry-cli --log-level debug --auth-token dummy_token_for_testing build -o test-org -p test-project upload test_artifacts/app-debug.apk" + echo "" + echo "🔍 WATCH FOR DEBUG LOGS BELOW:" + echo "Expected debug log: 'Using GitHub Actions PR head SHA from event payload: ${{ github.event.pull_request.head.sha }}'" + echo "" + echo "--- CLI OUTPUT START ---" + + # Test the CLI's head-sha detection with debug logging enabled + # Using a dummy auth token to get past auth check and reach the head-sha detection logic + ./target/release/sentry-cli --log-level debug --auth-token dummy_token_for_testing build -o test-org -p test-project upload test_artifacts/app-debug.apk || true + + echo "--- CLI OUTPUT END ---" + + echo "" + echo "=== Analysis ===" + echo "Expected behavior:" + echo " - CLI should detect GITHUB_EVENT_PATH environment variable" + echo " - CLI should extract PR head SHA: ${{ github.event.pull_request.head.sha }}" + echo " - CLI should use that instead of git HEAD: $(git rev-parse HEAD)" + + - name: Test CLI Without GitHub Context (Fallback Behavior) + run: | + echo "=== Testing Fallback Behavior (No GitHub Actions Context) ===" + + # Unset GitHub environment variables to test fallback + unset GITHUB_EVENT_PATH + unset GITHUB_EVENT_NAME + + echo "Running CLI without GitHub Actions context (with debug logging)..." + ./target/release/sentry-cli --log-level debug --auth-token dummy_token_for_testing build -o test-org -p test-project upload test_artifacts/app-debug.apk || true + + echo "" + echo "Expected: Should fall back to 'git rev-parse HEAD'" + + - name: Verify Our Fix Logic + run: | + echo "=== Verification Summary ===" + echo "" + echo "đŸŽ¯ Test Results:" + echo "1. Git HEAD (merge commit): $(git rev-parse HEAD)" + echo "2. PR Head SHA (real commit): ${{ github.event.pull_request.head.sha }}" + echo "3. GitHub Event Path exists: $([ -f "$GITHUB_EVENT_PATH" ] && echo "✅ YES" || echo "❌ NO")" + echo "" + + if [ "$(git rev-parse HEAD)" != "${{ github.event.pull_request.head.sha }}" ]; then + echo "✅ SUCCESS: This PR demonstrates the exact problem our fix solves!" + echo "" + echo "Without our fix: sentry-cli would use $(git rev-parse HEAD)" + echo "With our fix: sentry-cli should use ${{ github.event.pull_request.head.sha }}" + echo "" + echo "The fix detects GitHub Actions PR context and extracts the real commit SHA" + echo "from the event payload instead of using the temporary merge commit." + else + echo "â„šī¸ In this case, git HEAD happens to match the PR head SHA" + echo " But our fix still works correctly for the general case." + fi \ No newline at end of file diff --git a/src/utils/vcs.rs b/src/utils/vcs.rs index bc76f41333..0493fc13fd 100644 --- a/src/utils/vcs.rs +++ b/src/utils/vcs.rs @@ -536,11 +536,72 @@ fn find_matching_revs( } pub fn find_head() -> Result { + debug!("Finding HEAD"); + // If GITHUB_EVENT_PATH is set, try to extract PR head SHA from the event payload + if let Ok(event_path) = std::env::var("GITHUB_EVENT_PATH") { + debug!("Finding HEAD from event path: {}", event_path); + if let Ok(content) = std::fs::read_to_string(&event_path) { + if let Some(pr_head_sha) = extract_pr_head_sha_from_event(&content) { + debug!( + "Using GitHub Actions PR head SHA from event payload: {}", + pr_head_sha + ); + return Ok(pr_head_sha); + } + } + } + let repo = git2::Repository::open_from_env()?; let head = repo.revparse_single("HEAD")?; Ok(head.id().to_string()) } +/// Extracts the PR head SHA from GitHub Actions event payload JSON. +/// Returns None if not a PR event or if SHA cannot be extracted. +fn extract_pr_head_sha_from_event(json_content: &str) -> Option { + // Simple JSON parsing to extract pull_request.head.sha + // Look for the pattern: "pull_request": { ... "head": { ... "sha": "..." ... } ... } + + // Find if this is a pull_request event + if !json_content.contains("\"pull_request\"") { + debug!("Not a pull_request event"); + return None; + } + + // Find the pull_request section, then look for head.sha within it + if let Some(pr_start) = json_content.find("\"pull_request\":") { + let pr_section = &json_content[pr_start..]; + + // Look for "head": followed by "sha": within the pull_request section + if let Some(head_start) = pr_section.find("\"head\":") { + let head_section = &pr_section[head_start..]; + + // Find the next "sha": after "head": + if let Some(sha_start) = head_section.find("\"sha\":") { + let sha_line = &head_section[sha_start..]; + return extract_sha_from_line(sha_line); + } + } + } + + None +} + +/// Extracts SHA from a JSON line containing "sha": "abcd1234..." or "sha":"abcd1234..." +fn extract_sha_from_line(line: &str) -> Option { + if line.contains("\"sha\":") { + // Try with space first: "sha": " + if let Some(sha_part) = line.split("\"sha\": \"").nth(1) { + return sha_part.split('"').next().map(|sha| sha.to_owned()); + } + // Try without space: "sha":" + if let Some(sha_part) = line.split("\"sha\":\"").nth(1) { + return sha_part.split('"').next().map(|sha| sha.to_owned()); + } + } + None +} + /// Given commit specs, repos and remote_name this returns a list of head /// commits from it. pub fn find_heads( @@ -1466,4 +1527,134 @@ mod tests { std::env::remove_var("GITHUB_EVENT_NAME"); std::env::remove_var("GITHUB_REF"); } + + #[test] + fn test_extract_sha_from_line() { + // Test valid SHA extraction + assert_eq!( + extract_sha_from_line(" \"sha\": \"abc123def456\","), + Some("abc123def456".to_owned()) + ); + + // Test with different spacing + assert_eq!( + extract_sha_from_line("\"sha\":\"def789ghi012\""), + Some("def789ghi012".to_owned()) + ); + + // Test line without SHA + assert_eq!( + extract_sha_from_line(" \"ref\": \"refs/heads/main\","), + None + ); + + // Test empty line + assert_eq!(extract_sha_from_line(""), None); + } + + #[test] + fn test_extract_pr_head_sha_from_event() { + // Test valid PR event JSON + let pr_json = r#"{ + "action": "opened", + "number": 123, + "pull_request": { + "id": 789, + "head": { + "ref": "feature-branch", + "sha": "abc123def456789" + }, + "base": { + "ref": "main", + "sha": "def456ghi789012" + } + } +}"#; + + assert_eq!( + extract_pr_head_sha_from_event(pr_json), + Some("abc123def456789".to_owned()) + ); + + // Test non-PR event + let push_json = r#"{ + "action": "push", + "ref": "refs/heads/main", + "head_commit": { + "id": "xyz789abc123" + } +}"#; + + assert_eq!(extract_pr_head_sha_from_event(push_json), None); + + // Test malformed JSON (missing head SHA) + let malformed_json = r#"{ + "pull_request": { + "id": 789, + "head": { + "ref": "feature-branch" + } + } +}"#; + + assert_eq!(extract_pr_head_sha_from_event(malformed_json), None); + + // Test empty JSON + assert_eq!(extract_pr_head_sha_from_event("{}"), None); + + // Test real GitHub Actions PR event format (simplified) + let real_gh_json = r#"{ + "action": "synchronize", + "pull_request": { + "id": 2852219630, + "head": { + "label": "getsentry:no/test-pr-head-sha-workflow", + "ref": "no/test-pr-head-sha-workflow", + "sha": "19ef6adc4dbddf733db6e833e1f96fb056b6dba4" + }, + "base": { + "label": "getsentry:master", + "ref": "master", + "sha": "55e6bc8c264ce95164314275d805f477650c440d" + } + } +}"#; + + assert_eq!( + extract_pr_head_sha_from_event(real_gh_json), + Some("19ef6adc4dbddf733db6e833e1f96fb056b6dba4".to_owned()) + ); + } + + #[test] + fn test_find_head_with_github_event_path() { + use std::fs; + + let temp_dir = tempdir().expect("Failed to create temp dir"); + let event_file = temp_dir.path().join("event.json"); + + // Test with valid PR event + let pr_json = r#"{ + "action": "opened", + "pull_request": { + "head": { + "sha": "pr-head-sha-123" + } + } +}"#; + + fs::write(&event_file, pr_json).expect("Failed to write event file"); + + // Set GITHUB_EVENT_PATH and test find_head + std::env::set_var("GITHUB_EVENT_PATH", event_file.to_str().unwrap()); + + // Since we're not in a git repo, this would normally fail + // But with GITHUB_EVENT_PATH set, it should return the PR head SHA + let result = find_head(); + + std::env::remove_var("GITHUB_EVENT_PATH"); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "pr-head-sha-123"); + } } diff --git a/test_artifacts/app-debug.apk b/test_artifacts/app-debug.apk new file mode 100644 index 0000000000..4f2f3cd97d Binary files /dev/null and b/test_artifacts/app-debug.apk differ