[codesign] function to Upload to notary server (#2111)

diff --git a/codesign/lib/src/file_codesign_visitor.dart b/codesign/lib/src/file_codesign_visitor.dart
index 7ebc23d..56f7d1e 100644
--- a/codesign/lib/src/file_codesign_visitor.dart
+++ b/codesign/lib/src/file_codesign_visitor.dart
@@ -92,6 +92,7 @@
 ''';
   static const Duration _notarizationTimerDuration = Duration(seconds: 45);
   static final RegExp _notarytoolStatusCheckPattern = RegExp(r'[ ]*status: ([a-zA-z ]+)');
+  static final RegExp _notarytoolRequestPattern = RegExp(r'id: ([a-z0-9-]+)');
 
   static const String fixItInstructions = '''
 Codesign test failed.
@@ -273,10 +274,6 @@
     await completer.future;
   }
 
-  String uploadZipToNotary(File localFile, [int retryCount = 3]) {
-    throw UnimplementedError('will implement later');
-  }
-
   /// Make a request to the notary service to see if the notary job is finished.
   ///
   /// A return value of true means that notarization finished successfully,
@@ -319,4 +316,52 @@
     }
     throw CodesignException('Notarization failed with: $status\n$combinedOutput');
   }
+
+  /// Upload artifact to Apple notary service.
+  String uploadZipToNotary(File localFile, [int retryCount = 3, int sleepTime = 1]) {
+    while (retryCount > 0) {
+      final List<String> args = <String>[
+        'xcrun',
+        'notarytool',
+        'submit',
+        localFile.absolute.path,
+        '--apple-id',
+        codesignAppstoreId,
+        '--password',
+        appSpecificPassword,
+        '--team-id',
+        codesignTeamId,
+      ];
+
+      log.info('uploading ${args.join(' ')}');
+      final io.ProcessResult result = processManager.runSync(args);
+      if (result.exitCode != 0) {
+        throw CodesignException(
+            'Command "${args.join(' ')}" failed with exit code ${result.exitCode}\nStdout: ${result.stdout}\nStderr: ${result.stderr}');
+      }
+
+      final String combinedOutput = (result.stdout as String) + (result.stderr as String);
+      final RegExpMatch? match;
+      match = _notarytoolRequestPattern.firstMatch(combinedOutput);
+
+      if (match == null) {
+        log.warning('Failed to upload to the notary service with args: ${args.join(' ')}');
+        log.warning('{combinedOutput.trim()}');
+        retryCount -= 1;
+        log.warning('Trying again $retryCount more time${retryCount > 1 ? 's' : ''}...');
+        io.sleep(Duration(seconds: sleepTime));
+        continue;
+      }
+
+      final String requestUuid = match.group(1)!;
+      log.info('RequestUUID for ${localFile.path} is: $requestUuid');
+
+      return requestUuid;
+    }
+    log.warning('The upload to notary service failed after retries, and'
+        '  the output format does not match the current notary tool version.'
+        ' If after inspecting the output, you believe the process finished '
+        'successfully but was not detected, please contact flutter release engineers');
+    throw CodesignException('Failed to upload ${localFile.path} to the notary service');
+  }
 }
diff --git a/codesign/test/file_codesign_visitor_test.dart b/codesign/test/file_codesign_visitor_test.dart
index 4890a24..1b2bdb4 100644
--- a/codesign/test/file_codesign_visitor_test.dart
+++ b/codesign/test/file_codesign_visitor_test.dart
@@ -599,5 +599,180 @@
         ),
       );
     });
+
+    test('upload notary retries upon failure', () async {
+      fileSystem.file('${tempDir.absolute.path}/temp').createSync();
+      processManager.addCommands(<FakeCommand>[
+        FakeCommand(
+          command: <String>[
+            'xcrun',
+            'notarytool',
+            'submit',
+            '${tempDir.absolute.path}/temp',
+            '--apple-id',
+            randomString,
+            '--password',
+            randomString,
+            '--team-id',
+            randomString,
+          ],
+          stdout: '''Error uploading file.
+ Id: something that causes failure
+ path: /Users/flutter/Desktop/OvernightTextEditor_11.6.8.zip''',
+        ),
+        FakeCommand(
+          command: <String>[
+            'xcrun',
+            'notarytool',
+            'submit',
+            '${tempDir.absolute.path}/temp',
+            '--apple-id',
+            randomString,
+            '--password',
+            randomString,
+            '--team-id',
+            randomString,
+          ],
+          stdout: '''Successfully uploaded file.
+ id: 2efe2717-52ef-43a5-96dc-0797e4ca1041
+ path: /Users/flutter/Desktop/OvernightTextEditor_11.6.8.zip''',
+        ),
+      ]);
+
+      final String uuid = codesignVisitor.uploadZipToNotary(
+        fileSystem.file('${tempDir.absolute.path}/temp'),
+        3,
+        0,
+      );
+      expect(uuid, '2efe2717-52ef-43a5-96dc-0797e4ca1041');
+      final List<String> messages = records
+          .where((LogRecord record) => record.level == Level.WARNING)
+          .map((LogRecord record) => record.message)
+          .toList();
+      expect(
+        messages,
+        contains('Failed to upload to the notary service with args: '
+            'xcrun notarytool submit ${tempDir.absolute.path}/temp '
+            '--apple-id abcd1234 --password abcd1234 --team-id abcd1234'),
+      );
+      expect(
+        messages,
+        contains('Trying again 2 more times...'),
+      );
+    });
+
+    test('upload notary throws exception if exit code is unnormal', () async {
+      fileSystem.file('${tempDir.absolute.path}/temp').createSync();
+      processManager.addCommands(<FakeCommand>[
+        FakeCommand(
+          command: <String>[
+            'xcrun',
+            'notarytool',
+            'submit',
+            '${tempDir.absolute.path}/temp',
+            '--apple-id',
+            randomString,
+            '--password',
+            randomString,
+            '--team-id',
+            randomString,
+          ],
+          stdout: '''Error uploading file.
+ Id: something that causes failure
+ path: /Users/flutter/Desktop/OvernightTextEditor_11.6.8.zip''',
+          exitCode: -1,
+        ),
+      ]);
+
+      expect(
+        () => codesignVisitor.uploadZipToNotary(
+          fileSystem.file('${tempDir.absolute.path}/temp'),
+          1,
+          0,
+        ),
+        throwsA(
+          isA<CodesignException>(),
+        ),
+      );
+    });
+
+    test('upload notary throws exception after 3 default tries', () async {
+      fileSystem.file('${tempDir.absolute.path}/temp').createSync();
+      processManager.addCommands(<FakeCommand>[
+        FakeCommand(
+          command: <String>[
+            'xcrun',
+            'notarytool',
+            'submit',
+            '${tempDir.absolute.path}/temp',
+            '--apple-id',
+            randomString,
+            '--password',
+            randomString,
+            '--team-id',
+            randomString,
+          ],
+          stdout: '''Error uploading file.
+ Id: something that causes failure
+ path: /Users/flutter/Desktop/OvernightTextEditor_11.6.8.zip''',
+        ),
+        FakeCommand(
+          command: <String>[
+            'xcrun',
+            'notarytool',
+            'submit',
+            '${tempDir.absolute.path}/temp',
+            '--apple-id',
+            randomString,
+            '--password',
+            randomString,
+            '--team-id',
+            randomString,
+          ],
+          stdout: '''Error uploading file.
+ Id: something that causes failure
+ path: /Users/flutter/Desktop/OvernightTextEditor_11.6.8.zip''',
+        ),
+        FakeCommand(
+          command: <String>[
+            'xcrun',
+            'notarytool',
+            'submit',
+            '${tempDir.absolute.path}/temp',
+            '--apple-id',
+            randomString,
+            '--password',
+            randomString,
+            '--team-id',
+            randomString,
+          ],
+          stdout: '''Error uploading file.
+ Id: something that causes failure
+ path: /Users/flutter/Desktop/OvernightTextEditor_11.6.8.zip''',
+        ),
+      ]);
+
+      expect(
+        () => codesignVisitor.uploadZipToNotary(
+          fileSystem.file('${tempDir.absolute.path}/temp'),
+          3,
+          0,
+        ),
+        throwsA(
+          isA<CodesignException>(),
+        ),
+      );
+      final List<String> messages = records
+          .where((LogRecord record) => record.level == Level.WARNING)
+          .map((LogRecord record) => record.message)
+          .toList();
+      expect(
+        messages,
+        contains('The upload to notary service failed after retries, and'
+            '  the output format does not match the current notary tool version.'
+            ' If after inspecting the output, you believe the process finished '
+            'successfully but was not detected, please contact flutter release engineers'),
+      );
+    });
   });
 }