From 6c2e1d3c602d00cc6db92e70941409fbab5c23a3 Mon Sep 17 00:00:00 2001
From: cgeek <cem.moreau@gmail.com>
Date: Fri, 14 Jun 2024 12:16:19 +0200
Subject: [PATCH] feat(#195): `print-spec` and `release-network` commands

---
 Cargo.lock                                    | 304 ++++++++++++++++--
 Cargo.toml                                    |   1 +
 xtask/res/create_network_release.gql          |  16 +
 xtask/res/get_release.gql                     |   4 +-
 xtask/src/main.rs                             |  23 +-
 xtask/src/release_runtime.rs                  |  71 +++-
 .../release_runtime/create_network_release.rs |  76 +++++
 xtask/src/release_runtime/get_release.rs      |   6 +-
 8 files changed, 443 insertions(+), 58 deletions(-)
 create mode 100644 xtask/res/create_network_release.gql
 create mode 100644 xtask/src/release_runtime/create_network_release.rs

diff --git a/Cargo.lock b/Cargo.lock
index 44d19b957..b941ea064 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -850,6 +850,12 @@ version = "0.21.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
 
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
 [[package]]
 name = "base64ct"
 version = "1.6.0"
@@ -2500,6 +2506,7 @@ dependencies = [
  "pallet-transaction-payment-rpc",
  "pallet-transaction-payment-rpc-runtime-api",
  "pallet-treasury",
+ "reqwest 0.12.5",
  "rusty-hook",
  "sc-basic-authorship",
  "sc-chain-spec",
@@ -4027,7 +4034,7 @@ dependencies = [
  "futures-core",
  "futures-sink",
  "futures-util",
- "http",
+ "http 0.2.11",
  "indexmap 2.2.2",
  "slab",
  "tokio",
@@ -4224,6 +4231,17 @@ dependencies = [
  "itoa",
 ]
 
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
 [[package]]
 name = "http-body"
 version = "0.4.6"
@@ -4231,7 +4249,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
 dependencies = [
  "bytes",
- "http",
+ "http 0.2.11",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
+dependencies = [
+ "bytes",
+ "http 1.1.0",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.0",
  "pin-project-lite 0.2.13",
 ]
 
@@ -4270,8 +4311,8 @@ dependencies = [
  "futures-core",
  "futures-util",
  "h2",
- "http",
- "http-body",
+ "http 0.2.11",
+ "http-body 0.4.6",
  "httparse",
  "httpdate",
  "itoa",
@@ -4283,6 +4324,25 @@ dependencies = [
  "want",
 ]
 
+[[package]]
+name = "hyper"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "httparse",
+ "itoa",
+ "pin-project-lite 0.2.13",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
 [[package]]
 name = "hyper-rustls"
 version = "0.24.2"
@@ -4290,8 +4350,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
 dependencies = [
  "futures-util",
- "http",
- "hyper",
+ "http 0.2.11",
+ "hyper 0.14.28",
  "log",
  "rustls 0.21.10",
  "rustls-native-certs 0.6.3",
@@ -4300,6 +4360,24 @@ dependencies = [
  "webpki-roots 0.25.4",
 ]
 
+[[package]]
+name = "hyper-rustls"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
+dependencies = [
+ "futures-util",
+ "http 1.1.0",
+ "hyper 1.3.1",
+ "hyper-util",
+ "rustls 0.23.7",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls 0.26.0",
+ "tower-service",
+ "webpki-roots 0.26.3",
+]
+
 [[package]]
 name = "hyper-tls"
 version = "0.5.0"
@@ -4307,12 +4385,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
 dependencies = [
  "bytes",
- "hyper",
+ "hyper 0.14.28",
  "native-tls",
  "tokio",
  "tokio-native-tls",
 ]
 
+[[package]]
+name = "hyper-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "hyper 1.3.1",
+ "pin-project-lite 0.2.13",
+ "socket2 0.5.5",
+ "tokio",
+ "tower",
+ "tower-service",
+ "tracing",
+]
+
 [[package]]
 name = "iana-time-zone"
 version = "0.1.60"
@@ -4594,7 +4692,7 @@ dependencies = [
  "socket2 0.5.5",
  "widestring",
  "windows-sys 0.48.0",
- "winreg",
+ "winreg 0.50.0",
 ]
 
 [[package]]
@@ -4690,7 +4788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c8b3815d9f5d5de348e5f162b316dc9cdf4548305ebb15b4eb9328e66cf27d7a"
 dependencies = [
  "futures-util",
- "http",
+ "http 0.2.11",
  "jsonrpsee-core 0.16.3",
  "jsonrpsee-types 0.16.3",
  "pin-project",
@@ -4711,7 +4809,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9f9ed46590a8d5681975f126e22531698211b926129a40a2db47cbca429220"
 dependencies = [
  "futures-util",
- "http",
+ "http 0.2.11",
  "jsonrpsee-core 0.21.0",
  "pin-project",
  "rustls-native-certs 0.7.0",
@@ -4740,7 +4838,7 @@ dependencies = [
  "futures-timer",
  "futures-util",
  "globset",
- "hyper",
+ "hyper 0.14.28",
  "jsonrpsee-types 0.16.3",
  "parking_lot 0.12.1",
  "rand 0.8.5",
@@ -4765,7 +4863,7 @@ dependencies = [
  "beef",
  "futures-timer",
  "futures-util",
- "hyper",
+ "hyper 0.14.28",
  "jsonrpsee-types 0.21.0",
  "pin-project",
  "rustc-hash",
@@ -4784,8 +4882,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7e5f9fabdd5d79344728521bb65e3106b49ec405a78b66fbff073b72b389fa43"
 dependencies = [
  "async-trait",
- "hyper",
- "hyper-rustls",
+ "hyper 0.14.28",
+ "hyper-rustls 0.24.2",
  "jsonrpsee-core 0.16.3",
  "jsonrpsee-types 0.16.3",
  "rustc-hash",
@@ -4803,8 +4901,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "78b7de9f3219d95985eb77fd03194d7c1b56c19bce1abfcc9d07462574b15572"
 dependencies = [
  "async-trait",
- "hyper",
- "hyper-rustls",
+ "hyper 0.14.28",
+ "hyper-rustls 0.24.2",
  "jsonrpsee-core 0.21.0",
  "jsonrpsee-types 0.21.0",
  "serde",
@@ -4837,8 +4935,8 @@ checksum = "cf4d945a6008c9b03db3354fb3c83ee02d2faa9f2e755ec1dfb69c3551b8f4ba"
 dependencies = [
  "futures-channel",
  "futures-util",
- "http",
- "hyper",
+ "http 0.2.11",
+ "hyper 0.14.28",
  "jsonrpsee-core 0.16.3",
  "jsonrpsee-types 0.16.3",
  "serde",
@@ -4884,7 +4982,7 @@ version = "0.16.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4e1b3975ed5d73f456478681a417128597acd6a2487855fdb7b4a3d4d195bf5e"
 dependencies = [
- "http",
+ "http 0.2.11",
  "jsonrpsee-client-transport 0.16.3",
  "jsonrpsee-core 0.16.3",
  "jsonrpsee-types 0.16.3",
@@ -5252,7 +5350,7 @@ dependencies = [
  "libp2p-tls",
  "log",
  "parking_lot 0.12.1",
- "quinn-proto",
+ "quinn-proto 0.9.6",
  "rand 0.8.5",
  "rustls 0.20.9",
  "thiserror",
@@ -7796,6 +7894,23 @@ dependencies = [
  "pin-project-lite 0.1.12",
 ]
 
+[[package]]
+name = "quinn"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad"
+dependencies = [
+ "bytes",
+ "pin-project-lite 0.2.13",
+ "quinn-proto 0.11.3",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls 0.23.7",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
 [[package]]
 name = "quinn-proto"
 version = "0.9.6"
@@ -7814,6 +7929,36 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "quinn-proto"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe"
+dependencies = [
+ "bytes",
+ "rand 0.8.5",
+ "ring 0.17.7",
+ "rustc-hash",
+ "rustls 0.23.7",
+ "slab",
+ "thiserror",
+ "tinyvec",
+ "tracing",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46"
+dependencies = [
+ "libc",
+ "once_cell",
+ "socket2 0.5.5",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "quote"
 version = "1.0.35"
@@ -8074,9 +8219,9 @@ dependencies = [
  "futures-core",
  "futures-util",
  "h2",
- "http",
- "http-body",
- "hyper",
+ "http 0.2.11",
+ "http-body 0.4.6",
+ "hyper 0.14.28",
  "hyper-tls",
  "ipnet",
  "js-sys",
@@ -8090,7 +8235,7 @@ dependencies = [
  "serde",
  "serde_json",
  "serde_urlencoded",
- "sync_wrapper",
+ "sync_wrapper 0.1.2",
  "system-configuration",
  "tokio",
  "tokio-native-tls",
@@ -8099,7 +8244,49 @@ dependencies = [
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "web-sys",
- "winreg",
+ "winreg 0.50.0",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "http-body-util",
+ "hyper 1.3.1",
+ "hyper-rustls 0.27.2",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite 0.2.13",
+ "quinn",
+ "rustls 0.23.7",
+ "rustls-pemfile 2.0.0",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper 1.0.1",
+ "tokio",
+ "tokio-rustls 0.26.0",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots 0.26.3",
+ "winreg 0.52.0",
 ]
 
 [[package]]
@@ -8323,6 +8510,20 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "rustls"
+version = "0.23.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b"
+dependencies = [
+ "once_cell",
+ "ring 0.17.7",
+ "rustls-pki-types",
+ "rustls-webpki 0.102.2",
+ "subtle 2.5.0",
+ "zeroize",
+]
+
 [[package]]
 name = "rustls-native-certs"
 version = "0.6.3"
@@ -9157,8 +9358,8 @@ dependencies = [
  "fnv",
  "futures 0.3.30",
  "futures-timer",
- "hyper",
- "hyper-rustls",
+ "hyper 0.14.28",
+ "hyper-rustls 0.24.2",
  "libp2p",
  "log",
  "num_cpus",
@@ -9247,7 +9448,7 @@ name = "sc-rpc-server"
 version = "4.0.0-dev"
 source = "git+https://github.com/duniter/duniter-polkadot-sdk?branch=duniter-substrate-v1.6.0#c84530c57a6f9ab808a92f6b5d29338acf1c6e4f"
 dependencies = [
- "http",
+ "http 0.2.11",
  "jsonrpsee 0.16.3",
  "log",
  "serde_json",
@@ -10203,7 +10404,7 @@ dependencies = [
  "bytes",
  "flate2",
  "futures 0.3.30",
- "http",
+ "http 0.2.11",
  "httparse",
  "log",
  "rand 0.8.5",
@@ -11243,7 +11444,7 @@ name = "substrate-prometheus-endpoint"
 version = "0.10.0-dev"
 source = "git+https://github.com/duniter/duniter-polkadot-sdk?branch=duniter-substrate-v1.6.0#c84530c57a6f9ab808a92f6b5d29338acf1c6e4f"
 dependencies = [
- "hyper",
+ "hyper 0.14.28",
  "log",
  "prometheus",
  "thiserror",
@@ -11439,6 +11640,12 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
 
+[[package]]
+name = "sync_wrapper"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
+
 [[package]]
 name = "synstructure"
 version = "0.12.6"
@@ -11770,6 +11977,17 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "tokio-rustls"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
+dependencies = [
+ "rustls 0.23.7",
+ "rustls-pki-types",
+ "tokio",
+]
+
 [[package]]
 name = "tokio-stream"
 version = "0.1.14"
@@ -11861,6 +12079,7 @@ dependencies = [
  "futures-util",
  "pin-project",
  "pin-project-lite 0.2.13",
+ "tokio",
  "tower-layer",
  "tower-service",
  "tracing",
@@ -11876,8 +12095,8 @@ dependencies = [
  "bytes",
  "futures-core",
  "futures-util",
- "http",
- "http-body",
+ "http 0.2.11",
+ "http-body 0.4.6",
  "http-range-header",
  "pin-project-lite 0.2.13",
  "tower-layer",
@@ -12805,6 +13024,15 @@ version = "0.25.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
 
+[[package]]
+name = "webpki-roots"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd"
+dependencies = [
+ "rustls-pki-types",
+]
+
 [[package]]
 name = "weight-analyzer"
 version = "0.0.0"
@@ -13131,6 +13359,16 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if 1.0.0",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "ws2_32-sys"
 version = "0.2.1"
@@ -13203,7 +13441,7 @@ dependencies = [
  "memmap2 0.5.10",
  "parity-scale-codec",
  "placeholder",
- "reqwest",
+ "reqwest 0.11.24",
  "run_script",
  "scale-info",
  "scale-value",
diff --git a/Cargo.toml b/Cargo.toml
index cadf1cd16..54b9c46ec 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -57,6 +57,7 @@ substrate-build-script-utils = { git = 'https://github.com/duniter/duniter-polka
 
 [dev-dependencies]
 rusty-hook = "^0.11.2"
+reqwest = { version = "0.12.0", default-features = false, features = ["rustls-tls"] }
 
 # Dependencies for specific targets
 [target.'cfg(any(target_arch="x86_64", target_arch="aarch64"))'.dependencies]
diff --git a/xtask/res/create_network_release.gql b/xtask/res/create_network_release.gql
new file mode 100644
index 000000000..694afccf1
--- /dev/null
+++ b/xtask/res/create_network_release.gql
@@ -0,0 +1,16 @@
+mutation CreateReleaseMutation($branch: String!, $description: String!, $network: String! $links: [ReleaseAssetLinkInput!]!) {
+  releaseCreate(input: {
+    clientMutationId: "duniter-v2s-xtask"
+    description: $description
+    milestones: []
+    name: $network
+    projectPath: "nodes/rust/duniter-v2s"
+    ref: $branch
+    tagName: $network
+    assets: {
+      links: $links
+    }
+  }) {
+    errors
+  }
+}
diff --git a/xtask/res/get_release.gql b/xtask/res/get_release.gql
index 9ca58b82c..7fb7ef667 100644
--- a/xtask/res/get_release.gql
+++ b/xtask/res/get_release.gql
@@ -1,6 +1,6 @@
-query GetReleaseOfProjectQuery($milestone: String!) {
+query GetReleaseOfProjectQuery($tag: String!) {
     project(fullPath: "nodes/rust/duniter-v2s") {
-        release(tagName: $milestone) {
+        release(tagName: $tag) {
             id
             tagName
             assets {
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index c6b201b9d..7e4c82382 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -51,10 +51,12 @@ enum DuniterXTaskCommand {
         /// Raw spec filepath
         raw_spec: PathBuf,
     },
+    /// Release a new network
+    ReleaseNetwork { network: String, branch: String },
     /// Release a new runtime
     ReleaseRuntime { milestone: String, branch: String },
-    /// Update raw specs locally with the files published on a Release
-    UpdateRawSpecs { milestone: String },
+    /// Print the chainSpec published on given Network Release
+    PrintSpec { network: String },
     /// Create asset in a release
     CreateAssetLink {
         tag: String,
@@ -79,8 +81,14 @@ async fn main() -> Result<()> {
             );
         std::process::exit(1);
     }
-    Command::new("rustc").arg("--version").status()?;
-    Command::new("cargo").arg("--version").status()?;
+
+    match &args.command {
+        DuniterXTaskCommand::PrintSpec { .. } => { /* no print */ }
+        _ => {
+            Command::new("rustc").arg("--version").status()?;
+            Command::new("cargo").arg("--version").status()?;
+        }
+    }
 
     match args.command {
         DuniterXTaskCommand::Build { production } => build(production),
@@ -88,12 +96,13 @@ async fn main() -> Result<()> {
         DuniterXTaskCommand::InjectRuntimeCode { runtime, raw_spec } => {
             inject_runtime_code(&raw_spec, &runtime)
         }
+        DuniterXTaskCommand::ReleaseNetwork { network, branch } => {
+            release_runtime::release_network(network, branch).await
+        }
         DuniterXTaskCommand::ReleaseRuntime { milestone, branch } => {
             release_runtime::release_runtime(milestone, branch).await
         }
-        DuniterXTaskCommand::UpdateRawSpecs { milestone } => {
-            release_runtime::update_raw_specs(milestone).await
-        }
+        DuniterXTaskCommand::PrintSpec { network } => release_runtime::print_spec(network).await,
         DuniterXTaskCommand::CreateAssetLink {
             tag,
             asset_name,
diff --git a/xtask/src/release_runtime.rs b/xtask/src/release_runtime.rs
index 53bff4a97..c39f4a441 100644
--- a/xtask/src/release_runtime.rs
+++ b/xtask/src/release_runtime.rs
@@ -15,6 +15,7 @@
 // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
 
 mod create_asset_link;
+mod create_network_release;
 mod create_release;
 mod get_changes;
 mod get_issues;
@@ -61,6 +62,50 @@ struct CoreVersion {
     //transaction_version: u32,
 }
 
+pub(super) async fn release_network(network: String, branch: String) -> Result<()> {
+    let mut release_notes = String::from(
+        "
+# Runtime
+
+",
+    );
+
+    // Generate release notes
+    let currency = network.clone();
+    let env_var = "SRTOOL_OUTPUT".to_string();
+
+    if let Ok(sr_tool_output_file) = std::env::var(env_var) {
+        let read = fs::read_to_string(sr_tool_output_file);
+        match read {
+            Ok(sr_tool_output) => {
+                release_notes.push_str(
+                    gen_release_notes(currency.to_string(), sr_tool_output)
+                        .with_context(|| {
+                            format!("Fail to generate release notes for {}", currency)
+                        })?
+                        .as_str(),
+                );
+            }
+            Err(e) => {
+                eprintln!("srtool JSON output could not be read ({}). Skipped.", e)
+            }
+        }
+    }
+
+    println!("{}", release_notes);
+    let gitlab_token =
+        std::env::var("GITLAB_TOKEN").with_context(|| "missing env var GITLAB_TOKEN")?;
+    create_network_release::create_network_release(
+        gitlab_token,
+        branch,
+        network,
+        release_notes.to_string(),
+    )
+    .await?;
+
+    Ok(())
+}
+
 pub(super) async fn release_runtime(milestone: String, branch: String) -> Result<()> {
     // TODO: check spec_version in the code and bump if necessary (with a commit)
     // TODO: create and push a git tag runtime-{spec_version}
@@ -134,20 +179,22 @@ pub(super) async fn release_runtime(milestone: String, branch: String) -> Result
     Ok(())
 }
 
-pub(super) async fn update_raw_specs(milestone: String) -> Result<()> {
-    let specs = vec!["gdev-raw.json", "gtest-raw.json", "g1-raw.json"];
-    println!("Fetching release info…");
-    let assets = get_release::get_release(milestone).await?;
-    for spec in specs {
-        if let Some(gdev_raw_specs) = assets.iter().find(|asset| asset.ends_with(spec)) {
-            println!("Downloading {}…", spec);
-            let client = reqwest::Client::new();
-            let res = client.get(gdev_raw_specs).send().await?;
-            let write_to = format!("./node/specs/{}", spec);
-            fs::write(write_to, res.bytes().await?)?;
+pub(super) async fn print_spec(network: String) -> Result<()> {
+    let spec_file = match network.clone() {
+        network if network.starts_with("g1") => "g1.json",
+        network if network.starts_with("gtest") => "gtest.json",
+        network if network.starts_with("gdev") => "gdev.json",
+        _ => {
+            return Err(anyhow!("Invalid network"));
         }
+    };
+    let assets = get_release::get_release(network).await?;
+    if let Some(gdev_spec) = assets.iter().find(|asset| asset.ends_with(spec_file)) {
+        let client = reqwest::Client::new();
+        let res = client.get(gdev_spec).send().await?;
+        let spec = String::from_utf8(res.bytes().await?.to_vec())?;
+        println!("{}", spec);
     }
-    println!("Done.");
     Ok(())
 }
 
diff --git a/xtask/src/release_runtime/create_network_release.rs b/xtask/src/release_runtime/create_network_release.rs
new file mode 100644
index 000000000..6736a94ca
--- /dev/null
+++ b/xtask/src/release_runtime/create_network_release.rs
@@ -0,0 +1,76 @@
+// Copyright 2021 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+use anyhow::{anyhow, Result};
+use graphql_client::{GraphQLQuery, Response};
+
+#[derive(GraphQLQuery)]
+#[graphql(
+    schema_path = "res/schema.gql",
+    query_path = "res/create_network_release.gql",
+    response_derives = "Debug"
+)]
+pub struct CreateReleaseMutation;
+
+pub(super) async fn create_network_release(
+    gitlab_token: String,
+    branch: String,
+    network: String,
+    release_notes: String,
+) -> Result<()> {
+    // this is the important line
+    let request_body = CreateReleaseMutation::build_query(create_release_mutation::Variables {
+        branch,
+        description: release_notes,
+        network,
+        links: vec![],
+    });
+
+    let client = reqwest::Client::new();
+    let res = client
+        .post("https://git.duniter.org/api/graphql")
+        .header("PRIVATE-TOKEN", gitlab_token)
+        .json(&request_body)
+        .send()
+        .await?;
+    let response_body: Response<create_release_mutation::ResponseData> = res.json().await?;
+
+    if let Some(data) = response_body.data {
+        if let Some(release_create) = data.release_create {
+            if release_create.errors.is_empty() {
+                Ok(())
+            } else {
+                println!("{} errors:", release_create.errors.len());
+                for error in release_create.errors {
+                    println!("{}", error);
+                }
+                Err(anyhow!("Logic errors"))
+            }
+        } else if let Some(errors) = response_body.errors {
+            Err(anyhow!("Errors: {:?}", errors))
+        } else {
+            Err(anyhow!("Invalid response: no release_create"))
+        }
+    } else if let Some(errors) = response_body.errors {
+        println!("{} errors:", errors.len());
+        for error in errors {
+            println!("{}", error);
+        }
+        Err(anyhow!("GraphQL errors"))
+    } else {
+        Err(anyhow!("Invalid response: no data nor errors"))
+    }
+}
diff --git a/xtask/src/release_runtime/get_release.rs b/xtask/src/release_runtime/get_release.rs
index cec00aba9..238a3504d 100644
--- a/xtask/src/release_runtime/get_release.rs
+++ b/xtask/src/release_runtime/get_release.rs
@@ -25,12 +25,10 @@ use graphql_client::{GraphQLQuery, Response};
 )]
 pub struct GetReleaseOfProjectQuery;
 
-pub(super) async fn get_release(milestone: String) -> Result<Vec<String>> {
+pub(super) async fn get_release(tag: String) -> Result<Vec<String>> {
     // this is the important line
     let request_body =
-        GetReleaseOfProjectQuery::build_query(get_release_of_project_query::Variables {
-            milestone,
-        });
+        GetReleaseOfProjectQuery::build_query(get_release_of_project_query::Variables { tag });
 
     let client = reqwest::Client::new();
     let res = client
-- 
GitLab