diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9e1b7d09742b2f7126feb6d1caef94326f401add..5073f7a4d413e63e6b5017146119c00c817970e2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -450,15 +450,16 @@ src-distrib-tests-long:
 .build_template: &prepare_ssh_template
   before_script:
     - export GIT_SSH=$PWD/nix/frama-c-public/ssh.sh
-    - echo "$FRAMA_CI_BOT_SSH_PRIVATE" | nix run -f channel:nixos-19.03 coreutils --command base64 -d > nix/frama-c-public/id_ed25519
-    - nix run -f channel:nixos-19.03 coreutils --command chmod 400 nix/frama-c-public/id_ed25519
 
 release-branch:
   stage: release
+  variables:
+    REPOSITORY: "frama-c"
   <<: *prepare_ssh_template
   script:
     - (! git merge-base --is-ancestor a1e186c68a6418a53b3dc06237f49e8dcbf75f4a origin/$CI_COMMIT_BRANCH)
-    - nix run -f channel:nixos-19.03 openssh --command git push git@git.frama-c.com:pub/frama-c.git origin/$CI_COMMIT_BRANCH:refs/heads/$CI_COMMIT_BRANCH
+    - BRANCH="$CI_COMMIT_BRANCH" nix-shell -p coreutils openssh --run './nix/frama-c-public/publish-branch.sh'
+    # Note: BRANCH must be defined here, since we cannot *evaluate* a variable in 'variables'
   only:
     variables:
       - $RELEASE == "yes"
@@ -527,10 +528,13 @@ release-wiki:
 
 publish-frama-c:
   stage: publish
+  variables:
+    BRANCH: "master"
+    REPOSITORY: "frama-c"
   <<: *prepare_ssh_template
   script:
     - (! git merge-base --is-ancestor a1e186c68a6418a53b3dc06237f49e8dcbf75f4a origin/master)
-    - nix run -f channel:nixos-19.03 openssh --command git push git@git.frama-c.com:pub/frama-c.git origin/master:refs/heads/master
+    -  nix-shell -p coreutils openssh --run './nix/frama-c-public/publish-branch.sh'
   only:
     variables:
       - $PUBLISH == "yes"
@@ -538,10 +542,12 @@ publish-frama-c:
 
 publish-fclang:
   stage: publish
+  variables:
+    BRANCH: "master"
+    REPOSITORY: "frama-clang"
   <<: *prepare_ssh_template
   script:
-    - nix run -f channel:nixos-19.03 openssh --command git clone git@git.frama-c.com:frama-c/frama-clang.git nix/frama-c-public/frama-clang
-    - nix run -f channel:nixos-19.03 openssh --command git -C nix/frama-c-public/frama-clang push git@git.frama-c.com:pub/frama-clang origin/master:refs/heads/master
+    - nix-shell -p coreutils openssh --run './nix/frama-c-public/publish-branch.sh'
   only:
     variables:
       - $PUBLISH == "yes"
@@ -549,10 +555,12 @@ publish-fclang:
 
 publish-meta:
   stage: publish
+  variables:
+    BRANCH: "master"
+    REPOSITORY: "meta"
   <<: *prepare_ssh_template
   script:
-    - nix run -f channel:nixos-19.03 openssh --command git clone git@git.frama-c.com:frama-c/meta.git nix/frama-c-public/meta
-    - nix run -f channel:nixos-19.03 openssh --command git -C nix/frama-c-public/meta push git@git.frama-c.com:pub/meta origin/master:refs/heads/master
+    - nix-shell -p coreutils openssh --run './nix/frama-c-public/publish-branch.sh'
   only:
     variables:
       - $PUBLISH == "yes"
diff --git a/nix/frama-c-public/publish-branch.sh b/nix/frama-c-public/publish-branch.sh
new file mode 100755
index 0000000000000000000000000000000000000000..de4cae9e39d5a272883d1df3c986a4b8cd95a31d
--- /dev/null
+++ b/nix/frama-c-public/publish-branch.sh
@@ -0,0 +1,80 @@
+#! /usr/bin/env bash
+##########################################################################
+#                                                                        #
+#  This file is part of Frama-C.                                         #
+#                                                                        #
+#  Copyright (C) 2007-2022                                               #
+#    CEA (Commissariat à l'énergie atomique et aux énergies              #
+#         alternatives)                                                  #
+#                                                                        #
+#  you can redistribute it and/or modify it under the terms of the GNU   #
+#  Lesser General Public License as published by the Free Software       #
+#  Foundation, version 2.1.                                              #
+#                                                                        #
+#  It 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 Lesser General Public License for more details.                   #
+#                                                                        #
+#  See the GNU Lesser General Public License version 2.1                 #
+#  for more details (enclosed in the file licenses/LGPLv2.1).            #
+#                                                                        #
+##########################################################################
+
+# Note:
+#
+# This script *shall not* be run locally, it is meant to run in the
+# Frama-C CI when it is in publish or release mode.
+# The following variables are necessary:
+# - CI (provided by the GitLab pipeline)
+# - PUBLISH or RELEASE set to yes
+# - REPOSITORY: repository to publish
+# - BRANCH: the branch to push
+
+##########################################################################
+
+# Sanity checks
+
+if [[ "$CI" != "true" ]]; then
+  echo "Error: Do NOT run this script locally"
+  exit 2
+fi
+
+if [[ "$PUBLISH" != "yes" && "$RELEASE" != "yes" ]]; then
+  echo "Error: Publication only available in publish or release mode"
+  exit 2
+fi
+
+if [[ "$REPOSITORY" == "" ]]; then
+  echo "Error: REPOSITORY is not defined"
+  exit 2
+fi
+
+if [[ "$BRANCH" == "" ]]; then
+  echo "Error: BRANCH is not defined"
+  exit 2
+fi
+
+if [[ "$PUBLISH" == "yes" && "$BRANCH" != "master" ]]; then
+  echo "Error: In PUBLISH mode, only master can be pushed"
+  exit 2
+fi
+
+if [[ "$RELEASE" == "yes" && ! "$BRANCH" =~ stable/* ]]; then
+  echo "Error: In RELEASE mode, only a stable/ branch can be pushed"
+  exit 2
+fi
+
+# Actual work
+
+echo "$FRAMA_CI_BOT_SSH_PRIVATE" | base64 -d > nix/frama-c-public/id_ed25519
+chmod 400 nix/frama-c-public/id_ed25519
+
+if [[ "$REPOSITORY" != "frama-c" ]] ; then
+  DIRECTORY="nix/frama-c-public/$REPOSITORY"
+  git clone "git@git.frama-c.com:frama-c/$REPOSITORY.git" "$DIRECTORY"
+else
+  DIRECTORY="."
+fi
+
+git -C "$DIRECTORY" push "git@git.frama-c.com:pub/$REPOSITORY" "origin/$BRANCH":"refs/heads/$BRANCH"