diff --git a/swh/vault/cookers/git_bare.py b/swh/vault/cookers/git_bare.py
--- a/swh/vault/cookers/git_bare.py
+++ b/swh/vault/cookers/git_bare.py
@@ -76,6 +76,7 @@
 class RootObjectType(enum.Enum):
     DIRECTORY = "directory"
     REVISION = "revision"
+    RELEASE = "release"
     SNAPSHOT = "snapshot"
 
 
@@ -105,6 +106,8 @@
         """Returns whether the root object is present in the archive."""
         if self.obj_type is RootObjectType.REVISION:
             return not list(self.storage.revision_missing([self.obj_id]))
+        elif self.obj_type is RootObjectType.RELEASE:
+            return not list(self.storage.release_missing([self.obj_id]))
         elif self.obj_type is RootObjectType.DIRECTORY:
             return not list(self.storage.directory_missing([self.obj_id]))
         elif self.obj_type is RootObjectType.SNAPSHOT:
@@ -236,6 +239,28 @@
                 "\n".join(sorted(unexpected_errors)),
             )
 
+    def _make_stub_directory_revision(self, dir_id: Sha1Git) -> Sha1Git:
+        author = Person.from_fullname(
+            b"swh-vault, git-bare cooker <robot@softwareheritage.org>"
+        )
+        dt = datetime.datetime.now(tz=datetime.timezone.utc)
+        dt = dt.replace(microsecond=0)  # not supported by git
+        date = TimestampWithTimezone.from_datetime(dt)
+
+        revision = Revision(
+            author=author,
+            committer=author,
+            date=date,
+            committer_date=date,
+            message=b"Initial commit",
+            type=RevisionType.GIT,
+            directory=self.obj_id,
+            synthetic=True,
+        )
+        self.write_revision_node(revision)
+
+        return revision.id
+
     def write_refs(self, snapshot=None):
         """Writes all files in :file:`.git/refs/`.
 
@@ -243,26 +268,28 @@
         refs: Dict[bytes, bytes]  # ref name -> target
         if self.obj_type == RootObjectType.DIRECTORY:
             # We need a synthetic revision pointing to the directory
-            author = Person.from_fullname(
-                b"swh-vault, git-bare cooker <robot@softwareheritage.org>"
-            )
-            dt = datetime.datetime.now(tz=datetime.timezone.utc)
-            dt = dt.replace(microsecond=0)  # not supported by git
-            date = TimestampWithTimezone.from_datetime(dt)
-            revision = Revision(
-                author=author,
-                committer=author,
-                date=date,
-                committer_date=date,
-                message=b"Initial commit",
-                type=RevisionType.GIT,
-                directory=self.obj_id,
-                synthetic=True,
-            )
-            self.write_revision_node(revision)
-            refs = {b"refs/heads/master": hash_to_bytehex(revision.id)}
+            rev_id = self._make_stub_directory_revision(self.obj_id)
+
+            refs = {b"refs/heads/master": hash_to_bytehex(rev_id)}
         elif self.obj_type == RootObjectType.REVISION:
             refs = {b"refs/heads/master": hash_to_bytehex(self.obj_id)}
+        elif self.obj_type == RootObjectType.RELEASE:
+            (release,) = self.storage.release_get([self.obj_id])
+
+            if release.name and re.match(br"^[a-zA-Z0-9_.-]+$", release.name):
+                release_name = release.name
+            else:
+                release_name = b"release"
+
+            refs = {
+                b"refs/tags/" + release_name: hash_to_bytehex(self.obj_id),
+            }
+
+            if release.target_type.value == ModelObjectType.REVISION:
+                # Not necessary, but makes it easier to browse
+                refs[b"ref/heads/master"] = hash_to_bytehex(release.target)
+            # TODO: synthetize a master branch for other target types
+
         elif self.obj_type == RootObjectType.SNAPSHOT:
             if snapshot is None:
                 # refs were already written in a previous step
@@ -341,6 +368,8 @@
             self._push(self._dir_stack, [obj_id])
         elif self.obj_type is RootObjectType.SNAPSHOT:
             self.push_snapshot_subgraph(obj_id)
+        elif self.obj_type is RootObjectType.RELEASE:
+            self.push_releases_subgraphs([obj_id])
         else:
             assert_never(self.obj_type, f"Unexpected root object type: {self.obj_type}")
 
diff --git a/swh/vault/tests/test_git_bare_cooker.py b/swh/vault/tests/test_git_bare_cooker.py
--- a/swh/vault/tests/test_git_bare_cooker.py
+++ b/swh/vault/tests/test_git_bare_cooker.py
@@ -10,6 +10,7 @@
 """
 
 import datetime
+import enum
 import io
 import subprocess
 import tarfile
@@ -41,26 +42,99 @@
 from swh.vault.in_memory_backend import InMemoryVaultBackend
 
 
+class RootObjects(enum.Enum):
+    REVISION = enum.auto()
+    SNAPSHOT = enum.auto()
+    RELEASE = enum.auto()
+    WEIRD_RELEASE = enum.auto()  # has a : in the name + points to another release
+
+
 @pytest.mark.graph
 @pytest.mark.parametrize(
-    "snapshot,up_to_date_graph,tag,weird_branches",
+    "root_object,up_to_date_graph,tag,weird_branches",
     [
-        # 'no snp' implies no tag or tree, because there can only be one root object
         param(
-            False, False, False, False, id="no snp, outdated graph, no tag/tree/blob"
+            RootObjects.REVISION,
+            False,
+            False,
+            False,
+            id="rev, outdated graph, no tag/tree/blob",
+        ),
+        param(
+            RootObjects.REVISION,
+            True,
+            False,
+            False,
+            id="rev, updated graph, no tag/tree/blob",
+        ),
+        param(
+            RootObjects.RELEASE,
+            False,
+            False,
+            False,
+            id="rel, outdated graph, no tag/tree/blob",
+        ),
+        param(
+            RootObjects.RELEASE,
+            True,
+            False,
+            False,
+            id="rel, updated graph, no tag/tree/blob",
+        ),
+        param(
+            RootObjects.WEIRD_RELEASE,
+            True,
+            False,
+            False,
+            id="weird rel, updated graph, no tag/tree/blob",
+        ),
+        param(
+            RootObjects.SNAPSHOT,
+            False,
+            False,
+            False,
+            id="snp, outdated graph, no tag/tree/blob",
+        ),
+        param(
+            RootObjects.SNAPSHOT,
+            True,
+            False,
+            False,
+            id="snp, updated graph, no tag/tree/blob",
+        ),
+        param(
+            RootObjects.SNAPSHOT,
+            False,
+            True,
+            False,
+            id="snp, outdated graph, w/ tag, no tree/blob",
+        ),
+        param(
+            RootObjects.SNAPSHOT,
+            True,
+            True,
+            False,
+            id="snp, updated graph, w/ tag, no tree/blob",
+        ),
+        param(
+            RootObjects.SNAPSHOT,
+            False,
+            True,
+            True,
+            id="snp, outdated graph, w/ tag, tree, and blob",
         ),
-        param(False, True, False, False, id="no snp, updated graph, no tag/tree/blob"),
-        param(True, False, False, False, id="snp, outdated graph, no tag/tree/blob"),
-        param(True, True, False, False, id="snp, updated graph, no tag/tree/blob"),
-        param(True, False, True, False, id="snp, outdated graph, w/ tag, no tree/blob"),
-        param(True, True, True, False, id="snp, updated graph, w/ tag, no tree/blob"),
         param(
-            True, False, True, True, id="snp, outdated graph, w/ tag, tree, and blob"
+            RootObjects.SNAPSHOT,
+            True,
+            True,
+            True,
+            id="snp, updated graph, w/ tag, tree, and blob",
         ),
-        param(True, True, True, True, id="snp, updated graph, w/ tag, tree, and blob"),
     ],
 )
-def test_graph_revisions(swh_storage, up_to_date_graph, snapshot, tag, weird_branches):
+def test_graph_revisions(
+    swh_storage, up_to_date_graph, root_object, tag, weird_branches
+):
     r"""
     Build objects::
 
@@ -187,6 +261,13 @@
         target=rel3.id,
         synthetic=True,
     )
+    rel5 = Release(
+        name=b"1.0.0:weirdname",
+        message=b"weird release",
+        target_type=ObjectType.RELEASE,
+        target=rel2.id,
+        synthetic=True,
+    )
 
     # Create snapshot:
 
@@ -229,7 +310,7 @@
             edges.append((rel2, rev2))
             edges.append((snp, rel2))
         if weird_branches:
-            nodes.extend([cnt3, cnt4, cnt5, dir3, dir4, rel3, rel4])
+            nodes.extend([cnt3, cnt4, cnt5, dir3, dir4, rel3, rel4, rel5])
             edges.extend(
                 [
                     (dir3, cnt3),
@@ -239,6 +320,7 @@
                     (snp, rel4),
                     (rel4, rel3),
                     (rel3, cnt5),
+                    (rel5, rev2),
                 ]
             )
     else:
@@ -263,7 +345,7 @@
     swh_storage.content_add([cnt1, cnt2, cnt3, cnt4, cnt5])
     swh_storage.directory_add([dir1, dir2, dir3, dir4])
     swh_storage.revision_add([rev1, rev2])
-    swh_storage.release_add([rel2, rel3, rel4])
+    swh_storage.release_add([rel2, rel3, rel4, rel5])
     swh_storage.snapshot_add([snp])
 
     # Add spy on swh_storage, to make sure revision_log is not called
@@ -275,10 +357,12 @@
 
     # Cook
     backend = InMemoryVaultBackend()
-    if snapshot:
-        cooked_swhid = snp.swhid()
-    else:
-        cooked_swhid = rev2.swhid()
+    cooked_swhid = {
+        RootObjects.SNAPSHOT: snp.swhid(),
+        RootObjects.REVISION: rev2.swhid(),
+        RootObjects.RELEASE: rel2.swhid(),
+        RootObjects.WEIRD_RELEASE: rel5.swhid(),
+    }[root_object]
     cooker = GitBareCooker(
         cooked_swhid, backend=backend, storage=swh_storage, graph=swh_graph,
     )
@@ -298,6 +382,15 @@
         with tarfile.open(fileobj=io.BytesIO(bundle)) as tf:
             tf.extractall(tempdir)
 
+        if root_object in (RootObjects.SNAPSHOT, RootObjects.REVISION):
+            log_head = "master"
+        elif root_object == RootObjects.RELEASE:
+            log_head = "1.0.0"
+        elif root_object == RootObjects.WEIRD_RELEASE:
+            log_head = "release"
+        else:
+            assert False, root_object
+
         output = subprocess.check_output(
             [
                 "git",
@@ -306,13 +399,14 @@
                 "log",
                 "--format=oneline",
                 "--decorate=",
+                log_head,
             ]
         )
 
         assert output.decode() == f"{rev2.id.hex()} msg2\n{rev1.id.hex()} msg1\n"
 
     # Make sure the graph was used instead of swh_storage.revision_log
-    if snapshot:
+    if root_object == RootObjects.SNAPSHOT:
         if up_to_date_graph:
             # The graph has everything, so the first call succeeds and returns
             # all objects transitively pointed by the snapshot
@@ -329,10 +423,17 @@
                     unittest.mock.call(str(rev2.swhid()), edges="rev:rev"),
                 ]
             )
-    else:
+    elif root_object in (
+        RootObjects.REVISION,
+        RootObjects.RELEASE,
+        RootObjects.WEIRD_RELEASE,
+    ):
         swh_graph.visit_nodes.assert_has_calls(
             [unittest.mock.call(str(rev2.swhid()), edges="rev:rev")]
         )
+    else:
+        assert False, root_object
+
     if up_to_date_graph:
         swh_storage.revision_log.assert_not_called()
         swh_storage.revision_shortlog.assert_not_called()