diff --git a/Cargo.lock b/Cargo.lock
index 085c7339e68cb32e2991f83b16a990021bd945f2..fb1b1b03ef6780a8a447945f607c539396d65766 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -133,6 +133,12 @@ version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd"
 
+[[package]]
+name = "ascii"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
+
 [[package]]
 name = "asn1_der"
 version = "0.7.4"
@@ -850,6 +856,19 @@ dependencies = [
  "os_str_bytes",
 ]
 
+[[package]]
+name = "combine"
+version = "3.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680"
+dependencies = [
+ "ascii",
+ "byteorder",
+ "either",
+ "memchr",
+ "unreachable",
+]
+
 [[package]]
 name = "common-runtime"
 version = "0.8.0-dev"
@@ -1627,6 +1646,15 @@ version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
 
+[[package]]
+name = "encoding_rs"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
 [[package]]
 name = "enum-as-inner"
 version = "0.3.3"
@@ -1704,6 +1732,28 @@ dependencies = [
  "futures 0.3.19",
 ]
 
+[[package]]
+name = "failure"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
+dependencies = [
+ "backtrace",
+ "failure_derive",
+]
+
+[[package]]
+name = "failure_derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
 [[package]]
 name = "fake-simd"
 version = "0.1.2"
@@ -1809,6 +1859,21 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
 [[package]]
 name = "fork-tree"
 version = "3.0.0"
@@ -2502,6 +2567,64 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "graphql-introspection-query"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f2a4732cf5140bd6c082434494f785a19cfb566ab07d1382c3671f5812fed6d"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "graphql-parser"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5613c31f18676f164112732202124f373bb2103ff017b3b85ca954ea6a66ada"
+dependencies = [
+ "combine",
+ "failure",
+]
+
+[[package]]
+name = "graphql_client"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9b58571cfc3cc42c3e8ff44fc6cfbb6c0dea17ed22d20f9d8f1efc4e8209a3f"
+dependencies = [
+ "graphql_query_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "graphql_client_codegen"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4bf9cd823359d74ad3d3ecf1afd4a975f4ff2f891cdf9a66744606daf52de8c"
+dependencies = [
+ "graphql-introspection-query",
+ "graphql-parser",
+ "heck 0.3.3",
+ "lazy_static",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "syn",
+]
+
+[[package]]
+name = "graphql_query_derive"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e56b093bfda71de1da99758b036f4cc811fd2511c8a76f75680e9ffbd2bb4251"
+dependencies = [
+ "graphql_client_codegen",
+ "proc-macro2",
+ "syn",
+]
+
 [[package]]
 name = "gtest-runtime"
 version = "3.0.0"
@@ -2736,9 +2859,9 @@ dependencies = [
 
 [[package]]
 name = "httparse"
-version = "1.5.1"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
+checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
 
 [[package]]
 name = "httpdate"
@@ -2754,9 +2877,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 
 [[package]]
 name = "hyper"
-version = "0.14.16"
+version = "0.14.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55"
+checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f"
 dependencies = [
  "bytes 1.1.0",
  "futures-channel",
@@ -2767,7 +2890,7 @@ dependencies = [
  "http-body",
  "httparse",
  "httpdate",
- "itoa 0.4.7",
+ "itoa 1.0.1",
  "pin-project-lite 0.2.7",
  "socket2 0.4.1",
  "tokio",
@@ -2809,6 +2932,19 @@ dependencies = [
  "webpki-roots 0.22.2",
 ]
 
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes 1.1.0",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
 [[package]]
 name = "ident_case"
 version = "1.0.1"
@@ -3019,7 +3155,7 @@ dependencies = [
  "socket2 0.3.19",
  "widestring",
  "winapi 0.3.9",
- "winreg",
+ "winreg 0.6.2",
 ]
 
 [[package]]
@@ -4320,6 +4456,12 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
 [[package]]
 name = "minimal-lexical"
 version = "0.2.1"
@@ -4538,6 +4680,24 @@ dependencies = [
  "rand 0.8.4",
 ]
 
+[[package]]
+name = "native-tls"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
 [[package]]
 name = "net2"
 version = "0.2.37"
@@ -4787,12 +4947,51 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "openssl"
+version = "0.10.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
+dependencies = [
+ "bitflags",
+ "cfg-if 1.0.0",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "openssl-probe"
 version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
 
+[[package]]
+name = "openssl-sys"
+version = "0.9.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "os_str_bytes"
 version = "6.0.0"
@@ -6209,6 +6408,43 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "reqwest"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92"
+dependencies = [
+ "base64",
+ "bytes 1.1.0",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "lazy_static",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding 2.1.0",
+ "pin-project-lite 0.2.7",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url 2.2.2",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg 0.10.1",
+]
+
 [[package]]
 name = "resolv-conf"
 version = "0.7.0"
@@ -7510,6 +7746,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa 1.0.1",
+ "ryu",
+ "serde",
+]
+
 [[package]]
 name = "sha-1"
 version = "0.8.2"
@@ -8839,6 +9087,16 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
 [[package]]
 name = "tokio-rustls"
 version = "0.22.0"
@@ -9188,6 +9446,15 @@ dependencies = [
  "subtle",
 ]
 
+[[package]]
+name = "unreachable"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
+dependencies = [
+ "void",
+]
+
 [[package]]
 name = "unsigned-varint"
 version = "0.5.1"
@@ -9783,6 +10050,15 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "ws2_32-sys"
 version = "0.2.1"
@@ -9817,14 +10093,17 @@ dependencies = [
  "anyhow",
  "clap",
  "frame-metadata",
+ "graphql_client",
  "hex",
  "memmap2 0.5.0",
  "parity-scale-codec",
  "placeholder",
+ "reqwest",
  "run_script",
  "scale-info",
  "serde",
  "serde_json",
+ "tokio",
  "version-compare",
  "version_check",
 ]
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index cb1e389cb49bd5f35b77855623575f05bc708e31..9f85cb58a42b07a9b0c7a251cc40f351618d59a5 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -17,12 +17,15 @@ anyhow = "1.0.32"
 clap = { version = "3.0", features = ["derive"] }
 codec = { package = "parity-scale-codec", version = "2", default-features = false, features = ["derive", "full", "bit-vec"] }
 frame-metadata = "14.0.0"
+graphql_client = "0.10.0"
 hex = "0.4"
 memmap2 = "0.5.0"
 placeholder = "1.1.3"
+reqwest = { version = "0.11.11", features = ["json"] }
 run_script = "0.6.3"
 scale-info = { version = "1.0.0", features = ["bit-vec"] }
-serde = "1"
+serde = { version = "1.0.101", features = ["derive"] }
 serde_json = "1.0"
+tokio = { version = "1.15.0", features = ["macros"] }
 version_check = "0.9.2"
 version-compare = "0.0.11"
diff --git a/xtask/res/create_release.gql b/xtask/res/create_release.gql
new file mode 100644
index 0000000000000000000000000000000000000000..de9c9d6170d40253c6a4f4bd3d7594f1ac8df830
--- /dev/null
+++ b/xtask/res/create_release.gql
@@ -0,0 +1,13 @@
+mutation CreateReleaseMutation($branch: String!, $description: String!, $milestone: String!) {
+  releaseCreate(input: {
+    clientMutationId: "duniter-v2s-xtask"
+    description: $description
+    milestones: [$milestone]
+    name: $milestone
+    projectPath: "nodes/rust/duniter-v2s"
+    ref: $branch
+    tagName: $milestone
+  }) {
+    errors
+  }
+}
diff --git a/xtask/res/get_changes.gql b/xtask/res/get_changes.gql
new file mode 100644
index 0000000000000000000000000000000000000000..2cb9db20866ae7efdf1ba14f10f39455d312b9f6
--- /dev/null
+++ b/xtask/res/get_changes.gql
@@ -0,0 +1,10 @@
+query GetChangesQuery($milestone: String!) {
+  project(fullPath: "nodes/rust/duniter-v2s") {
+		mergeRequests(milestoneTitle: $milestone, state: merged) {
+      nodes {
+        iid
+        title
+      }
+    }
+  }
+}
diff --git a/xtask/res/runtime_release_notes.template b/xtask/res/runtime_release_notes.template
index c33733edee15ce0f85ebb5001b2761cb5ca39e03..c1ee069f3189ef969bf86505d4c0dab5deddf3b3 100644
--- a/xtask/res/runtime_release_notes.template
+++ b/xtask/res/runtime_release_notes.template
@@ -5,12 +5,12 @@ The runtimes have been built using [{srtool_version}](https://github.com/parityt
 ## ÄžDev
 
 ```
-🏋️ Runtime Size:		{runtime_human_size} ({runtime_size} bytes)
-🔥 Core Version:		{core_version}
-🗜 Compressed:		Yes: {compression_percent} %
-🎁 Metadata version:		{metadata_version}
-🗳️ system.setCode hash:   {proposal_hash}
-#️⃣ Blake2-256 hash:    {blake2_256}
+🏋️ Runtime Size: {runtime_human_size} ({runtime_size} bytes)
+🔥 Core Version: {core_version}
+🗜 Compressed: Yes: {compression_percent} %
+🎁 Metadata version: {metadata_version}
+🗳️ system.setCode hash: {proposal_hash}
+#️⃣ Blake2-256 hash:  {blake2_256}
 ```
 
 # Changes
diff --git a/xtask/res/schema.gql b/xtask/res/schema.gql
new file mode 100644
index 0000000000000000000000000000000000000000..27559c05236ac9e6fa36a32e93f0452b172bdea3
--- /dev/null
+++ b/xtask/res/schema.gql
@@ -0,0 +1,85 @@
+schema {
+  query: Query
+  mutation: Mutation
+}
+
+type Query {
+  project(fullPath: ID!): Project
+  mergeRequest(id: MergeRequestID!): MergeRequest
+}
+
+type Mutation {
+  releaseCreate(input: ReleaseCreateInput!): ReleaseCreatePayload
+}
+
+type Project {
+  mergeRequests(
+    state: MergeRequestState
+    milestoneTitle: String
+  ): MergeRequestConnection
+}
+
+scalar MergeRequestID
+
+type MergeRequest {
+  conflicts: Boolean!
+  diffHeadSha: String
+  draft: Boolean!
+  headPipeline: Pipeline
+  id: ID!
+  iid: String!
+  mergeable: Boolean!
+  title: String!
+}
+
+type MergeRequestConnection {
+  count: Int!
+  nodes: [MergeRequest]
+}
+
+enum MergeRequestState {
+  opened
+  closed
+  locked
+  all
+  merged
+}
+
+type Pipeline {
+  active: Boolean!
+  cancelable: Boolean!
+  id: ID!
+  iid: String!
+}
+
+input ReleaseCreateInput {
+  projectPath: ID!
+  tagName: String!
+  name: String
+  description: String
+  milestones: [String!]
+  assets: ReleaseAssetsInput
+  clientMutationId: String
+}
+
+input ReleaseAssetsInput {
+  links: [ReleaseAssetLinkInput!]
+}
+
+input ReleaseAssetLinkInput {
+  name: String!
+  url: String!
+  directAssetPath: String
+  linkType: ReleaseAssetLinkType = OTHER
+}
+
+enum ReleaseAssetLinkType {
+  OTHER
+  RUNBOOK
+  PACKAGE
+  IMAGE
+}
+
+type ReleaseCreatePayload {
+  errors: [String!]!
+}
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 934d2f49640722c30154c4b44ed3e4c5585e05e0..c14b004c6df2483512669e88691bd80644a71f5e 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -56,7 +56,8 @@ enum DuniterXTaskCommand {
     Test,
 }
 
-fn main() -> Result<()> {
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> Result<()> {
     let args = DuniterXTask::parse();
 
     if !version_check::is_min_version(MIN_RUST_VERSION).unwrap_or(false)
@@ -78,7 +79,7 @@ fn main() -> Result<()> {
             inject_runtime_code(&raw_spec, &runtime)
         }
         DuniterXTaskCommand::ReleaseRuntime { spec_version } => {
-            release_runtime::release_runtime(spec_version)
+            release_runtime::release_runtime(spec_version).await
         }
         DuniterXTaskCommand::Test => test(),
     }
diff --git a/xtask/src/release_runtime.rs b/xtask/src/release_runtime.rs
index 978ea69827a3ecf44e033ca39407ff2027e9f1b5..084af923f1eaacffe07e064595ff7f13ea86280d 100644
--- a/xtask/src/release_runtime.rs
+++ b/xtask/src/release_runtime.rs
@@ -14,30 +14,33 @@
 // You should have received a copy of the GNU Affero General Public License
 // along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
 
+mod create_release;
+mod get_changes;
+
 use anyhow::{anyhow, Context, Result};
 use serde::Deserialize;
 use std::io::Read;
 use std::process::Command;
 
-#[derive(Deserialize)]
+#[derive(Default, Deserialize)]
 struct Srtool {
     gen: String,
     rustc: String,
     runtimes: SrtoolRuntimes,
 }
 
-#[derive(Deserialize)]
+#[derive(Default, Deserialize)]
 struct SrtoolRuntimes {
     compact: SrtoolRuntime,
     compressed: SrtoolRuntime,
 }
 
-#[derive(Deserialize)]
+#[derive(Default, Deserialize)]
 struct SrtoolRuntime {
     subwasm: SrtoolRuntimeSubWasm,
 }
 
-#[derive(Deserialize)]
+#[derive(Default, Deserialize)]
 struct SrtoolRuntimeSubWasm {
     core_version: String,
     metadata_version: u32,
@@ -46,7 +49,7 @@ struct SrtoolRuntimeSubWasm {
     proposal_hash: String,
 }
 
-pub(super) fn release_runtime(_spec_version: u32) -> Result<()> {
+pub(super) async fn release_runtime(spec_version: u32) -> Result<()> {
     // Get current dir
     let pwd = std::env::current_dir()?
         .into_os_string()
@@ -88,16 +91,20 @@ pub(super) fn release_runtime(_spec_version: u32) -> Result<()> {
     .with_context(|| "Fail to parse srtool json output")?;
 
     // Generate release notes
-    let release_notes =
-        gen_release_notes(srtool).with_context(|| "Fail to generate release notes")?;
+    let release_notes = gen_release_notes(spec_version, srtool)
+        .await
+        .with_context(|| "Fail to generate release notes")?;
 
     // TODO: Call gitlab API to publish the release notes (and upload the wasm)
     println!("{}", release_notes);
+    let gitlab_token =
+        std::env::var("GITLAB_TOKEN").with_context(|| "missing env var GITLAB_TOKEN")?;
+    create_release::create_release(gitlab_token, spec_version, release_notes).await?;
 
     Ok(())
 }
 
-fn gen_release_notes(srtool: Srtool) -> Result<String> {
+async fn gen_release_notes(spec_version: u32, srtool: Srtool) -> Result<String> {
     // Read template file
     const RELEASE_NOTES_TEMPLATE_FILEPATH: &str = "xtask/res/runtime_release_notes.template";
     let mut file = std::fs::File::open(RELEASE_NOTES_TEMPLATE_FILEPATH)?;
@@ -109,8 +116,8 @@ fn gen_release_notes(srtool: Srtool) -> Result<String> {
     let wasm = srtool.runtimes.compressed.subwasm;
     let compression_percent = (1.0 - (wasm.size as f64 / uncompressed_size as f64)) * 100.0;
 
-    // TODO: get changes (list of MRs) from gitlab API
-    let changes = String::new();
+    // Get changes (list of MRs) from gitlab API
+    let changes = get_changes::get_changes(spec_version).await?;
 
     // Fill template values
     let mut values = std::collections::HashMap::new();
diff --git a/xtask/src/release_runtime/create_release.rs b/xtask/src/release_runtime/create_release.rs
new file mode 100644
index 0000000000000000000000000000000000000000..11d0b3afa52ab3e011ac70508d5efc71a9c20c64
--- /dev/null
+++ b/xtask/src/release_runtime/create_release.rs
@@ -0,0 +1,72 @@
+// Copyright 2021 Axiom-Team
+//
+// This file is part of Substrate-Libre-Currency.
+//
+// Substrate-Libre-Currency 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.
+//
+// Substrate-Libre-Currency 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 Substrate-Libre-Currency. 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_release.gql",
+    response_derives = "Debug"
+)]
+pub struct CreateReleaseMutation;
+
+pub(super) async fn create_release(
+    gitlab_token: String,
+    spec_version: u32,
+    release_notes: String,
+) -> Result<()> {
+    // this is the important line
+    let request_body = CreateReleaseMutation::build_query(create_release_mutation::Variables {
+        branch: format!("release/runtime-{}", spec_version - (spec_version % 100)),
+        description: release_notes,
+        milestone: format!("runtime-{}", spec_version),
+    });
+
+    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 {
+            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_changes.rs b/xtask/src/release_runtime/get_changes.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0caaa68d3578ae3cb72ca9e018dcafcc429beb22
--- /dev/null
+++ b/xtask/src/release_runtime/get_changes.rs
@@ -0,0 +1,75 @@
+// Copyright 2021 Axiom-Team
+//
+// This file is part of Substrate-Libre-Currency.
+//
+// Substrate-Libre-Currency 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.
+//
+// Substrate-Libre-Currency 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 Substrate-Libre-Currency. 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/get_changes.gql",
+    response_derives = "Debug"
+)]
+pub struct GetChangesQuery;
+
+pub(super) async fn get_changes(spec_version: u32) -> Result<String> {
+    // this is the important line
+    let request_body = GetChangesQuery::build_query(get_changes_query::Variables {
+        milestone: format!("runtime-{}", spec_version),
+    });
+
+    let client = reqwest::Client::new();
+    let res = client
+        .post("https://git.duniter.org/api/graphql")
+        .json(&request_body)
+        .send()
+        .await?;
+    let response_body: Response<get_changes_query::ResponseData> = res.json().await?;
+
+    if let Some(data) = response_body.data {
+        if let Some(project) = data.project {
+            if let Some(merge_requests) = project.merge_requests {
+                if let Some(nodes) = merge_requests.nodes {
+                    let mut changes = String::new();
+                    for merge_request in nodes {
+                        if let Some(merge_request) = merge_request {
+                            changes.push_str(&format!(
+                                "* {mr_title} (!{mr_number})\n",
+                                mr_title = merge_request.title,
+                                mr_number = merge_request.iid
+                            ));
+                        }
+                    }
+                    Ok(changes)
+                } else {
+                    Err(anyhow!("No changes found"))
+                }
+            } else {
+                Err(anyhow!("No changes found"))
+            }
+        } else {
+            Err(anyhow!("Project not found"))
+        }
+    } 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"))
+    }
+}