Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .github/workflows/test-pr-head-sha.yml
Original file line number Diff line number Diff line change
@@ -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
191 changes: 191 additions & 0 deletions src/utils/vcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,11 +536,72 @@ fn find_matching_revs(
}

pub fn find_head() -> Result<String> {
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<String> {
// 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<String> {
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(
Expand Down Expand Up @@ -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");
}
}
Binary file added test_artifacts/app-debug.apk
Binary file not shown.