Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ concurrency:

jobs:
claude-review:
# The Claude action requires the PR author to have write access to the upstream repo.
# Fork PRs run under the fork author's identity, which makes automated review fail
# during the action's permission preflight.
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is good. I don't think we need the changes below

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept the fork-PR skip and reverted the extra permission/token changes below it. I also re-ran actionlint on .github/workflows/claude-code-review.yml before pushing the update.

# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@ jobs:

test-ios:
needs: detect-changes
if: needs.detect-changes.outputs.ios == 'true'
if: |
needs.detect-changes.outputs.ios == 'true' &&
(
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository
)
uses: ./.github/workflows/test-ios.yml
secrets: inherit

Expand Down
72 changes: 57 additions & 15 deletions crates/xmtp_mls/src/groups/mls_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2555,21 +2555,7 @@ where
err = ?err
);

if (intent.publish_attempts + 1) as usize >= MAX_INTENT_PUBLISH_ATTEMPTS {
tracing::error!(
intent.id,
intent.kind = %intent.kind,
inbox_id = self.context.inbox_id(),
installation_id = %self.context.installation_id(),group_id = hex::encode(&self.group_id),
"intent {} has reached max publish attempts", intent.id);
// TODO: Eventually clean up errored attempts
let id = utils::id::calculate_message_id_for_intent(&intent)?;
db.set_group_intent_error_and_fail_msg(&intent, id)?;
} else {
// Reset so the next retry re-encrypts at the current epoch.
db.increment_intent_publish_attempt_count(intent.id)?;
db.set_group_intent_to_publish(intent.id)?;
}
handle_published_intent_send_failure(&db, &intent)?;
return Err(err)?;
}
(kind, Ok(_)) => {
Expand Down Expand Up @@ -3909,13 +3895,37 @@ pub(crate) fn decode_staged_commit(
Ok(xmtp_db::db_deserialize(data)?)
}

fn handle_published_intent_send_failure<Db: QueryGroupIntent>(
db: &Db,
intent: &StoredGroupIntent,
) -> Result<(), GroupError> {
if (intent.publish_attempts + 1) as usize >= MAX_INTENT_PUBLISH_ATTEMPTS {
tracing::error!(
intent.id,
intent.kind = %intent.kind,
"intent {} has reached max publish attempts",
intent.id
);
let id = utils::id::calculate_message_id_for_intent(intent)?;
db.set_group_intent_error_and_fail_msg(intent, id)?;
} else {
// Reset so the next retry re-encrypts at the current epoch.
db.increment_intent_publish_attempt_count(intent.id)?;
db.set_group_intent_to_publish(intent.id)?;
}

Ok(())
}

#[cfg(test)]
pub(crate) mod tests {

use super::*;
use crate::{builder::ClientBuilder, utils::TestMlsGroup};
use mockall::predicate::eq;
use std::sync::Arc;
use xmtp_cryptography::utils::generate_local_wallet;
use xmtp_db::mock::MockDbQuery;

/// This test is not reproducible in webassembly, b/c webassembly has only one thread.
#[cfg_attr(
Expand Down Expand Up @@ -3991,6 +4001,38 @@ pub(crate) mod tests {
assert_eq!(hmac_keys[2].epoch, current_epoch + 1);
}

#[test]
fn send_failures_for_published_intents_revert_to_to_publish() {
let intent = StoredGroupIntent {
id: 42,
kind: IntentKind::SendMessage,
group_id: xmtp_common::rand_vec::<16>(),
data: Vec::new(),
state: IntentState::Published,
payload_hash: Some(xmtp_common::rand_vec::<32>()),
post_commit_data: None,
publish_attempts: 0,
staged_commit: None,
published_in_epoch: Some(7),
should_push: false,
sequence_id: None,
originator_id: None,
};

let mut db = MockDbQuery::new();
db.expect_increment_intent_publish_attempt_count()
.with(eq(intent.id))
.times(1)
.returning(|_| Ok(()));
db.expect_set_group_intent_to_publish()
.with(eq(intent.id))
.times(1)
.returning(|_| Ok(()));

let result = handle_published_intent_send_failure(&db, &intent);
assert!(result.is_ok());
}

/// Test that process_delete_message handles completely malformed bytes gracefully
///
/// This verifies sync resilience when receiving corrupted DeleteMessage protos.
Expand Down
Loading