Changing the JSON metadata format for the website. (#15834)

diff --git a/dev/bots/prepare_package.dart b/dev/bots/prepare_package.dart
index 3e86375..be2a5b9 100644
--- a/dev/bots/prepare_package.dart
+++ b/dev/bots/prepare_package.dart
@@ -461,6 +461,42 @@
     await _updateMetadata();
   }
 
+  Map<String, dynamic> _addRelease(Map<String, dynamic> jsonData) {
+    jsonData['base_url'] = '$baseUrl$releaseFolder';
+    if (!jsonData.containsKey('current_release')) {
+      jsonData['current_release'] = <String, String>{};
+    }
+    jsonData['current_release'][branchName] = revision;
+    if (!jsonData.containsKey('releases')) {
+      jsonData['releases'] = <Map<String, dynamic>>[];
+    }
+
+    final Map<String, dynamic> newEntry = <String, dynamic>{};
+    newEntry['hash'] = revision;
+    newEntry['channel'] = branchName;
+    newEntry['version'] = version;
+    newEntry['release_date'] = new DateTime.now().toUtc().toIso8601String();
+    newEntry['archive'] = destinationArchivePath;
+
+    // Search for any entries with the same hash and channel and remove them.
+    final List<dynamic> releases = jsonData['releases'];
+    final List<Map<String, dynamic>> prunedReleases = <Map<String, dynamic>>[];
+    for (Map<String, dynamic> entry in releases) {
+      if (entry['hash'] != newEntry['hash'] || entry['channel'] != newEntry['channel']) {
+        prunedReleases.add(entry);
+      }
+    }
+
+    prunedReleases.add(newEntry);
+    prunedReleases.sort((Map<String, dynamic> a, Map<String, dynamic> b) {
+      final DateTime aDate = DateTime.parse(a['release_date']);
+      final DateTime bDate = DateTime.parse(b['release_date']);
+      return bDate.compareTo(aDate);
+    });
+    jsonData['releases'] = prunedReleases;
+    return jsonData;
+  }
+
   Future<Null> _updateMetadata() async {
     // We can't just cat the metadata from the server with 'gsutil cat', because
     // Windows wants to echo the commands that execute in gsutil.bat to the
@@ -482,23 +518,7 @@
       throw new ProcessRunnerException('Unable to parse JSON metadata received from cloud: $e');
     }
 
-    // Update the metadata file with the data for this package.
-    jsonData['base_url'] = '$baseUrl$releaseFolder';
-    if (!jsonData.containsKey('current_release')) {
-      jsonData['current_release'] = <String, String>{};
-    }
-    jsonData['current_release'][branchName] = revision;
-    if (!jsonData.containsKey('releases')) {
-      jsonData['releases'] = <String, dynamic>{};
-    }
-    if (!jsonData['releases'].containsKey(revision)) {
-      jsonData['releases'][revision] = <String, Map<String, String>>{};
-    }
-    final Map<String, String> metadata = <String, String>{};
-    metadata['${platformName}_archive'] = destinationArchivePath;
-    metadata['release_date'] = new DateTime.now().toUtc().toIso8601String();
-    metadata['version'] = version;
-    jsonData['releases'][revision][branchName] = metadata;
+    jsonData = _addRelease(jsonData);
 
     const JsonEncoder encoder = const JsonEncoder.withIndent('  ');
     metadataFile.writeAsStringSync(encoder.convert(jsonData));
diff --git a/dev/bots/test/prepare_package_test.dart b/dev/bots/test/prepare_package_test.dart
index cd166b5..f1272ce 100644
--- a/dev/bots/test/prepare_package_test.dart
+++ b/dev/bots/test/prepare_package_test.dart
@@ -110,8 +110,7 @@
       test('sets PUB_CACHE properly', () async {
         final String createBase = path.join(tmpDir.absolute.path, 'create_');
         final Map<String, List<ProcessResult>> calls = <String, List<ProcessResult>>{
-          'git clone -b dev https://chromium.googlesource.com/external/github.com/flutter/flutter':
-              null,
+          'git clone -b dev https://chromium.googlesource.com/external/github.com/flutter/flutter': null,
           'git reset --hard $testRef': null,
           'git remote set-url origin https://github.com/flutter/flutter.git': null,
           'git describe --tags --abbrev=0': <ProcessResult>[new ProcessResult(0, 0, 'v1.2.3', '')],
@@ -154,8 +153,7 @@
       test('calls the right commands for archive output', () async {
         final String createBase = path.join(tmpDir.absolute.path, 'create_');
         final Map<String, List<ProcessResult>> calls = <String, List<ProcessResult>>{
-          'git clone -b dev https://chromium.googlesource.com/external/github.com/flutter/flutter':
-              null,
+          'git clone -b dev https://chromium.googlesource.com/external/github.com/flutter/flutter': null,
           'git reset --hard $testRef': null,
           'git remote set-url origin https://github.com/flutter/flutter.git': null,
           'git describe --tags --abbrev=0': <ProcessResult>[new ProcessResult(0, 0, 'v1.2.3', '')],
@@ -234,27 +232,38 @@
         final String archiveName = platform.isLinux ? 'archive.tar.xz' : 'archive.zip';
         final String archiveMime = platform.isLinux ? 'application/x-gtar' : 'application/zip';
         final String archivePath = path.join(tempDir.absolute.path, archiveName);
-        final String gsArchivePath = 'gs://flutter_infra/releases/dev/$platformName/$archiveName';
+        final String gsArchivePath = 'gs://flutter_infra/releases/release/$platformName/$archiveName';
         final String jsonPath = path.join(tempDir.absolute.path, releasesName);
         final String gsJsonPath = 'gs://flutter_infra/releases/$releasesName';
         final String releasesJson = '''{
-    "base_url": "https://storage.googleapis.com/flutter_infra/releases",
-    "current_release": {
-        "beta": "6da8ec6bd0c4801b80d666869e4069698561c043",
-        "dev": "f88c60b38c3a5ef92115d24e3da4175b4890daba"
+  "base_url": "https://storage.googleapis.com/flutter_infra/releases",
+  "current_release": {
+    "beta": "3ea4d06340a97a1e9d7cae97567c64e0569dcaa2",
+    "dev": "5a58b36e36b8d7aace89d3950e6deb307956a6a0"
+  },
+  "releases": [
+    {
+      "hash": "5a58b36e36b8d7aace89d3950e6deb307956a6a0",
+      "channel": "dev",
+      "version": "v0.2.3",
+      "release_date": "2018-03-20T01:47:02.851729Z",
+      "archive": "dev/$platformName/flutter_${platformName}_v0.2.3-dev.zip"
     },
-    "releases": {
-        "6da8ec6bd0c4801b80d666869e4069698561c043": {
-            "${platformName}_archive": "dev/linux/flutter_${platformName}_0.21.0-beta.zip",
-            "release_date": "2017-12-19T10:30:00,847287019-08:00",
-            "version": "0.21.0-beta"
-        },
-        "f88c60b38c3a5ef92115d24e3da4175b4890daba": {
-            "${platformName}_archive": "dev/linux/flutter_${platformName}_0.22.0-dev.zip",
-            "release_date": "2018-01-19T13:30:09,728487019-08:00",
-            "version": "0.22.0-dev"
-        }
+    {
+      "hash": "b9bd51cc36b706215915711e580851901faebb40",
+      "channel": "beta",
+      "version": "v0.2.2",
+      "release_date": "2018-03-16T18:48:13.375013Z",
+      "archive": "dev/$platformName/flutter_${platformName}_v0.2.2-dev.zip"
+    },
+    {
+      "hash": "$testRef",
+      "channel": "release",
+      "version": "v0.0.0",
+      "release_date": "2018-03-20T01:47:02.851729Z",
+      "archive": "release/$platformName/flutter_${platformName}_v0.0.0-dev.zip"
     }
+  ]
 }
 ''';
         new File(jsonPath).writeAsStringSync(releasesJson);
@@ -271,8 +280,8 @@
         final ArchivePublisher publisher = new ArchivePublisher(
           tempDir,
           testRef,
-          Branch.dev,
-          '1.2.3',
+          Branch.release,
+          'v1.2.3',
           outputFile,
           processManager: processManager,
           subprocessOutput: false,
@@ -285,15 +294,25 @@
         expect(releaseFile.existsSync(), isTrue);
         final String contents = releaseFile.readAsStringSync();
         // Make sure new data is added.
-        expect(contents, contains('"dev": "$testRef"'));
-        expect(contents, contains('"$testRef": {'));
-        expect(contents, contains('"${platformName}_archive": "dev/$platformName/$archiveName"'));
+        expect(contents, contains('"hash": "$testRef"'));
+        expect(contents, contains('"channel": "release"'));
+        expect(contents, contains('"archive": "release/$platformName/$archiveName"'));
         // Make sure existing entries are preserved.
-        expect(contents, contains('"6da8ec6bd0c4801b80d666869e4069698561c043": {'));
-        expect(contents, contains('"f88c60b38c3a5ef92115d24e3da4175b4890daba": {'));
-        expect(contents, contains('"beta": "6da8ec6bd0c4801b80d666869e4069698561c043"'));
-        // Make sure it's valid JSON, and in the right format.
+        expect(contents, contains('"hash": "5a58b36e36b8d7aace89d3950e6deb307956a6a0"'));
+        expect(contents, contains('"hash": "b9bd51cc36b706215915711e580851901faebb40"'));
+        expect(contents, contains('"channel": "beta"'));
+        expect(contents, contains('"channel": "dev"'));
+        // Make sure old matching entries are removed.
+        expect(contents, isNot(contains('v0.0.0')));
         final Map<String, dynamic> jsonData = json.decode(contents);
+        final List<dynamic> releases = jsonData['releases'];
+        expect(releases.length, equals(3));
+        // Make sure the new entry is first (and hopefully it takes less than a
+        // minute to go from publishArchive above to this line!).
+        expect(
+          new DateTime.now().difference(DateTime.parse(releases[0]['release_date'])),
+          lessThan(const Duration(minutes: 1)),
+        );
         const JsonEncoder encoder = const JsonEncoder.withIndent('  ');
         expect(contents, equals(encoder.convert(jsonData)));
       });