From 2e2e968655d5c7fff27dfba1fc769a8ae0cda174 Mon Sep 17 00:00:00 2001
From: hda10527 <lukas.koenen@h-da.de>
Date: Sat, 10 Apr 2021 22:41:37 +0200
Subject: [PATCH] chore(pipeline): add manual task to create a new release

- add jobs to create installers

Closes #6
---
 .gitlab-ci.yml                      | 163 +++++++++++++++++++++++++++-
 .idea/openconnect-gui.iml           |   8 +-
 CMakeLists.txt                      |   5 +-
 README.md                           |  15 ---
 infra/scripts/addReleaseLink.js     |  71 ++++++++++++
 infra/scripts/downloadRelease.js    |  76 +++++++++++++
 infra/scripts/downloadTapAdapter.js |  55 ++++++++++
 release.config.js                   | 146 +++++++++++++++++++++++++
 8 files changed, 518 insertions(+), 21 deletions(-)
 create mode 100644 infra/scripts/addReleaseLink.js
 create mode 100644 infra/scripts/downloadRelease.js
 create mode 100644 infra/scripts/downloadTapAdapter.js
 create mode 100644 release.config.js

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3d94aa6..26a593d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,9 +1,20 @@
 stages:
-  - lint
+  - analyze
+  - release
+  - release:create_assets
+
+.node_packages_install: &node_packages_install
+  - npm install
+    yargs
+    @gitbeaker/node
+    semver
+    node-fetch
+    tar
+    node-7z
 
 .linters:
+  stage: analyze
   image: python:rc-alpine3.13
-  stage: lint
   before_script:
     - pip3 install cpplint
 
@@ -15,3 +26,151 @@ lint_application:
     paths:
       - coverage.xml
   needs: [ ]
+
+create_release:
+  stage: release
+  image: node:lts
+  rules:
+    - if: "$CI_COMMIT_BRANCH == 'master'"
+      when: manual
+      allow_failure: false
+  variables:
+    GIT_AUTHOR_NAME: project_12589_bot
+    GIT_AUTHOR_EMAIL: project12589_bot@example.com
+    GIT_COMMITTER_NAME: project_12589_bot
+    GIT_COMMITTER_EMAIL: project12589_bot@example.com
+  before_script:
+    - npm install
+      semantic-release
+      @semantic-release/gitlab
+      @semantic-release/git
+      conventional-changelog-conventionalcommits
+  script:
+    - npx semantic-release
+  # This job has explicitly no needs, because it should
+  # only run if everything has successfully been executed.
+
+create_rpm_file:
+  stage: release:create_assets
+  image: fedora:33
+  rules:
+    - if: "$CI_COMMIT_BRANCH == 'master'"
+  before_script:
+    - yum -y install
+      cmake
+      rpmdevtools
+      qt5-qtbase-devel
+      openconnect-devel
+      npm
+    - *node_packages_install
+    - useradd --create-home builder # create user -> discouraged to build packages as the root user
+    - su builder -c "rpmdev-setuptree"
+    - node $CI_PROJECT_DIR/infra/scripts/downloadRelease.js
+      --gitlab_host $CI_SERVER_URL
+      --gitlab_token $GITLAB_TOKEN
+      --project_id $CI_PROJECT_ID
+      --installer_type rpm
+      --output_path  /home/builder/rpmbuild/SOURCES
+      --extract_output_path /home/builder
+  script:
+    - su builder -c "rpmbuild -bb /home/builder/openconnect-hda/rpm/openconnect-hda.spec"
+    - node $CI_PROJECT_DIR/infra/scripts/addReleaseLink.js
+      --gitlab_host $CI_SERVER_URL
+      --gitlab_token $GITLAB_TOKEN
+      --project_id $CI_PROJECT_ID
+      --installer_type rpm
+      --asset_path  /home/builder/rpmbuild/RPMS/x86_64
+  needs:
+    - job: create_release
+
+create_deb_file:
+  stage: release:create_assets
+  image: ubuntu:20.04
+  rules:
+    - if: "$CI_COMMIT_BRANCH == 'master'"
+  before_script:
+    - apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y
+      pkgconf
+      wget
+      debhelper
+      devscripts
+      libopenconnect5
+      libopenconnect-dev
+      vpnc
+      qtbase5-dev
+      npm
+    - wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null
+      | gpg --dearmor -
+      | tee /etc/apt/trusted.gpg.d/kitware.gpg >/dev/null
+    - echo deb https://apt.kitware.com/ubuntu/ focal main >> /etc/apt/sources.list
+    - apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y cmake # get current cmake version
+    - *node_packages_install
+    - useradd --create-home builder # create user -> discouraged to build packages as the root user
+    - node $CI_PROJECT_DIR/infra/scripts/downloadRelease.js
+      --gitlab_host $CI_SERVER_URL
+      --gitlab_token $GITLAB_TOKEN
+      --project_id $CI_PROJECT_ID
+      --installer_type deb
+      --output_path /home/builder
+      --extract_output_path /home/builder
+  script:
+    - chown -R builder /home/builder/openconnect-hda && cd /home/builder/openconnect-hda/debian
+    - su builder -c "debuild -us -uc"
+    - node $CI_PROJECT_DIR/infra/scripts/addReleaseLink.js
+      --gitlab_host $CI_SERVER_URL
+      --gitlab_token $GITLAB_TOKEN
+      --project_id $CI_PROJECT_ID
+      --installer_type deb
+      --asset_path  /home/builder
+  needs:
+    - create_release
+
+create_msi_file:
+  stage: release:create_assets
+  rules:
+    - if: "$CI_COMMIT_BRANCH == 'master'"
+  variables:
+    ARCHIVE_URL: http://build.openvpn.net/downloads/releases/
+    TAP_ADAPTER_FILENAME: tap-windows-9.24.6-I601-amd64.msm
+    OPENVPN_FILENAME: OpenVPN-2.5.2-I601-amd64.msi
+  before_script:
+    - *node_packages_install
+    - node $CI_PROJECT_DIR/infra/scripts/downloadRelease.js
+      --gitlab_host $CI_SERVER_URL
+      --gitlab_token $GITLAB_TOKEN
+      --project_id $CI_PROJECT_ID
+      --installer_type msi
+    - mkdir $CI_PROJECT_DIR/openconnect-hda/cmake-build-release
+    - cd $CI_PROJECT_DIR/openconnect-hda/cmake-build-release
+    - cmake -DCMAKE_BUILD_TYPE=Release -G "CodeBlocks - Unix Makefiles" ..
+    - cd $CI_PROJECT_DIR/openconnect-hda
+    - cmake --build $CI_PROJECT_DIR/openconnect-hda/cmake-build-release --target openconnect-hda
+    - node $CI_PROJECT_DIR/infra/scripts/downloadTapAdapter.js
+      --archive_url $ARCHIVE_URL
+      --tap_adapter_filename $TAP_ADAPTER_FILENAME
+      --openvpn_filename $OPENVPN_FILENAME
+      --output_path $CI_PROJECT_DIR/openconnect-hda/windows/tap-adapter
+  script:
+    - cd $CI_PROJECT_DIR/openconnect-hda/windows/wix
+    - candle
+      -arch x64
+      -out build/
+      openconnect-hda.wxs
+    - light
+      -out build/openconnect-hda.msi
+      -pdbout build/openconnect-hda.wixpdb
+      -cultures:null
+      -ext "WixUIExtension.dll"
+      -wixprojectfile openconnect-hda.wixproj
+      build/openconnect-hda.wixobj
+    - node $CI_PROJECT_DIR/infra/scripts/addReleaseLink.js
+      --gitlab_host $CI_SERVER_URL
+      --gitlab_token $GITLAB_TOKEN
+      --project_id $CI_PROJECT_ID
+      --installer_type msi
+      --asset_path $CI_PROJECT_DIR/openconnect-hda/windows/wix/build
+  needs:
+    - job: create_release
+  tags:
+    - win
+    - openconnect-hda
diff --git a/.idea/openconnect-gui.iml b/.idea/openconnect-gui.iml
index f08604b..e621658 100644
--- a/.idea/openconnect-gui.iml
+++ b/.idea/openconnect-gui.iml
@@ -1,2 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<module classpath="CMake" type="CPP_MODULE" version="4" />
\ No newline at end of file
+<module classpath="CMake" type="CPP_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="Python" name="Python facet">
+      <configuration sdkName="Python 3.8" />
+    </facet>
+  </component>
+</module>
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2e89d2c..c4abbf7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -6,12 +6,11 @@ set(CMAKE_AUTOMOC ON)
 set(CMAKE_AUTORCC ON)
 set(CMAKE_AUTOUIC ON)
 
+# assume default location of qt cmake files
 if (WIN32)
-    # On Windows Machines set this to the install directory of Qt
     set(Qt5_DIR "C:/Qt/Qt5.12.10/5.12.10/mingw73_64/lib/cmake/Qt5")
 elseif (UNIX)
-    # On Linux set this to the path where find_package() etc. should search for Qt Files
-    set(CMAKE_PREFIX_PATH "/usr/lib64/cmake/Qt5")
+    set(Qt5_DIR "/usr/lib64/cmake/Qt5")
 endif ()
 
 set(QT_VERSION 5)
diff --git a/README.md b/README.md
index 3b27064..26cbb1c 100644
--- a/README.md
+++ b/README.md
@@ -86,21 +86,6 @@ Options:
 * `openconnect` development files (on some distributions additional `-devel / -dev` and `-lib` package)
 * `vpnc` if not installed with `openconnect` package
 
-## History
-* 0.3.1
-  * switch to gnutls to fix security issue (see https://www.openssl.org/news/secadv/20210325.txt)
-* 0.3.0
-  * separate ui and openconnect library calls
-  * run ui and openconnect library calls in different threads
-  * refactor PopupInfo
-* 0.2.0
-  * Fix connect and disconnect process
-  * Refactor connection status update
-* 0.1.1
-  * Updated package dependencies
-* 0.1.0
-  * Working version without complete documentation and without code signing (windows)
-
 ## Meta
 
 Lukas Koenen – lukas.koenen@h-da.de
diff --git a/infra/scripts/addReleaseLink.js b/infra/scripts/addReleaseLink.js
new file mode 100644
index 0000000..74fed49
--- /dev/null
+++ b/infra/scripts/addReleaseLink.js
@@ -0,0 +1,71 @@
+const gitlab = require("@gitbeaker/node");
+const semver = require("semver");
+const fs = require("fs")
+
+async function addReleaseLink(gitlabHost, gitlabToken, projectId, installerType, assetPath) {
+    const api = new gitlab.Gitlab({
+        host: gitlabHost,
+        oauthToken: gitlabToken
+    });
+
+    const projectUrl = (await api.Projects.show(projectId)).web_url;
+    const projectName = (await api.Projects.show(projectId)).name;
+    const projectRelease = semver.maxSatisfying(
+        (await api.Releases.all(projectId)).map(release => release.tag_name), "*");
+
+    const linkTargetFilename = {
+        rpm: `${projectName}-${projectRelease}-1.fc33.x86_64.rpm`,
+        deb: `${projectName}_${projectRelease}-1_amd64.deb`,
+        msi: `${projectName}.msi`
+    }
+    const filePath = `${assetPath}/${linkTargetFilename[installerType]}`;
+
+    const releaseLinks = (await api.ReleaseLinks.all(projectId, projectRelease)).map(link => link.tag_name);
+    if (!releaseLinks.includes(linkTargetFilename[installerType])) {
+        const uploadResponse = (await api.Projects.upload(
+            projectId, fs.readFileSync(filePath), {
+                metadata: {
+                    filename: `${linkTargetFilename[installerType]}`,
+                    contentType: "multipart/form-data"
+                }
+            }))
+        await api.ReleaseLinks.create(
+            projectId, projectRelease, `${linkTargetFilename[installerType]}`, `${projectUrl}${uploadResponse.url}`)
+    }
+}
+
+const argv = require("yargs/yargs")(process.argv.slice(2))
+    .option("gitlab_host", {
+        description: "Set the gitlab host",
+        type: "string",
+        required: true
+    })
+    .option("gitlab_token", {
+        description: "Set the gitlab token",
+        type: "string",
+        required: true
+    })
+    .option("project_id", {
+        description: "Set the gitlab project id",
+        type: "number",
+        required: true
+    })
+    .option("installer_type", {
+        description: "Set the type of the asset",
+        choices: ["rpm", "deb", "msi"],
+        required: true
+    })
+    .option("asset_path", {
+        description: "Set the path of the installer",
+        type: "string",
+        default: process.cwd()
+    })
+    .showHelpOnFail(true)
+    .alias("help", "h")
+    .hide("version")
+    .argv;
+
+addReleaseLink(argv.gitlab_host, argv.gitlab_token, argv.project_id, argv.installer_type, argv.asset_path).catch((error) => {
+    console.log(error);
+    process.exit(1);
+});
diff --git a/infra/scripts/downloadRelease.js b/infra/scripts/downloadRelease.js
new file mode 100644
index 0000000..4f57983
--- /dev/null
+++ b/infra/scripts/downloadRelease.js
@@ -0,0 +1,76 @@
+const gitlab = require("@gitbeaker/node");
+const semver = require("semver");
+const fetch = require("node-fetch");
+const fs = require('fs');
+const stream = require('stream');
+const util = require('util');
+const tar = require("tar");
+
+async function downloadRelease(gitlabHost, gitlabToken, projectId, installerType, outputPath, extractOutputPath) {
+    const api = new gitlab.Gitlab({
+        host: gitlabHost,
+        jobToken: gitlabToken
+
+    });
+
+    const projectUrl = (await api.Projects.show(projectId)).web_url;
+    const projectName = (await api.Projects.show(projectId)).name;
+    const projectRelease = semver.maxSatisfying(
+        (await api.Releases.all(projectId)).map(release => release.tag_name), "*");
+
+    const outputFilename = {
+        rpm: `${projectName}-${projectRelease}.tar.gz`,
+        deb: `${projectName}_${projectRelease}.orig.tar.gz`,
+        msi: `${projectName}.tar.gz`
+    }
+
+    await util.promisify(stream.pipeline)(
+        (await fetch(`${projectUrl}/-/archive/${projectRelease}/${projectName}-${projectRelease}.tar.gz`)).body,
+        fs.createWriteStream(`${outputPath}/${outputFilename[installerType]}`)
+    );
+
+    await tar.extract({file: `${outputPath}/${outputFilename[installerType]}`, cwd: extractOutputPath});
+    fs.renameSync(`${extractOutputPath}/${projectName}-${projectRelease}`, `${extractOutputPath}/${projectName}`);
+}
+
+const argv = require("yargs/yargs")(process.argv.slice(2))
+    .option("gitlab_host", {
+        description: "Set the gitlab host",
+        type: "string",
+        required: true
+    })
+    .option("gitlab_token", {
+        description: "Set the gitlab token",
+        type: "string",
+        required: true
+    })
+    .option("project_id", {
+        description: "Set the gitlab project id",
+        type: "number",
+        required: true
+    })
+    .option("installer_type", {
+        description: "Set the type of the installer",
+        choices: ["rpm", "deb", "msi"],
+        required: true
+    })
+    .option("output_path", {
+        description: "Set the output path for the release archive",
+        type: "string",
+        default: process.cwd()
+    })
+    .option("extract_output_path", {
+        description: "Set output path for the uncompressed release archive",
+        type: "string",
+        default: process.cwd()
+    })
+    .showHelpOnFail(true)
+    .alias("help", "h")
+    .hide("version")
+    .argv;
+
+downloadRelease(argv.gitlab_host, argv.gitlab_token, argv.project_id, argv.installer_type, argv.output_path, argv.extract_output_path).catch((error) => {
+    console.log(error);
+    process.exit(1);
+});
+
diff --git a/infra/scripts/downloadTapAdapter.js b/infra/scripts/downloadTapAdapter.js
new file mode 100644
index 0000000..2a4afb4
--- /dev/null
+++ b/infra/scripts/downloadTapAdapter.js
@@ -0,0 +1,55 @@
+const fetch = require("node-fetch");
+const fs = require("fs");
+const stream = require("stream");
+const util = require("util");
+const seven = require("node-7z");
+
+async function downloadTapAdapter(tapAdapterFilename, openvpnFilename, archiveUrl, outputPath) {
+    await util.promisify(stream.pipeline)(
+        (await fetch(`${archiveUrl}/${tapAdapterFilename}`)).body,
+        fs.createWriteStream(`${outputPath}/${tapAdapterFilename}`)
+    );
+
+    await util.promisify(stream.pipeline)(
+        (await fetch(`${archiveUrl}/${openvpnFilename}`)).body,
+        fs.createWriteStream(`${outputPath}/${openvpnFilename}`)
+    );
+
+    seven.extract(`${outputPath}/${openvpnFilename}`, `${outputPath}`, {
+        recursive: true,
+        $cherryPick: "tapctl.exe"
+    }).on("end", function () {
+        fs.unlinkSync(`${outputPath}/${openvpnFilename}`)
+    });
+}
+
+const argv = require("yargs/yargs")(process.argv.slice(2))
+    .option("tap_adapter_filename", {
+        description: "Set the filename of the tap adapter merge module",
+        type: "string",
+        required: true
+    })
+    .option("openvpn_filename", {
+        description: "Set the filename of openvpn installer, used to extract the tabctl utility",
+        type: "string",
+        required: true
+    })
+    .option("archive_url", {
+        description: "Set the url of the file archive",
+        type: "string",
+        default: "http://build.openvpn.net/downloads/releases/"
+    })
+    .option("output_path", {
+        description: "Set the output path for the files",
+        type: "string",
+        default: process.cwd()
+    })
+    .showHelpOnFail(true)
+    .alias("help", "h")
+    .hide("version")
+    .argv;
+
+downloadTapAdapter(argv.tap_adapter_filename, argv.openvpn_filename, argv.archive_url, argv.output_path).catch((error) => {
+    console.log(error);
+    process.exit(1);
+});
\ No newline at end of file
diff --git a/release.config.js b/release.config.js
new file mode 100644
index 0000000..c10dd6b
--- /dev/null
+++ b/release.config.js
@@ -0,0 +1,146 @@
+"use strict";
+
+module.exports = {
+    plugins: [
+        [
+            "@semantic-release/commit-analyzer",
+            {
+                preset: "conventionalcommits"
+            }
+        ],
+        [
+            "@semantic-release/release-notes-generator",
+            {
+                preset: "conventionalcommits"
+            }
+        ],
+        {
+            prepare: (_, context) => {
+                const fs = require('fs')
+                const path = require("path");
+                const dateformat = require("dateformat");
+                const parser = require("conventional-commits-parser").sync;
+                const filter = require("conventional-commits-filter");
+
+                String.prototype.splice = function (index, remove, string) {
+                    return this.slice(0, index) + string + this.slice(index + Math.abs(remove));
+                };
+
+                const bumpFiles = ["rpm/openconnect-hda.spec", "makepkg/PKGBUILD", "windows/wix/openconnect-hda.wxs", "src/openconnect-hda.8"];
+                const changelogFiles = ["debian/changelog", "rpm/openconnect-hda.spec"];
+
+                const parsedCommits = filter(
+                    context.commits.filter(({message}) =>
+                        message.trim()
+                    ).map(rawCommit => ({
+                        ...rawCommit,
+                        ...parser(rawCommit.message)
+                    })).filter(({type}) =>
+                        type === "feat" || type === "fix"
+                    )
+                );
+
+                const debianChangelog = [
+                    `openconnect-hda (${context.nextRelease.gitTag}-1) unstable; urgency=low`,
+                    "",
+                    ...parsedCommits.map(commit => "  * " + commit.subject),
+                    "",
+                    ` -- ${context.env.GIT_COMMITTER_NAME} <${context.env.GIT_COMMITTER_EMAIL}> ${dateformat(new Date(), "ddd, d mmm yyyy HH:MM:ss Z").replace("GMT", "")}`
+                ].join("\n");
+
+                const rpmChangelog = [
+                    `* ${dateformat(new Date(), "ddd mmm d yyyy")} ${context.env.GIT_COMMITTER_NAME} <${context.env.GIT_COMMITTER_EMAIL}> - ${context.nextRelease.gitTag}-1`,
+                    ...parsedCommits.map(commit => "- " + commit.subject)
+                ].join("\n");
+
+                const versionUpdaterByFile = {
+                    "windows/wix/openconnect-hda.wxs"(content) {
+                        const versionStringIndex = content.indexOf("Version=");
+
+                        const versionString = content.substring(versionStringIndex, content.indexOf("\n", versionStringIndex));
+                        const newVersionString = versionString.replace(context.lastRelease.gitTag, context.nextRelease.gitTag);
+
+                        return content.splice(versionStringIndex, versionString.length, newVersionString);
+                    },
+                    "rpm/openconnect-hda.spec"(content) {
+                        const versionStringIndex = content.indexOf("Version:");
+
+                        const versionString = content.substring(versionStringIndex, content.indexOf("\n", versionStringIndex));
+                        const newVersionString = versionString.replace(context.lastRelease.gitTag, context.nextRelease.gitTag);
+
+                        return content.splice(versionStringIndex, versionString.length, newVersionString);
+                    },
+                    "makepkg/PKGBUILD"(content) {
+                        const versionStringIndex = content.indexOf("pkgver=");
+
+                        const versionString = content.substring(versionStringIndex, content.indexOf("\n", versionStringIndex));
+                        const newVersionString = versionString.replace(context.lastRelease.gitTag, context.nextRelease.gitTag);
+
+                        return content.splice(versionStringIndex, versionString.length, newVersionString);
+                    },
+                    "src/openconnect-hda.8"(content) {
+                        return content.replace(context.lastRelease.gitTag, context.nextRelease.gitTag);
+                    }
+                };
+
+                const changelogUpdaterByFile = {
+                    "rpm/openconnect-hda.spec"(content) {
+                        return content.splice(content.indexOf("%changelog") + "%changelog".length, 0, "\n" + rpmChangelog + "\n");
+                    },
+                    "debian/changelog"(content) {
+                        return content.splice(0, 0, debianChangelog + "\n\n");
+                    }
+                }
+
+                function updateFile(updater, file) {
+                    const configPath = path.resolve(process.cwd(), file)
+                    try {
+                        const stat = fs.lstatSync(configPath);
+                        if (!stat.isFile()) {
+                            return;
+                        }
+
+                        let content = fs.readFileSync(configPath, 'utf8').toString();
+                        fs.writeFileSync(configPath, updater(content), 'utf8');
+                    } catch (err) {
+                        if (err.code !== 'ENOENT') console.warn(err.message);
+                    }
+                }
+
+                bumpFiles.forEach(file => {
+                        const updater = versionUpdaterByFile[file];
+                        if (!updater) {
+                            throw Error(`Unable to locate updater for provided file (${file}).`);
+                        }
+
+                        updateFile(updater, file);
+                    }
+                )
+
+                changelogFiles.forEach(file => {
+                        const updater = changelogUpdaterByFile[file];
+                        if (!updater) {
+                            throw Error(`Unable to locate updater for provided file (${file}).`);
+                        }
+
+                        updateFile(updater, file);
+                    }
+                )
+            }
+        },
+        [
+            "@semantic-release/git",
+            {
+                assets: ["debian/changelog", "rpm/openconnect-hda.spec", "makepkg/PKGBUILD", "windows/wix/openconnect-hda.wxs", "src/openconnect-hda.8"],
+                message: "release(<%= nextRelease.version %>): auto generated release version <%= nextRelease.version %> at <%= new Date().toISOString() %>\n\n<%= nextRelease.notes %>"
+            }
+        ],
+        [
+            "@semantic-release/gitlab",
+            {
+                gitlabUrl: "https://code.fbi.h-da.de/"
+            }
+        ]
+    ],
+    tagFormat: "${version}"
+}
-- 
GitLab