summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rwxr-xr-xci/buildserver-build.sh34
-rw-r--r--ci/buildserver-config.sh26
-rw-r--r--ci/linux-repository-builder/build-linux-repositories-config.sh32
-rwxr-xr-xci/linux-repository-builder/build-linux-repositories.sh182
-rw-r--r--ci/linux-repository-builder/build-linux-repositories@.service24
-rw-r--r--ci/linux-repository-builder/build-linux-repositories@.timer9
-rwxr-xr-xci/linux-repository-builder/prepare-apt-repository.sh (renamed from ci/prepare-apt-repository.sh)50
-rwxr-xr-xci/linux-repository-builder/prepare-rpm-repository.sh102
-rw-r--r--ci/mullvad-browser/download-mullvad-browser.service17
-rwxr-xr-xci/mullvad-browser/download-mullvad-browser.sh113
-rw-r--r--ci/mullvad-browser/download-mullvad-browser.timer10
-rw-r--r--ci/mullvad-browser/tor-browser-developers-signing-key.asc283
-rwxr-xr-xci/prepare-rpm-repository.sh46
-rwxr-xr-xci/publish-app-to-repositories.sh121
-rwxr-xr-xci/publish-linux-repositories.sh128
15 files changed, 950 insertions, 227 deletions
diff --git a/ci/buildserver-build.sh b/ci/buildserver-build.sh
index a730ad100d..9e8b1eb04f 100755
--- a/ci/buildserver-build.sh
+++ b/ci/buildserver-build.sh
@@ -16,6 +16,9 @@ shopt -s nullglob
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BUILD_DIR="$SCRIPT_DIR/mullvadvpn-app"
+# All non-dev builds have their artifacts placed under this directory
+ARTIFACT_DIR="$SCRIPT_DIR/artifacts"
+# Keeps track of which git commit hashes has been built, to not build them again
LAST_BUILT_DIR="$SCRIPT_DIR/last-built"
BRANCHES_TO_BUILD=("origin/main")
@@ -44,27 +47,19 @@ case "$(uname -s)" in
;;
esac
+
+# Automatically copy artifacts to the inbox of the repository builder service
+# for dev and staging (production is pushed manually)
function publish_linux_repositories {
local artifact_dir=$1
local version=$2
- local deb_repo_dir="$SCRIPT_DIR/deb/$version"
- echo "Preparing Apt repository in $deb_repo_dir"
- "$SCRIPT_DIR/prepare-apt-repository.sh" "$artifact_dir" "$version" "$deb_repo_dir"
-
- local rpm_repo_dir="$SCRIPT_DIR/rpm/$version"
- echo "Preparing RPM repository in $rpm_repo_dir"
- "$SCRIPT_DIR/prepare-rpm-repository.sh" "$artifact_dir" "$version" "$rpm_repo_dir"
+ "$SCRIPT_DIR/publish-app-to-repositories.sh" --dev "$artifact_dir" "$version"
- "$SCRIPT_DIR/publish-linux-repositories.sh" --dev "$version" \
- --deb "$deb_repo_dir" \
- --rpm "$rpm_repo_dir"
# If this is a release build, also push to staging.
# Publishing to production is done manually.
if [[ $version != *"-dev-"* ]]; then
- "$SCRIPT_DIR/publish-linux-repositories.sh" --staging "$version" \
- --deb "$deb_repo_dir" \
- --rpm "$rpm_repo_dir"
+ "$SCRIPT_DIR/publish-app-to-repositories.sh" --staging "$artifact_dir" "$version"
fi
}
@@ -86,9 +81,9 @@ function upload {
sha256sum "${files[@]}" > "$checksums_path"
case "$(uname -s)" in
- # Linux is both the build and upload server. Just move directly to target dir
+ # Linux is both the build and upload server. Just copy directly to target dir
Linux*)
- mv "${files[@]}" "$checksums_path" "$UPLOAD_DIR/"
+ cp "${files[@]}" "$checksums_path" "$UPLOAD_DIR/"
;;
# Other platforms need to transfer their artifacts to the Linux build machine.
Darwin*|MINGW*|MSYS_NT*)
@@ -192,7 +187,7 @@ function build_ref {
local version=""
version="$(run_in_build_env cargo run -q --bin mullvad-version | tr -d "\r" || return 1)"
- local artifact_dir="dist/$version"
+ local artifact_dir="$ARTIFACT_DIR/$version"
mkdir -p "$artifact_dir"
local build_args=(--optimize --sign)
@@ -240,8 +235,11 @@ function build_ref {
publish_linux_repositories "$artifact_dir" "$version"
fi
(cd "$artifact_dir" && upload "$version") || return 1
- # shellcheck disable=SC2216
- yes | rm -r "$artifact_dir"
+ # Remove artifacts from dev builds. They are not really needed and take up lots of space.
+ if [[ $version == *"-dev-"* ]]; then
+ # shellcheck disable=SC2216
+ yes | rm -r "$artifact_dir"
+ fi
touch "$LAST_BUILT_DIR/$current_hash"
diff --git a/ci/buildserver-config.sh b/ci/buildserver-config.sh
index 4420a11300..00f770dbeb 100644
--- a/ci/buildserver-config.sh
+++ b/ci/buildserver-config.sh
@@ -3,29 +3,13 @@
# Buildserver configuration. Runtime values are defined here instead of
# the scripts where they are used.
-# Which gpg key to sign things with
-export CODE_SIGNING_KEY_FINGERPRINT="A1198702FC3E0A09A9AE5B75D5A1D4F266DE8DDF"
-
-# Debian codenames we support.
-SUPPORTED_DEB_CODENAMES=("sid" "testing" "bookworm" "bullseye")
-# Ubuntu codenames we support (latest two LTS) ...
-SUPPORTED_DEB_CODENAMES+=("noble" "jammy" "focal")
-# ... + latest non-LTS Ubuntu. We officially only support the latest non-LTS.
-# But to not cause too much disturbance just when Ubuntu is released, we keep
-# the last two codenames working in the repository.
-SUPPORTED_DEB_CODENAMES+=("mantic")
-export SUPPORTED_DEB_CODENAMES
-
-export SUPPORTED_RPM_ARCHITECTURES=("x86_64" "aarch64")
-
-# Servers to upload Linux deb/rpm repositories and all other build artifacts to.
-export DEV_UPLOAD_SERVERS=("cdn.devmole.eu")
-export STAGING_UPLOAD_SERVERS=("cdn.stagemole.eu")
+# Servers to upload build artifacts to.
export PRODUCTION_UPLOAD_SERVERS=("cdn.mullvad.net")
-export DEV_LINUX_REPOSITORY_PUBLIC_URL="https://repository.devmole.eu"
-export STAGING_LINUX_REPOSITORY_PUBLIC_URL="https://repository.stagemole.eu"
-export PRODUCTION_LINUX_REPOSITORY_PUBLIC_URL="https://repository.mullvad.net"
+# Where to publish new app artifact notification files to, so that
+# build-linux-repositories picks it up.
+# Keep in sync with build-linux-repositories-config.sh
+#export LINUX_REPOSITORY_INBOX_DIR_BASE="PLEASE CONFIGURE ME"
# What container volumes cargo should put caches in.
# Specify differently if running multiple builds in parallel on one machine,
diff --git a/ci/linux-repository-builder/build-linux-repositories-config.sh b/ci/linux-repository-builder/build-linux-repositories-config.sh
new file mode 100644
index 0000000000..bed015f5c0
--- /dev/null
+++ b/ci/linux-repository-builder/build-linux-repositories-config.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+# The directory to use as inbox. This is where .src files are read
+#export LINUX_REPOSITORY_INBOX_DIR_BASE="PLEASE CONFIGURE ME"
+
+# GPG key to sign the repositories with
+export CODE_SIGNING_KEY_FINGERPRINT="A1198702FC3E0A09A9AE5B75D5A1D4F266DE8DDF"
+
+# Debian codenames we support.
+SUPPORTED_DEB_CODENAMES=("sid" "testing" "bookworm" "bullseye")
+# Ubuntu codenames we support. Latest two LTS. But when adding a new
+# don't immediately remove the oldest one. Allow for some transition period
+# with the last three.
+SUPPORTED_DEB_CODENAMES+=("noble" "jammy" "focal")
+# ... + latest non-LTS Ubuntu. We officially only support the latest non-LTS.
+# But to not cause too much disturbance just when Ubuntu is released, we keep
+# the last two codenames working in the repository.
+SUPPORTED_DEB_CODENAMES+=("mantic")
+export SUPPORTED_DEB_CODENAMES
+
+export SUPPORTED_RPM_ARCHITECTURES=("x86_64" "aarch64")
+
+export REPOSITORIES=("stable" "beta")
+
+export PRODUCTION_LINUX_REPOSITORY_PUBLIC_URL="https://repository.mullvad.net"
+export STAGING_LINUX_REPOSITORY_PUBLIC_URL="https://repository.stagemole.eu"
+export DEV_LINUX_REPOSITORY_PUBLIC_URL="https://repository.devmole.eu"
+
+# Servers to upload Linux deb/rpm repositories to
+export PRODUCTION_REPOSITORY_SERVER="cdn.mullvad.net"
+export STAGING_REPOSITORY_SERVER="cdn.stagemole.eu"
+export DEV_REPOSITORY_SERVER="cdn.devmole.eu"
diff --git a/ci/linux-repository-builder/build-linux-repositories.sh b/ci/linux-repository-builder/build-linux-repositories.sh
new file mode 100755
index 0000000000..418183485b
--- /dev/null
+++ b/ci/linux-repository-builder/build-linux-repositories.sh
@@ -0,0 +1,182 @@
+#!/usr/bin/env bash
+#
+# Builds linux deb and rpm repositories and uploads them to a repository server.
+# One instance of this script targets *one* server environment.
+# This means that if you want to publish to development, staging and production servers,
+# you need to run three instances of this script.
+#
+# This script works on an $inbox_dir. In this directory it expects one directory per repository.
+# For each repository it will read all files having the .src extension
+# These files are expected to contain a single line, a path to some directory where
+# it can read new artifacts for that product.
+# All deb and rpm files from that directory will be signed and moved over to a folder with
+# the same name as the .src file, but with a .latest extension instead.
+# So artifacts read from `app.src` will be moved to `app.latest/`.
+#
+# Then the deb and rpm repositories will be generated and all deb and rpm files in
+# $inbox_dir/$repository/*.latest/ will be added to the repository. These repositories are then synced
+# to `$repository_server_upload_domain respectively.
+
+set -eu
+# nullglob is needed to produce expected results when globing an empty directory
+shopt -s nullglob
+
+function usage() {
+ echo "Usage: $0 <environment>"
+ echo
+ echo "Example usage: ./build-linux-repositories.sh production"
+ echo
+ echo "This script reads an inbox folder for the corresponding server environment."
+ echo "It then generates and uploads new Linux repositories for all"
+ echo "the latest artifacts"
+ echo
+ echo "Options:"
+ echo " -h | --help Show this help message and exit."
+ exit 1
+}
+
+if [[ "$#" == 0 || $1 == "-h" || $1 == "--help" ]]; then
+ usage
+fi
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+# shellcheck source=ci/linux-repository-builder/build-linux-repositories-config.sh
+source "$SCRIPT_DIR/build-linux-repositories-config.sh"
+
+environment="$1"
+case "$environment" in
+ "production")
+ repository_server_upload_domain="$PRODUCTION_REPOSITORY_SERVER"
+ repository_server_public_url="$PRODUCTION_LINUX_REPOSITORY_PUBLIC_URL"
+ ;;
+ "staging")
+ repository_server_upload_domain="$STAGING_REPOSITORY_SERVER"
+ repository_server_public_url="$STAGING_LINUX_REPOSITORY_PUBLIC_URL"
+ ;;
+ "dev")
+ repository_server_upload_domain="$DEV_REPOSITORY_SERVER"
+ repository_server_public_url="$DEV_LINUX_REPOSITORY_PUBLIC_URL"
+ ;;
+ *)
+ echo "Unknown environment. Specify production, staging or dev" >&2
+ exit 1
+ ;;
+esac
+
+inbox_dir="$LINUX_REPOSITORY_INBOX_DIR_BASE/$environment"
+
+if [[ ! -d "$inbox_dir" ]]; then
+ echo "Inbox $inbox_dir does not exist" 1>&2
+ exit 1
+fi
+
+# Process all .src files in the given inbox dir.
+# Signs and moves all found artifacts into .latest directories
+# Returns 0 if everything went well and there are new artifacts for a product.
+# Returns 1 if no new artifacts were found
+function process_inbox {
+ local inbox_dir=$1
+ echo "[#] Processing inbox at $inbox_dir"
+
+ local found_new_artifacts="false"
+ # Read all notify files and move the artifacts they point to into a local .latest copy
+ for notify_file in "$inbox_dir"/*.src; do
+ if [[ ! -f "$notify_file" ]]; then
+ echo "Ignoring non-file $notify_file" 1>&2
+ continue
+ fi
+ echo "Processing notify file $notify_file"
+
+ local src_dir
+ src_dir=$(cat "$notify_file")
+ if [[ ! -d "$src_dir" ]]; then
+ echo "Artifact source dir $src_dir from notify file $notify_file does not exist" 1>&2
+ continue
+ fi
+
+ # Removing the file before we move the artifacts out of where it points.
+ # This ensures we don't get stuck in a loop processing it over and over
+ # if the signing and moving fails.
+ rm "$notify_file"
+
+ local artifact_dir=${notify_file/%.src/.latest}
+ # Recreate the artifact dir, cleaning it
+ rm -rf "$artifact_dir" && mkdir -p "$artifact_dir" || exit 1
+
+ # The fact that we have processed one .src file is enough tro trigger a repository
+ # rebuild. Because if a product would like to publish "no artifacts" they should
+ # be able to create a .src file pointing to an empty directory
+ found_new_artifacts="true"
+
+ echo "Moving artifacts from $src_dir to $artifact_dir"
+ # Move all deb and rpm files into the .latest dir
+ for src_deb in "$src_dir"/*.deb; do
+ echo "Signing and moving $src_deb into $artifact_dir/"
+ dpkg-sig --sign builder "$src_deb"
+ mv "$src_deb" "$artifact_dir/"
+ done
+ for src_rpm in "$src_dir"/*.rpm; do
+ echo "Signing and moving $src_rpm into $artifact_dir/"
+ rpm --addsign "$src_rpm"
+ mv "$src_rpm" "$artifact_dir/"
+ done
+ rm -r "$src_dir" || echo "Failed to remove src dir $src_dir"
+ done
+
+ if [[ $found_new_artifacts == "false" ]]; then
+ return 1
+ fi
+ return 0
+}
+
+function rsync_repo {
+ local local_repo_dir=$1
+ local remote_repo_dir=$2
+
+ echo "Syncing to $repository_server_upload_domain:$remote_repo_dir"
+ rsync -av --delete --mkpath --rsh='ssh -p 1122' \
+ "$local_repo_dir"/ \
+ build@"$repository_server_upload_domain":"$remote_repo_dir"
+}
+
+for repository in "${REPOSITORIES[@]}"; do
+ deb_remote_repo_dir="deb/$repository"
+ rpm_remote_repo_dir="rpm/$repository"
+
+ repository_inbox_dir="$inbox_dir/$repository"
+ if ! process_inbox "$repository_inbox_dir"; then
+ echo "Nothing new in inbox at $repository_inbox_dir"
+ continue
+ fi
+
+ # Read all .latest artifact dirs into array
+ readarray -d '' artifact_dirs < <(find "$repository_inbox_dir" -maxdepth 1 -name "*.latest" -type d -print0)
+ if [ "${#artifact_dirs[@]}" -lt 1 ]; then
+ echo "No artifact directories in $repository_inbox_dir to generate repositories from" >&2
+ continue
+ fi
+
+ echo "Generating repositories from these artifact directories: ${artifact_dirs[*]}"
+
+ # Generate deb repository from all the .latest artifacts
+
+ deb_repo_dir="$repository_inbox_dir/repos/deb"
+ rm -rf "$deb_repo_dir" && mkdir -p "$deb_repo_dir" || exit 1
+ "$SCRIPT_DIR/prepare-apt-repository.sh" "$deb_repo_dir" "${artifact_dirs[@]}"
+
+ # Generate rpm repository from all the .latest artifacts
+
+ rpm_repo_dir="$repository_inbox_dir/repos/rpm"
+ rm -rf "$rpm_repo_dir" && mkdir -p "$rpm_repo_dir" || exit 1
+ "$SCRIPT_DIR/prepare-rpm-repository.sh" "$rpm_repo_dir" \
+ "$repository_server_public_url" "$rpm_remote_repo_dir" "$repository" "${artifact_dirs[@]}"
+
+ # rsync repositories to repository server
+
+ echo "[#] Syncing deb repository to $deb_remote_repo_dir"
+ rsync_repo "$deb_repo_dir" "$deb_remote_repo_dir"
+ echo "[#] Syncing rpm repository to $rpm_remote_repo_dir"
+ rsync_repo "$rpm_repo_dir" "$rpm_remote_repo_dir"
+
+done
diff --git a/ci/linux-repository-builder/build-linux-repositories@.service b/ci/linux-repository-builder/build-linux-repositories@.service
new file mode 100644
index 0000000000..e4b8d78a23
--- /dev/null
+++ b/ci/linux-repository-builder/build-linux-repositories@.service
@@ -0,0 +1,24 @@
+# Create separate services with instance names being set to "$server_environment".
+# See the build-linux-repositories.sh script for details.
+#
+# For example:
+# * build-linux-repositories@production.service
+# * build-linux-repositories@staging.service
+# * build-linux-repositories@dev.service
+#
+# To install this service, do the following:
+#
+# * Log in to the user with `sudo machinectl shell <build account>@` to get a proper shell
+# * Copy this service file and corresponding timer file to ~/.config/systemd/user
+# * Edit `ExecStart` path to point to absolute path to script
+# * Reload systemd: `systemctl --user daemon-reload`
+# * Start the timers:
+# $ systemctl --user enable --now build-linux-repositories@production.timer
+# ... Repeat for other environments.
+
+[Unit]
+Description=Mullvad Linux repository generation and upload service
+
+[Service]
+Type=oneshot
+ExecStart=./build-linux-repositories.sh %i
diff --git a/ci/linux-repository-builder/build-linux-repositories@.timer b/ci/linux-repository-builder/build-linux-repositories@.timer
new file mode 100644
index 0000000000..b4f4aaa202
--- /dev/null
+++ b/ci/linux-repository-builder/build-linux-repositories@.timer
@@ -0,0 +1,9 @@
+[Unit]
+Description=Run Mullvad Linux repository generation and upload service periodically
+
+[Timer]
+OnCalendar=*-*-* *:*:30
+Persistent=true
+
+[Install]
+WantedBy=timers.target
diff --git a/ci/prepare-apt-repository.sh b/ci/linux-repository-builder/prepare-apt-repository.sh
index 5f4eee9746..18a2d5bdf9 100755
--- a/ci/prepare-apt-repository.sh
+++ b/ci/linux-repository-builder/prepare-apt-repository.sh
@@ -1,20 +1,40 @@
#!/usr/bin/env bash
-#
-# Usage: ./prepare-apt-repository.sh <artifact dir> <app version> <repository dir>
-#
-# Will create a deb repository in <repository dir> and add all .deb files from
-# <artifact dir> matching <app version> to the repository.
set -eu
+# nullglob is needed to produce expected results when globing an empty directory
+shopt -s nullglob
+
+function usage() {
+ echo "Usage: $0 <repository dir> <artifact dirs...>"
+ echo
+ echo "Will create a deb repository in <repository dir> and add all .deb files from all <artifact dirs>"
+ echo
+ echo "Options:"
+ echo " -h | --help Show this help message and exit."
+ exit 1
+}
+
+if [[ "$#" == 0 || $1 == "-h" || $1 == "--help" ]]; then
+ usage
+fi
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-# shellcheck source=ci/buildserver-config.sh
-source "$SCRIPT_DIR/buildserver-config.sh"
+# shellcheck source=ci/linux-repository-builder/build-linux-repositories-config.sh
+source "$SCRIPT_DIR/build-linux-repositories-config.sh"
-artifact_dir=$1
-version=$2
-repo_dir=$3
+repo_dir=${1:?"Specify the output repository directory as the first argument"}
+
+artifact_dirs=()
+while [ "$#" -gt 1 ]; do
+ artifact_dirs+=("$2")
+ shift
+done
+
+if [ "${#artifact_dirs[@]}" -lt 1 ]; then
+ echo "No artifact directories given" >&2
+ exit 1
+fi
function generate_repository_configuration {
local codename=$1
@@ -51,10 +71,12 @@ echo "Writing repository configuration to $repo_dir/conf/distributions"
generate_deb_distributions_content > "$repo_dir/conf/distributions"
echo ""
-for deb_path in "$artifact_dir"/MullvadVPN-"$version"*.deb; do
- for codename in "${SUPPORTED_DEB_CODENAMES[@]}"; do
- add_deb_to_repo "$deb_path" "$codename"
+for artifact_dir in "${artifact_dirs[@]}"; do
+ for deb_path in "$artifact_dir"/*.deb; do
+ for codename in "${SUPPORTED_DEB_CODENAMES[@]}"; do
+ add_deb_to_repo "$deb_path" "$codename"
+ echo ""
+ done
echo ""
done
- echo ""
done
diff --git a/ci/linux-repository-builder/prepare-rpm-repository.sh b/ci/linux-repository-builder/prepare-rpm-repository.sh
new file mode 100755
index 0000000000..4e258dedb4
--- /dev/null
+++ b/ci/linux-repository-builder/prepare-rpm-repository.sh
@@ -0,0 +1,102 @@
+#!/usr/bin/env bash
+
+set -eu
+# nullglob is needed to produce expected results when globing an empty directory
+shopt -s nullglob
+
+function usage() {
+ echo "Usage: $0 <repository dir> <repository server url> <remote repo dir> <stable_or_beta> <artifact dirs...>"
+ echo
+ echo "Example: $0 ./repos/rpm https://cdn.mullvad.net/ rpm/stable stable inbox/app.latest inbox/browser.latest"
+ echo
+ echo "Will create an rpm repository in <repository dir> and add all .rpm files from all <artifact dirs> to it."
+ echo
+ echo "Options:"
+ echo " -h | --help Show this help message and exit."
+ exit 1
+}
+
+if [[ "$#" == 0 || $1 == "-h" || $1 == "--help" ]]; then
+ usage
+fi
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+# shellcheck source=ci/linux-repository-builder/build-linux-repositories-config.sh
+source "$SCRIPT_DIR/build-linux-repositories-config.sh"
+
+repo_dir=${1:?"Specify the output repository directory as the first argument"}
+repository_server_url=${2:?"Give repository server URL as second argument (needed for repository metadata generation)"}
+remote_repo_dir=${3:?"Give remote repo dir as third argument"}
+stable_or_beta=${4:?"Give 'stable' or 'beta' as fourth argument"}
+shift 4
+
+artifact_dirs=()
+while [ "$#" -gt 0 ]; do
+ artifact_dirs+=("$1")
+ shift
+done
+
+if [[ "$stable_or_beta" != "beta" && "$stable_or_beta" != "stable" ]]; then
+ echo "<stable or beta> argument must be 'stable' or 'beta'"
+ exit 1
+fi
+if [ "${#artifact_dirs[@]}" -lt 1 ]; then
+ echo "No artifact directories given" >&2
+ exit 1
+fi
+
+# Writes the mullvad.repo config file to the repository root.
+# This needs to contain the absolute url and path to the repository.
+# As such, it depends on what server we upload to as well as if it's stable or beta.
+function generate_rpm_repository_configuration {
+ local repository_dir=$1
+ local repository_server_url=$2
+ local remote_repo_dir=$3
+ local stable_or_beta=$4
+
+ local repository_name="Mullvad VPN"
+ if [[ "$stable_or_beta" == "beta" ]]; then
+ repository_name+=" (beta)"
+ fi
+
+ echo -e "[mullvad-$stable_or_beta]
+name=$repository_name
+baseurl=$repository_server_url/$remote_repo_dir/\$basearch
+type=rpm
+enabled=1
+gpgcheck=1
+gpgkey=$repository_server_url/rpm/mullvad-keyring.asc" > "$repository_dir/mullvad.repo"
+}
+
+for artifact_dir in "${artifact_dirs[@]}"; do
+ for arch in "${SUPPORTED_RPM_ARCHITECTURES[@]}"; do
+ arch_repo_dir="$repo_dir/$arch"
+ for rpm_path in "$artifact_dir"/*"$arch".rpm; do
+ mkdir -p "$arch_repo_dir"
+ echo "[#] Copying $rpm_path to $arch_repo_dir/"
+ cp "$rpm_path" "$arch_repo_dir"/
+ done
+ done
+done
+
+for arch in "${SUPPORTED_RPM_ARCHITECTURES[@]}"; do
+ arch_repo_dir="$repo_dir/$arch"
+ if [[ ! -d "$arch_repo_dir" ]]; then
+ echo "No $arch repository in $repo_dir" >&2
+ continue
+ fi
+
+ echo "Generating architecture repository metadata for $arch_repo_dir"
+
+ # Generate repository metadata files (containing among other things checksums
+ # for the rpm files in it)
+ createrepo_c "$arch_repo_dir"
+
+ # Sign repository metadata (created by createrepo_c above)
+ gpg --detach-sign --armor "$arch_repo_dir/repodata/repomd.xml"
+done
+
+echo "Generating global repository configuration in $repo_dir"
+generate_rpm_repository_configuration "$repo_dir" "$repository_server_url" "$remote_repo_dir" "$stable_or_beta"
+
diff --git a/ci/mullvad-browser/download-mullvad-browser.service b/ci/mullvad-browser/download-mullvad-browser.service
new file mode 100644
index 0000000000..3b6b8a9b98
--- /dev/null
+++ b/ci/mullvad-browser/download-mullvad-browser.service
@@ -0,0 +1,17 @@
+# To install this do the following:
+#
+# * Create a directory for the browser GPG keyring (used to verify downloaded artifacts)
+# * Import the key there: `GNUPGHOME=/the/gpg/home gpg --import ./tor-browser-developers-signing-key.asc`
+# * Copy this service file and corresponding timer file to ~/.config/systemd/user
+# * Edit `Environment` below to point `GNUPGHOME` to the path you created above
+# * Edit `ExecStart` path to point to absolute path to download script
+# * Reload systemd: `systemctl --user daemon-reload`
+# * Start the timer: `systemctl --user enable --now download-mullvad-browser.timer`
+
+[Unit]
+Description=Mullvad Browser download service
+
+[Service]
+Type=oneshot
+Environment="GNUPGHOME=..."
+ExecStart=./download-mullvad-browser.sh
diff --git a/ci/mullvad-browser/download-mullvad-browser.sh b/ci/mullvad-browser/download-mullvad-browser.sh
new file mode 100755
index 0000000000..5c09ac21cf
--- /dev/null
+++ b/ci/mullvad-browser/download-mullvad-browser.sh
@@ -0,0 +1,113 @@
+#!/usr/bin/bash -e
+
+set -eu
+
+# TODO: Uncomment when alpha is to be released
+# BROWSER_RELEASES=("alpha" "stable")
+BROWSER_RELEASES=("stable")
+REPOSITORIES=("stable" "beta")
+TMP_DIR=$(mktemp -qdt mullvad-browser-tmp-XXXXXXX)
+WORKDIR=/tmp/mullvad-browser-download
+NOTIFY_DIR=/tmp/linux-repositories/production
+
+
+function usage() {
+ echo "Usage: $0"
+ echo
+ echo "This script downloads, verifies, and notifies about Mullvad browser packages."
+ echo
+ echo "Options:"
+ echo " -h | --help Show this help message and exit."
+ exit 1
+}
+
+
+function main() {
+ local package_filename_base=$1
+ local extension=$2
+ PACKAGE_FILENAME="$package_filename_base.$extension"
+
+ PACKAGE_URL=https://cdn.mullvad.net/browser/$PACKAGE_FILENAME
+ SIGNATURE_URL=$PACKAGE_URL.asc
+
+ echo "[#] Downloading $PACKAGE_FILENAME"
+ if ! wget --quiet --show-progress "$PACKAGE_URL"; then
+ echo "[!] Failed to download $PACKAGE_URL"
+ exit 1
+ fi
+
+ echo "[#] Downloading $PACKAGE_FILENAME.asc"
+ if ! wget --quiet --show-progress "$SIGNATURE_URL"; then
+ echo "[!] Failed to download $SIGNATURE_URL"
+ exit 1
+ fi
+
+ echo "[#] Verifying $PACKAGE_FILENAME signature"
+ if ! gpg --verify "$PACKAGE_FILENAME".asc; then
+ echo "[!] Failed to verify signature"
+ exit 1
+ fi
+ rm "$PACKAGE_FILENAME.asc"
+
+ # Hack to get the architecture into the filename
+ local filename_with_arch="${package_filename_base}_x86_64.$extension"
+ mv "$PACKAGE_FILENAME" "$filename_with_arch"
+ PACKAGE_FILENAME="$filename_with_arch"
+
+ # Check if the deb package has changed since last time
+ # Handle the bootstrap problem by checking if the "output file" even exists and just moving on if it doesn't
+ if [[ -f "$WORKDIR/$PACKAGE_FILENAME" ]] && cmp "$PACKAGE_FILENAME" "$WORKDIR/$PACKAGE_FILENAME"; then
+ echo "[#] $PACKAGE_FILENAME has not changed"
+ rm "$PACKAGE_FILENAME"
+ return
+ fi
+
+ echo "[#] $PACKAGE_FILENAME has changed"
+ ln "$PACKAGE_FILENAME" $WORKDIR/
+}
+
+if [[ ${1:-} == "-h" ]] || [[ ${1:-} == "--help" ]]; then
+ usage
+fi
+
+
+if ! [[ -d $NOTIFY_DIR ]]; then
+ echo "[!] $NOTIFY_DIR does not exist"
+ exit 1
+fi
+
+
+if ! [[ -d $WORKDIR ]]; then
+ echo "[#] Creating $WORKDIR"
+ mkdir -p $WORKDIR
+fi
+
+
+pushd "$TMP_DIR" > /dev/null
+
+
+echo "[#] Configured releases are: ${BROWSER_RELEASES[*]}"
+for release in "${BROWSER_RELEASES[@]}"; do
+ main "mullvad-browser-$release" "deb"
+ main "mullvad-browser-$release" "rpm"
+done
+
+if [[ -z "$(ls -A "$TMP_DIR")" ]]; then
+ echo "[#] No new browser build(s) exist"
+ exit
+fi
+
+echo "[#] New browser build(s) exist"
+for repository in "${REPOSITORIES[@]}"; do
+ inbox_dir="$NOTIFY_DIR/$repository"
+
+ REPOSITORY_TMP_ARTIFACT_DIR=$(mktemp -qdt mullvad-browser-tmp-XXXXXXX)
+ cp "$TMP_DIR"/* "$REPOSITORY_TMP_ARTIFACT_DIR"
+
+ repository_notify_file="$inbox_dir/browser.src"
+ echo "[#] Notifying $repository_notify_file"
+ echo "$REPOSITORY_TMP_ARTIFACT_DIR" > "$repository_notify_file"
+done
+
+# Remove our temporary working directory
+rm -r "$TMP_DIR"
diff --git a/ci/mullvad-browser/download-mullvad-browser.timer b/ci/mullvad-browser/download-mullvad-browser.timer
new file mode 100644
index 0000000000..699192eeae
--- /dev/null
+++ b/ci/mullvad-browser/download-mullvad-browser.timer
@@ -0,0 +1,10 @@
+[Unit]
+Description=Run Mullvad Browser download service periodically
+
+[Timer]
+# Run every fifteen minutes
+OnCalendar=*:0,15,30,45
+Persistent=true
+
+[Install]
+WantedBy=timers.target
diff --git a/ci/mullvad-browser/tor-browser-developers-signing-key.asc b/ci/mullvad-browser/tor-browser-developers-signing-key.asc
new file mode 100644
index 0000000000..2994a84ad1
--- /dev/null
+++ b/ci/mullvad-browser/tor-browser-developers-signing-key.asc
@@ -0,0 +1,283 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+=QHmJ
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/ci/prepare-rpm-repository.sh b/ci/prepare-rpm-repository.sh
deleted file mode 100755
index 22616c591f..0000000000
--- a/ci/prepare-rpm-repository.sh
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env bash
-#
-# Usage: ./prepare-rpm-repository.sh <artifact dir> <app version> <repository dir>
-#
-# Will create an rpm repository in <repository dir> and add all .rpm files from
-# <artifact dir> matching <app version> to the repository.
-
-set -eu
-
-SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-
-# shellcheck source=ci/buildserver-config.sh
-source "$SCRIPT_DIR/buildserver-config.sh"
-
-artifact_dir=$1
-version=$2
-repo_dir=$3
-
-function create_repository {
- local arch_repo_dir=$1
- local rpm_path=$2
-
- mkdir -p "$arch_repo_dir"
-
- # Copy RPM file into repository
- cp "$rpm_path" "$arch_repo_dir"/
-
- # Generate repository metadata files (containing among other things checksums
- # for the above artifact)
- createrepo_c "$arch_repo_dir"
-
- # Sign repository metadata (created by createrepo_c above)
- # --yes is passed to automatically overwrite existing files
- # in the case where the build server re-builds something we already
- # have built.
- gpg --detach-sign --armor --yes "$arch_repo_dir/repodata/repomd.xml"
-}
-
-for arch in "${SUPPORTED_RPM_ARCHITECTURES[@]}"; do
- rpm_path="$artifact_dir"/MullvadVPN-"$version"_"$arch".rpm
- if [[ ! -e "$rpm_path" ]]; then
- echo "RPM at $rpm_path does not exist" >&2
- exit 1
- fi
- create_repository "$repo_dir/$arch" "$rpm_path"
-done
diff --git a/ci/publish-app-to-repositories.sh b/ci/publish-app-to-repositories.sh
new file mode 100755
index 0000000000..0a05e48a21
--- /dev/null
+++ b/ci/publish-app-to-repositories.sh
@@ -0,0 +1,121 @@
+#!/usr/bin/env bash
+
+set -eu
+# nullglob is needed to produce expected results when globing an empty directory
+shopt -s nullglob
+
+function usage() {
+ echo "Usage: $0 [--production/--staging/--dev] <artifact dir> <app version>"
+ echo
+ echo "Copies app deb and rpm artifacts over to the repository building service inbox directory."
+ echo "Makes that service publish the new artifacts to the corresponding repository server."
+ echo
+ echo "Options:"
+ echo " -h | --help Show this help message and exit."
+ echo " --production Publish app to production environment"
+ echo " --staging Publish app to staging environment"
+ echo " --dev Publish app to development environment"
+ echo ""
+ echo "Arguments:"
+ echo " <artifact dir> Directory to copy from. Will copy all app deb/rpms matching <version>"
+ echo " <version> App version to copy. If <artifact dir> has apps for multiple versions"
+ echo " only apps matching this version will be copied"
+ exit 1
+}
+
+if [[ "$#" == 0 || $1 == "-h" || $1 == "--help" ]]; then
+ usage
+fi
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+# shellcheck source=ci/buildserver-config.sh
+source "$SCRIPT_DIR/buildserver-config.sh"
+
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ "--production")
+ environment="production"
+ ;;
+ "--staging")
+ environment="staging"
+ ;;
+ "--dev")
+ environment="dev"
+ ;;
+ -*)
+ echo "Unknown option \"$1\"" >&2
+ exit 1
+ ;;
+ *)
+ if [[ -z ${artifact_dir+x} ]]; then
+ artifact_dir=$1
+ elif [[ -z ${version+x} ]]; then
+ version=$1
+ else
+ echo "Too many arguments" >&2
+ exit 1
+ fi
+ ;;
+ esac
+ shift
+done
+
+if [[ -z ${environment+x} ]]; then
+ echo "Pass either --dev, --staging or --production to select target servers" >&2
+ exit 1
+fi
+if [[ -z ${artifact_dir+x} ]]; then
+ echo "Please give the artifact directory as an argument to this script" >&2
+ exit 1
+fi
+if [[ -z ${version+x} ]]; then
+ echo "Please give the release version as an argument to this script" >&2
+ exit 1
+fi
+
+function copy_linux_artifacts_to_dir {
+ local src_dir=$1
+ local version=$2
+ local dst_dir=$3
+
+ for deb_path in "$src_dir"/MullvadVPN-"$version"*.deb; do
+ echo "Copying $deb_path into $dst_dir/"
+ cp "$deb_path" "$dst_dir/"
+ done
+ for rpm_path in "$src_dir"/MullvadVPN-"$version"*.rpm; do
+ echo "Copying $rpm_path into $dst_dir/"
+ cp "$rpm_path" "$dst_dir/"
+ done
+}
+
+function notify_repository_service {
+ local artifact_dir=$1
+ local version=$2
+ local repository_inbox_dir=$3
+
+ local tmp_notify_file
+ tmp_notify_file=$(mktemp -p "$repository_inbox_dir")
+ local notify_file="$repository_inbox_dir/app.src"
+
+ # Temporarily write the file to a different path and then move it.
+ # As long as the tmp dir and destination dir is on the same filesystem,
+ # this is guaranteed to be atomic, preventing partial reads by the consuming
+ # repository building service.
+ echo "$artifact_dir" > "$tmp_notify_file"
+ echo "Writing notify file $notify_file"
+ mv "$tmp_notify_file" "$notify_file"
+}
+
+stable_or_beta="stable"
+if [[ $version == *"-beta"* ]]; then
+ stable_or_beta="beta"
+fi
+repository_inbox_dir="$LINUX_REPOSITORY_INBOX_DIR_BASE/$environment/$stable_or_beta"
+repository_tmp_store_dir="$(mktemp -qdt "mullvadvpn-app-$version-tmp-XXXXXXX")"
+
+echo "Copying app artifacts for $version from $artifact_dir to $repository_tmp_store_dir"
+copy_linux_artifacts_to_dir "$artifact_dir" "$version" "$repository_tmp_store_dir"
+
+echo "Notifying repository building service in $repository_inbox_dir"
+notify_repository_service "$repository_tmp_store_dir" "$version" "$repository_inbox_dir"
diff --git a/ci/publish-linux-repositories.sh b/ci/publish-linux-repositories.sh
deleted file mode 100755
index 76d09c33d9..0000000000
--- a/ci/publish-linux-repositories.sh
+++ /dev/null
@@ -1,128 +0,0 @@
-#!/usr/bin/env bash
-#
-# Usage: ./publish-linux-repositories.sh [--production/--staging/--dev] <app version> [--deb <deb repository dir>] [--rpm <rpm repository dir>]
-#
-# Rsyncs a locally prepared and stored apt and/or rpm repository to the dev/staging/production
-# repository servers.
-
-set -eu
-
-SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-
-# shellcheck source=ci/buildserver-config.sh
-source "$SCRIPT_DIR/buildserver-config.sh"
-
-while [ "$#" -gt 0 ]; do
- case "$1" in
- "--production")
- repository_servers=("${PRODUCTION_UPLOAD_SERVERS[@]}")
- repository_server_url="$PRODUCTION_LINUX_REPOSITORY_PUBLIC_URL"
- ;;
- "--staging")
- repository_servers=("${STAGING_UPLOAD_SERVERS[@]}")
- repository_server_url="$STAGING_LINUX_REPOSITORY_PUBLIC_URL"
- ;;
- "--dev")
- repository_servers=("${DEV_UPLOAD_SERVERS[@]}")
- repository_server_url="$DEV_LINUX_REPOSITORY_PUBLIC_URL"
- ;;
- "--deb")
- deb_repo_dir=$2
- shift
- ;;
- "--rpm")
- rpm_repo_dir=$2
- shift
- ;;
- -*)
- echo "Unknown option \"$1\"" >&2
- exit 1
- ;;
- *)
- if [[ -z ${version+x} ]]; then
- version=$1
- else
- echo "Too many arguments" >&2
- exit 1
- fi
- ;;
- esac
- shift
-done
-
-if [[ -z ${version+x} ]]; then
- echo "Please give the release version as an argument to this script" >&2
- exit 1
-fi
-if [[ -z ${deb_repo_dir+x} && -z ${rpm_repo_dir+x} ]]; then
- echo "Please specify at least one of --deb or --rpm" >&2
- exit 1
-fi
-if [[ -z ${repository_servers+x} ]]; then
- echo "Pass either --dev, --staging or --production to select target servers" >&2
- exit 1
-fi
-
-function rsync_repo {
- local local_repo_dir=$1
- local remote_repo_dir=$2
-
- for server in "${repository_servers[@]}"; do
- echo "Syncing to $server:$remote_repo_dir"
- rsync -av --delete --mkpath --rsh='ssh -p 1122' \
- "$local_repo_dir"/ \
- build@"$server":"$remote_repo_dir"
- done
-}
-
-# Writes the mullvad.repo config file to the repository
-# root. This needs to contain the absolute url and path
-# to the repository. As such, it depends on what server
-# we upload to as well as if it's stable or beta. That's
-# why we need to do it just before upload.
-function generate_rpm_repository_configuration {
- local repository_dir=$1
- local stable_or_beta=$2
-
- local repository_name="Mullvad VPN"
- if [[ "$stable_or_beta" == "beta" ]]; then
- repository_name+=" (beta)"
- fi
-
- echo -e "[mullvad-$stable_or_beta]
-name=$repository_name
-baseurl=$repository_server_url/rpm/$stable_or_beta/\$basearch
-type=rpm
-enabled=1
-gpgcheck=1
-gpgkey=$repository_server_url/rpm/mullvad-keyring.asc" > "$repository_dir/mullvad.repo"
-}
-
-if [[ -n ${deb_repo_dir+x} ]]; then
- echo "Publishing deb repository from $deb_repo_dir"
- if [[ ! -d "$deb_repo_dir" ]]; then
- echo "$deb_repo_dir does not exist" >&2
- exit 1
- fi
-
- rsync_repo "$deb_repo_dir" "deb/beta"
- if [[ $version != *"-beta"* ]]; then
- rsync_repo "$deb_repo_dir" "deb/stable"
- fi
-fi
-
-if [[ -n ${rpm_repo_dir+x} ]]; then
- echo "Publishing rpm repository from $rpm_repo_dir"
- if [[ ! -d "$rpm_repo_dir" ]]; then
- echo "$rpm_repo_dir does not exist" >&2
- exit 1
- fi
-
- generate_rpm_repository_configuration "$rpm_repo_dir" "beta"
- rsync_repo "$rpm_repo_dir" "rpm/beta"
- if [[ $version != *"-beta"* ]]; then
- generate_rpm_repository_configuration "$rpm_repo_dir" "stable"
- rsync_repo "$rpm_repo_dir" "rpm/stable"
- fi
-fi
-