From 74be1d4e13f2a630c65cf951b3f06f493019b2db Mon Sep 17 00:00:00 2001
From: Johannes Loher <johannes.loher@fg4f.de>
Date: Wed, 6 Jan 2021 00:59:22 +0100
Subject: [PATCH 1/6] put release artifacts in ds4 directory

---
 .gitlab-ci.yml  | 5 +++--
 src/system.json | 4 ++--
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c3370fa4..bc2e0930 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -45,11 +45,12 @@ build:
     stage: build
     script:
         - npm run build
+        - mv dist ds4
     cache:
         <<: *global_cache
     artifacts:
         paths:
-            - dist
+            - ds4
         expire_in: 1 week
 
 deploy:
@@ -58,7 +59,7 @@ deploy:
     dependencies:
         - build
     script:
-        - rsync --delete -az ./dist/ rsync://${DEPLOYMENT_USER}@${DEPLOYMENT_SERVER}:${DEPLOYMENT_PATH}
+        - rsync --delete -az ./ds4/ rsync://${DEPLOYMENT_USER}@${DEPLOYMENT_SERVER}:${DEPLOYMENT_PATH}
     environment:
         name: production
         url: https://vtt.f3l.de/
diff --git a/src/system.json b/src/system.json
index 3929e3ab..43f2245b 100644
--- a/src/system.json
+++ b/src/system.json
@@ -22,7 +22,7 @@
     "gridUnits": "m",
     "primaryTokenAttribute": "combatValues.hitPoints.current",
     "url": "https://git.f3l.de/dungeonslayers/ds4",
-    "manifest": "https://git.f3l.de/dungeonslayers/ds4/-/blob/master/src/system.json",
-    "download": "https://git.f3l.de/dungeonslayers/ds4/-/archive/master/ds4-master.zip",
+    "manifest": "https://git.f3l.de/dungeonslayers/ds4/-/raw/latest/src/system.json?inline=false",
+    "download": "https://git.f3l.de/dungeonslayers/ds4/-/jobs/artifacts/latest/download?job=build",
     "license": "MIT"
 }

From 6d02f1623a9b7cc8ede66057150bd384203a6b67 Mon Sep 17 00:00:00 2001
From: Johannes Loher <johannes.loher@fg4f.de>
Date: Wed, 6 Jan 2021 01:30:21 +0100
Subject: [PATCH 2/6] reference a version tag in the download url

---
 src/system.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/system.json b/src/system.json
index 43f2245b..a57664c3 100644
--- a/src/system.json
+++ b/src/system.json
@@ -22,7 +22,7 @@
     "gridUnits": "m",
     "primaryTokenAttribute": "combatValues.hitPoints.current",
     "url": "https://git.f3l.de/dungeonslayers/ds4",
-    "manifest": "https://git.f3l.de/dungeonslayers/ds4/-/raw/latest/src/system.json?inline=false",
-    "download": "https://git.f3l.de/dungeonslayers/ds4/-/jobs/artifacts/latest/download?job=build",
+    "manifest": "https://git.f3l.de/dungeonslayers/ds4/-/raw/master/src/system.json?inline=false",
+    "download": "https://git.f3l.de/dungeonslayers/ds4/-/jobs/artifacts/0.1.0/download?job=build",
     "license": "MIT"
 }

From b4f697f9cc1dee8b4604a56d9ed3e8bfeae99750 Mon Sep 17 00:00:00 2001
From: Johannes Loher <johannes.loher@fg4f.de>
Date: Wed, 6 Jan 2021 01:31:23 +0100
Subject: [PATCH 3/6] format gulpfile

---
 gulpfile.js | 858 ++++++++++++++++++++++++++--------------------------
 1 file changed, 429 insertions(+), 429 deletions(-)

diff --git a/gulpfile.js b/gulpfile.js
index 83aa3820..dc62e8b7 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -1,429 +1,429 @@
-const gulp = require("gulp");
-const fs = require("fs-extra");
-const path = require("path");
-const chalk = require("chalk");
-const archiver = require("archiver");
-const stringify = require("json-stringify-pretty-compact");
-const typescript = require("typescript");
-
-const ts = require("gulp-typescript");
-const less = require("gulp-less");
-const sass = require("gulp-sass");
-const git = require("gulp-git");
-
-const argv = require("yargs").argv;
-
-sass.compiler = require("sass");
-
-function getConfig() {
-    const configPath = path.resolve(process.cwd(), "foundryconfig.json");
-    let config;
-
-    if (fs.existsSync(configPath)) {
-        config = fs.readJSONSync(configPath);
-        return config;
-    } else {
-        return;
-    }
-}
-
-function getManifest() {
-    const json = {};
-
-    if (fs.existsSync("src")) {
-        json.root = "src";
-    } else {
-        json.root = "dist";
-    }
-
-    const modulePath = path.join(json.root, "module.json");
-    const systemPath = path.join(json.root, "system.json");
-
-    if (fs.existsSync(modulePath)) {
-        json.file = fs.readJSONSync(modulePath);
-        json.name = "module.json";
-    } else if (fs.existsSync(systemPath)) {
-        json.file = fs.readJSONSync(systemPath);
-        json.name = "system.json";
-    } else {
-        return;
-    }
-
-    return json;
-}
-
-/**
- * TypeScript transformers
- * @returns {typescript.TransformerFactory<typescript.SourceFile>}
- */
-function createTransformer() {
-    /**
-     * @param {typescript.Node} node
-     */
-    function shouldMutateModuleSpecifier(node) {
-        if (!typescript.isImportDeclaration(node) && !typescript.isExportDeclaration(node)) return false;
-        if (node.moduleSpecifier === undefined) return false;
-        if (!typescript.isStringLiteral(node.moduleSpecifier)) return false;
-        if (!node.moduleSpecifier.text.startsWith("./") && !node.moduleSpecifier.text.startsWith("../")) return false;
-        if (path.extname(node.moduleSpecifier.text) !== "") return false;
-        return true;
-    }
-
-    /**
-     * Transforms import/export declarations to append `.js` extension
-     * @param {typescript.TransformationContext} context
-     */
-    function importTransformer(context) {
-        return (node) => {
-            /**
-             * @param {typescript.Node} node
-             */
-            function visitor(node) {
-                if (shouldMutateModuleSpecifier(node)) {
-                    if (typescript.isImportDeclaration(node)) {
-                        const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`);
-                        return typescript.updateImportDeclaration(
-                            node,
-                            node.decorators,
-                            node.modifiers,
-                            node.importClause,
-                            newModuleSpecifier
-                        );
-                    } else if (typescript.isExportDeclaration(node)) {
-                        const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`);
-                        return typescript.updateExportDeclaration(
-                            node,
-                            node.decorators,
-                            node.modifiers,
-                            node.exportClause,
-                            newModuleSpecifier
-                        );
-                    }
-                }
-                return typescript.visitEachChild(node, visitor, context);
-            }
-
-            return typescript.visitNode(node, visitor);
-        };
-    }
-
-    return importTransformer;
-}
-
-const tsConfig = ts.createProject("tsconfig.json", {
-    getCustomTransformers: (_program) => ({
-        after: [createTransformer()],
-    }),
-});
-
-/********************/
-/*		BUILD		*/
-/********************/
-
-/**
- * Build TypeScript
- */
-function buildTS() {
-    return gulp.src("src/**/*.ts").pipe(tsConfig()).pipe(gulp.dest("dist"));
-}
-
-/**
- * Build Less
- */
-function buildLess() {
-    return gulp.src("src/*.less").pipe(less()).pipe(gulp.dest("dist"));
-}
-
-/**
- * Build SASS
- */
-function buildSASS() {
-    return gulp.src("src/*.scss").pipe(sass().on("error", sass.logError)).pipe(gulp.dest("dist"));
-}
-
-/**
- * Copy static files
- */
-async function copyFiles() {
-    const statics = ["lang", "fonts", "assets", "templates", "module.json", "system.json", "template.json"];
-    try {
-        for (const file of statics) {
-            if (fs.existsSync(path.join("src", file))) {
-                await fs.copy(path.join("src", file), path.join("dist", file));
-            }
-        }
-        return Promise.resolve();
-    } catch (err) {
-        Promise.reject(err);
-    }
-}
-
-/**
- * Watch for changes for each build step
- */
-function buildWatch() {
-    gulp.watch("src/**/*.ts", { ignoreInitial: false }, buildTS);
-    gulp.watch("src/**/*.less", { ignoreInitial: false }, buildLess);
-    gulp.watch("src/**/*.scss", { ignoreInitial: false }, buildSASS);
-    gulp.watch(["src/fonts", "src/lang", "src/templates", "src/*.json"], { ignoreInitial: false }, copyFiles);
-}
-
-/********************/
-/*		CLEAN		*/
-/********************/
-
-/**
- * Remove built files from `dist` folder
- * while ignoring source files
- */
-async function clean() {
-    const name = path.basename(path.resolve("."));
-    const files = [];
-
-    // If the project uses TypeScript
-    if (fs.existsSync(path.join("src", `${name}.ts`))) {
-        files.push(
-            "lang",
-            "templates",
-            "assets",
-            "module",
-            `${name}.js`,
-            "module.json",
-            "system.json",
-            "template.json"
-        );
-    }
-
-    // If the project uses Less or SASS
-    if (fs.existsSync(path.join("src", `${name}.less`)) || fs.existsSync(path.join("src", `${name}.scss`))) {
-        files.push("fonts", `${name}.css`);
-    }
-
-    console.log(" ", chalk.yellow("Files to clean:"));
-    console.log("   ", chalk.blueBright(files.join("\n    ")));
-
-    // Attempt to remove the files
-    try {
-        for (const filePath of files) {
-            await fs.remove(path.join("dist", filePath));
-        }
-        return Promise.resolve();
-    } catch (err) {
-        Promise.reject(err);
-    }
-}
-
-/********************/
-/*		LINK		*/
-/********************/
-
-/**
- * Link build to User Data folder
- */
-async function linkUserData() {
-    const name = path.basename(path.resolve("."));
-    const config = fs.readJSONSync("foundryconfig.json");
-
-    let destDir;
-    try {
-        if (
-            fs.existsSync(path.resolve(".", "dist", "module.json")) ||
-            fs.existsSync(path.resolve(".", "src", "module.json"))
-        ) {
-            destDir = "modules";
-        } else if (
-            fs.existsSync(path.resolve(".", "dist", "system.json")) ||
-            fs.existsSync(path.resolve(".", "src", "system.json"))
-        ) {
-            destDir = "systems";
-        } else {
-            throw Error(`Could not find ${chalk.blueBright("module.json")} or ${chalk.blueBright("system.json")}`);
-        }
-
-        let linkDir;
-        if (config.dataPath) {
-            if (!fs.existsSync(path.join(config.dataPath, "Data")))
-                throw Error("User Data path invalid, no Data directory found");
-
-            linkDir = path.join(config.dataPath, "Data", destDir, name);
-        } else {
-            throw Error("No User Data path defined in foundryconfig.json");
-        }
-
-        if (argv.clean || argv.c) {
-            console.log(chalk.yellow(`Removing build in ${chalk.blueBright(linkDir)}`));
-
-            await fs.remove(linkDir);
-        } else if (!fs.existsSync(linkDir)) {
-            console.log(chalk.green(`Copying build to ${chalk.blueBright(linkDir)}`));
-            await fs.symlink(path.resolve("./dist"), linkDir);
-        }
-        return Promise.resolve();
-    } catch (err) {
-        Promise.reject(err);
-    }
-}
-
-/*********************/
-/*		PACKAGE		 */
-/*********************/
-
-/**
- * Package build
- */
-async function packageBuild() {
-    const manifest = getManifest();
-
-    return new Promise((resolve, reject) => {
-        try {
-            // Remove the package dir without doing anything else
-            if (argv.clean || argv.c) {
-                console.log(chalk.yellow("Removing all packaged files"));
-                fs.removeSync("package");
-                return;
-            }
-
-            // Ensure there is a directory to hold all the packaged versions
-            fs.ensureDirSync("package");
-
-            // Initialize the zip file
-            const zipName = `${manifest.file.name}-v${manifest.file.version}.zip`;
-            const zipFile = fs.createWriteStream(path.join("package", zipName));
-            const zip = archiver("zip", { zlib: { level: 9 } });
-
-            zipFile.on("close", () => {
-                console.log(chalk.green(zip.pointer() + " total bytes"));
-                console.log(chalk.green(`Zip file ${zipName} has been written`));
-                return resolve();
-            });
-
-            zip.on("error", (err) => {
-                throw err;
-            });
-
-            zip.pipe(zipFile);
-
-            // Add the directory with the final code
-            zip.directory("dist/", manifest.file.name);
-
-            zip.finalize();
-        } catch (err) {
-            return reject(err);
-        }
-    });
-}
-
-/*********************/
-/*		PACKAGE		 */
-/*********************/
-
-/**
- * Update version and URLs in the manifest JSON
- */
-function updateManifest(cb) {
-    const packageJson = fs.readJSONSync("package.json");
-    const config = getConfig(),
-        manifest = getManifest(),
-        rawURL = config.rawURL,
-        repoURL = config.repository,
-        manifestRoot = manifest.root;
-
-    if (!config) cb(Error(chalk.red("foundryconfig.json not found")));
-    if (!manifest) cb(Error(chalk.red("Manifest JSON not found")));
-    if (!rawURL || !repoURL) cb(Error(chalk.red("Repository URLs not configured in foundryconfig.json")));
-
-    try {
-        const version = argv.update || argv.u;
-
-        /* Update version */
-
-        const versionMatch = /^(\d{1,}).(\d{1,}).(\d{1,})$/;
-        const currentVersion = manifest.file.version;
-        let targetVersion = "";
-
-        if (!version) {
-            cb(Error("Missing version number"));
-        }
-
-        if (versionMatch.test(version)) {
-            targetVersion = version;
-        } else {
-            targetVersion = currentVersion.replace(versionMatch, (substring, major, minor, patch) => {
-                console.log(substring, Number(major) + 1, Number(minor) + 1, Number(patch) + 1);
-                if (version === "major") {
-                    return `${Number(major) + 1}.0.0`;
-                } else if (version === "minor") {
-                    return `${major}.${Number(minor) + 1}.0`;
-                } else if (version === "patch") {
-                    return `${major}.${minor}.${Number(patch) + 1}`;
-                } else {
-                    return "";
-                }
-            });
-        }
-
-        if (targetVersion === "") {
-            return cb(Error(chalk.red("Error: Incorrect version arguments.")));
-        }
-
-        if (targetVersion === currentVersion) {
-            return cb(Error(chalk.red("Error: Target version is identical to current version.")));
-        }
-        console.log(`Updating version number to '${targetVersion}'`);
-
-        packageJson.version = targetVersion;
-        manifest.file.version = targetVersion;
-
-        /* Update URLs */
-
-        const result = `${rawURL}/v${manifest.file.version}/package/${manifest.file.name}-v${manifest.file.version}.zip`;
-
-        manifest.file.url = repoURL;
-        manifest.file.manifest = `${rawURL}/master/${manifestRoot}/${manifest.name}`;
-        manifest.file.download = result;
-
-        const prettyProjectJson = stringify(manifest.file, {
-            maxLength: 35,
-            indent: "\t",
-        });
-
-        fs.writeJSONSync("package.json", packageJson, { spaces: "\t" });
-        fs.writeFileSync(path.join(manifest.root, manifest.name), prettyProjectJson, "utf8");
-
-        return cb();
-    } catch (err) {
-        cb(err);
-    }
-}
-
-function gitAdd() {
-    return gulp.src("package").pipe(git.add({ args: "--no-all" }));
-}
-
-function gitCommit() {
-    return gulp.src("./*").pipe(
-        git.commit(`v${getManifest().file.version}`, {
-            args: "-a",
-            disableAppendPaths: true,
-        })
-    );
-}
-
-function gitTag() {
-    const manifest = getManifest();
-    return git.tag(`v${manifest.file.version}`, `Updated to ${manifest.file.version}`, (err) => {
-        if (err) throw err;
-    });
-}
-
-const execGit = gulp.series(gitAdd, gitCommit, gitTag);
-
-const execBuild = gulp.parallel(buildTS, buildLess, buildSASS, copyFiles);
-
-exports.build = gulp.series(clean, execBuild);
-exports.watch = buildWatch;
-exports.clean = clean;
-exports.link = linkUserData;
-exports.package = packageBuild;
-exports.update = updateManifest;
-exports.publish = gulp.series(clean, updateManifest, execBuild, packageBuild, execGit);
+const gulp = require("gulp");
+const fs = require("fs-extra");
+const path = require("path");
+const chalk = require("chalk");
+const archiver = require("archiver");
+const stringify = require("json-stringify-pretty-compact");
+const typescript = require("typescript");
+
+const ts = require("gulp-typescript");
+const less = require("gulp-less");
+const sass = require("gulp-sass");
+const git = require("gulp-git");
+
+const argv = require("yargs").argv;
+
+sass.compiler = require("sass");
+
+function getConfig() {
+    const configPath = path.resolve(process.cwd(), "foundryconfig.json");
+    let config;
+
+    if (fs.existsSync(configPath)) {
+        config = fs.readJSONSync(configPath);
+        return config;
+    } else {
+        return;
+    }
+}
+
+function getManifest() {
+    const json = {};
+
+    if (fs.existsSync("src")) {
+        json.root = "src";
+    } else {
+        json.root = "dist";
+    }
+
+    const modulePath = path.join(json.root, "module.json");
+    const systemPath = path.join(json.root, "system.json");
+
+    if (fs.existsSync(modulePath)) {
+        json.file = fs.readJSONSync(modulePath);
+        json.name = "module.json";
+    } else if (fs.existsSync(systemPath)) {
+        json.file = fs.readJSONSync(systemPath);
+        json.name = "system.json";
+    } else {
+        return;
+    }
+
+    return json;
+}
+
+/**
+ * TypeScript transformers
+ * @returns {typescript.TransformerFactory<typescript.SourceFile>}
+ */
+function createTransformer() {
+    /**
+     * @param {typescript.Node} node
+     */
+    function shouldMutateModuleSpecifier(node) {
+        if (!typescript.isImportDeclaration(node) && !typescript.isExportDeclaration(node)) return false;
+        if (node.moduleSpecifier === undefined) return false;
+        if (!typescript.isStringLiteral(node.moduleSpecifier)) return false;
+        if (!node.moduleSpecifier.text.startsWith("./") && !node.moduleSpecifier.text.startsWith("../")) return false;
+        if (path.extname(node.moduleSpecifier.text) !== "") return false;
+        return true;
+    }
+
+    /**
+     * Transforms import/export declarations to append `.js` extension
+     * @param {typescript.TransformationContext} context
+     */
+    function importTransformer(context) {
+        return (node) => {
+            /**
+             * @param {typescript.Node} node
+             */
+            function visitor(node) {
+                if (shouldMutateModuleSpecifier(node)) {
+                    if (typescript.isImportDeclaration(node)) {
+                        const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`);
+                        return typescript.updateImportDeclaration(
+                            node,
+                            node.decorators,
+                            node.modifiers,
+                            node.importClause,
+                            newModuleSpecifier,
+                        );
+                    } else if (typescript.isExportDeclaration(node)) {
+                        const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`);
+                        return typescript.updateExportDeclaration(
+                            node,
+                            node.decorators,
+                            node.modifiers,
+                            node.exportClause,
+                            newModuleSpecifier,
+                        );
+                    }
+                }
+                return typescript.visitEachChild(node, visitor, context);
+            }
+
+            return typescript.visitNode(node, visitor);
+        };
+    }
+
+    return importTransformer;
+}
+
+const tsConfig = ts.createProject("tsconfig.json", {
+    getCustomTransformers: (_program) => ({
+        after: [createTransformer()],
+    }),
+});
+
+/********************/
+/*		BUILD		*/
+/********************/
+
+/**
+ * Build TypeScript
+ */
+function buildTS() {
+    return gulp.src("src/**/*.ts").pipe(tsConfig()).pipe(gulp.dest("dist"));
+}
+
+/**
+ * Build Less
+ */
+function buildLess() {
+    return gulp.src("src/*.less").pipe(less()).pipe(gulp.dest("dist"));
+}
+
+/**
+ * Build SASS
+ */
+function buildSASS() {
+    return gulp.src("src/*.scss").pipe(sass().on("error", sass.logError)).pipe(gulp.dest("dist"));
+}
+
+/**
+ * Copy static files
+ */
+async function copyFiles() {
+    const statics = ["lang", "fonts", "assets", "templates", "module.json", "system.json", "template.json"];
+    try {
+        for (const file of statics) {
+            if (fs.existsSync(path.join("src", file))) {
+                await fs.copy(path.join("src", file), path.join("dist", file));
+            }
+        }
+        return Promise.resolve();
+    } catch (err) {
+        Promise.reject(err);
+    }
+}
+
+/**
+ * Watch for changes for each build step
+ */
+function buildWatch() {
+    gulp.watch("src/**/*.ts", { ignoreInitial: false }, buildTS);
+    gulp.watch("src/**/*.less", { ignoreInitial: false }, buildLess);
+    gulp.watch("src/**/*.scss", { ignoreInitial: false }, buildSASS);
+    gulp.watch(["src/fonts", "src/lang", "src/templates", "src/*.json"], { ignoreInitial: false }, copyFiles);
+}
+
+/********************/
+/*		CLEAN		*/
+/********************/
+
+/**
+ * Remove built files from `dist` folder
+ * while ignoring source files
+ */
+async function clean() {
+    const name = path.basename(path.resolve("."));
+    const files = [];
+
+    // If the project uses TypeScript
+    if (fs.existsSync(path.join("src", `${name}.ts`))) {
+        files.push(
+            "lang",
+            "templates",
+            "assets",
+            "module",
+            `${name}.js`,
+            "module.json",
+            "system.json",
+            "template.json",
+        );
+    }
+
+    // If the project uses Less or SASS
+    if (fs.existsSync(path.join("src", `${name}.less`)) || fs.existsSync(path.join("src", `${name}.scss`))) {
+        files.push("fonts", `${name}.css`);
+    }
+
+    console.log(" ", chalk.yellow("Files to clean:"));
+    console.log("   ", chalk.blueBright(files.join("\n    ")));
+
+    // Attempt to remove the files
+    try {
+        for (const filePath of files) {
+            await fs.remove(path.join("dist", filePath));
+        }
+        return Promise.resolve();
+    } catch (err) {
+        Promise.reject(err);
+    }
+}
+
+/********************/
+/*		LINK		*/
+/********************/
+
+/**
+ * Link build to User Data folder
+ */
+async function linkUserData() {
+    const name = path.basename(path.resolve("."));
+    const config = fs.readJSONSync("foundryconfig.json");
+
+    let destDir;
+    try {
+        if (
+            fs.existsSync(path.resolve(".", "dist", "module.json")) ||
+            fs.existsSync(path.resolve(".", "src", "module.json"))
+        ) {
+            destDir = "modules";
+        } else if (
+            fs.existsSync(path.resolve(".", "dist", "system.json")) ||
+            fs.existsSync(path.resolve(".", "src", "system.json"))
+        ) {
+            destDir = "systems";
+        } else {
+            throw Error(`Could not find ${chalk.blueBright("module.json")} or ${chalk.blueBright("system.json")}`);
+        }
+
+        let linkDir;
+        if (config.dataPath) {
+            if (!fs.existsSync(path.join(config.dataPath, "Data")))
+                throw Error("User Data path invalid, no Data directory found");
+
+            linkDir = path.join(config.dataPath, "Data", destDir, name);
+        } else {
+            throw Error("No User Data path defined in foundryconfig.json");
+        }
+
+        if (argv.clean || argv.c) {
+            console.log(chalk.yellow(`Removing build in ${chalk.blueBright(linkDir)}`));
+
+            await fs.remove(linkDir);
+        } else if (!fs.existsSync(linkDir)) {
+            console.log(chalk.green(`Copying build to ${chalk.blueBright(linkDir)}`));
+            await fs.symlink(path.resolve("./dist"), linkDir);
+        }
+        return Promise.resolve();
+    } catch (err) {
+        Promise.reject(err);
+    }
+}
+
+/*********************/
+/*		PACKAGE		 */
+/*********************/
+
+/**
+ * Package build
+ */
+async function packageBuild() {
+    const manifest = getManifest();
+
+    return new Promise((resolve, reject) => {
+        try {
+            // Remove the package dir without doing anything else
+            if (argv.clean || argv.c) {
+                console.log(chalk.yellow("Removing all packaged files"));
+                fs.removeSync("package");
+                return;
+            }
+
+            // Ensure there is a directory to hold all the packaged versions
+            fs.ensureDirSync("package");
+
+            // Initialize the zip file
+            const zipName = `${manifest.file.name}-v${manifest.file.version}.zip`;
+            const zipFile = fs.createWriteStream(path.join("package", zipName));
+            const zip = archiver("zip", { zlib: { level: 9 } });
+
+            zipFile.on("close", () => {
+                console.log(chalk.green(zip.pointer() + " total bytes"));
+                console.log(chalk.green(`Zip file ${zipName} has been written`));
+                return resolve();
+            });
+
+            zip.on("error", (err) => {
+                throw err;
+            });
+
+            zip.pipe(zipFile);
+
+            // Add the directory with the final code
+            zip.directory("dist/", manifest.file.name);
+
+            zip.finalize();
+        } catch (err) {
+            return reject(err);
+        }
+    });
+}
+
+/*********************/
+/*		PACKAGE		 */
+/*********************/
+
+/**
+ * Update version and URLs in the manifest JSON
+ */
+function updateManifest(cb) {
+    const packageJson = fs.readJSONSync("package.json");
+    const config = getConfig(),
+        manifest = getManifest(),
+        rawURL = config.rawURL,
+        repoURL = config.repository,
+        manifestRoot = manifest.root;
+
+    if (!config) cb(Error(chalk.red("foundryconfig.json not found")));
+    if (!manifest) cb(Error(chalk.red("Manifest JSON not found")));
+    if (!rawURL || !repoURL) cb(Error(chalk.red("Repository URLs not configured in foundryconfig.json")));
+
+    try {
+        const version = argv.update || argv.u;
+
+        /* Update version */
+
+        const versionMatch = /^(\d{1,}).(\d{1,}).(\d{1,})$/;
+        const currentVersion = manifest.file.version;
+        let targetVersion = "";
+
+        if (!version) {
+            cb(Error("Missing version number"));
+        }
+
+        if (versionMatch.test(version)) {
+            targetVersion = version;
+        } else {
+            targetVersion = currentVersion.replace(versionMatch, (substring, major, minor, patch) => {
+                console.log(substring, Number(major) + 1, Number(minor) + 1, Number(patch) + 1);
+                if (version === "major") {
+                    return `${Number(major) + 1}.0.0`;
+                } else if (version === "minor") {
+                    return `${major}.${Number(minor) + 1}.0`;
+                } else if (version === "patch") {
+                    return `${major}.${minor}.${Number(patch) + 1}`;
+                } else {
+                    return "";
+                }
+            });
+        }
+
+        if (targetVersion === "") {
+            return cb(Error(chalk.red("Error: Incorrect version arguments.")));
+        }
+
+        if (targetVersion === currentVersion) {
+            return cb(Error(chalk.red("Error: Target version is identical to current version.")));
+        }
+        console.log(`Updating version number to '${targetVersion}'`);
+
+        packageJson.version = targetVersion;
+        manifest.file.version = targetVersion;
+
+        /* Update URLs */
+
+        const result = `${rawURL}/v${manifest.file.version}/package/${manifest.file.name}-v${manifest.file.version}.zip`;
+
+        manifest.file.url = repoURL;
+        manifest.file.manifest = `${rawURL}/master/${manifestRoot}/${manifest.name}`;
+        manifest.file.download = result;
+
+        const prettyProjectJson = stringify(manifest.file, {
+            maxLength: 35,
+            indent: "\t",
+        });
+
+        fs.writeJSONSync("package.json", packageJson, { spaces: "\t" });
+        fs.writeFileSync(path.join(manifest.root, manifest.name), prettyProjectJson, "utf8");
+
+        return cb();
+    } catch (err) {
+        cb(err);
+    }
+}
+
+function gitAdd() {
+    return gulp.src("package").pipe(git.add({ args: "--no-all" }));
+}
+
+function gitCommit() {
+    return gulp.src("./*").pipe(
+        git.commit(`v${getManifest().file.version}`, {
+            args: "-a",
+            disableAppendPaths: true,
+        }),
+    );
+}
+
+function gitTag() {
+    const manifest = getManifest();
+    return git.tag(`v${manifest.file.version}`, `Updated to ${manifest.file.version}`, (err) => {
+        if (err) throw err;
+    });
+}
+
+const execGit = gulp.series(gitAdd, gitCommit, gitTag);
+
+const execBuild = gulp.parallel(buildTS, buildLess, buildSASS, copyFiles);
+
+exports.build = gulp.series(clean, execBuild);
+exports.watch = buildWatch;
+exports.clean = clean;
+exports.link = linkUserData;
+exports.package = packageBuild;
+exports.update = updateManifest;
+exports.publish = gulp.series(clean, updateManifest, execBuild, packageBuild, execGit);

From fed3209c5803b5d10a004102b7059576585a3379 Mon Sep 17 00:00:00 2001
From: Johannes Loher <johannes.loher@fg4f.de>
Date: Wed, 6 Jan 2021 03:22:25 +0100
Subject: [PATCH 4/6] update the readme

---
 README.md | 73 ++++++++++++++++++++++++++++++++++++++++++-------------
 1 file changed, 56 insertions(+), 17 deletions(-)

diff --git a/README.md b/README.md
index 8119bbdb..4222e363 100644
--- a/README.md
+++ b/README.md
@@ -3,50 +3,89 @@
 An implementation of the Dungeonslayers 4 game system for [Foundry Virtual
 Tabletop](http://foundryvtt.com).
 
-## Prerequisites
+This system provides character sheet support for Actors and Items and mechanical
+support for dice and rules necessary to
+play games of Dungeponslayers 4.
 
-In order to build this system, a recent version of `npm` is required.
+## Installation
 
-## Building
+To install and use the Dungeonslayers 4 system for Foundry Virtual Tabletop,
+simply paste the following URL into the **Install System** dialog on the Setup
+menu of the application.
 
-To build the system, first install all required dependencies:
+https://git.f3l.de/dungeonslayers/ds4/-/raw/master/src/system.json?inline=false
+
+## Development
+
+### Prerequisits
+
+In order to build this system, recent versions of `node` and `npm` are required.
+We recommend using the latest lts version of `node`, which is `v14.15.4` at the
+time of writing. If you use `nvm` to manage your `node` versions, you can simply
+run
+```
+nvm install
+```
+
+in the project's root directory.
+
+You also need to install the the project's dependencies. To do so, run
 
 ```
 npm install
 ```
 
-Then build the project by running
+### Building
+
+You can build the project by running
 
 ```
 npm run build
 ```
 
-If you'd like the built system to be automatically linked to your local Foundry
-VTT installation's data folder, add a file called `foundryconfig.json` to the
-project root with the following contents:
+Alternatively, you can run
+
+```
+npm run build:watch
+```
+
+to watch for changes and automatically build as necessary.
+
+### Linking the built system to Foundry VTT
+
+In order to provide a fluent development experience, it is recommended to link
+the built system to your local Foundry VTT installation's data folder. In order
+to do so, first add a file called `foundryconfig.json` to the project root with
+the following content:
 
 ```
 {
-    "dataPath": "/<absolute path to your home>/.local/share/FoundryVTT",
-    "repository": "",
-    "rawURL": ""
+    "dataPath": "<path to your home directory>/.local/share/FoundryVTT"
 }
 ```
 
+On platforms other than Linux you need to adjust the path accordingly.
+
 Then run
 
 ```
 npm run link
 ```
 
-If you want the system to be continuously build upon every saved change, just
-run
+### Running the tests
+
+You can run the tests with the following command:
 
 ```
-npm run build:watch
+npm test
 ```
 
-# Licensing
+## Contributing
+
+Code and content contributions are accepted. Please feel free to submit issues
+to the issue tracker or submit merge requests for code changes.
+
+## Licensing
 
 Dungeonslayers (© Christian Kennig) is licensed under [CC BY-NC-SA 3.0](https://creativecommons.org/licenses/by-nc-sa/3.0/de/deed.en).
 
@@ -56,5 +95,5 @@ CC BY-NC-SA 3.0.  Hence the modified icons are also published under this
 license. A copy of this license can be found under
 [src/assets/official/LICENSE](src/assets/official/LICENSE).
 
-The rest of this project is licensed under the MIT License, a copy of which can
-be found under [LICENSE](LICENSE).
+The software component of this project is licensed under the MIT License, a copy
+of which can be found under [LICENSE](LICENSE).

From 6e363b3a02c5b9211f471063fd7e32f0bfe17120 Mon Sep 17 00:00:00 2001
From: Johannes Loher <johannes.loher@fg4f.de>
Date: Wed, 6 Jan 2021 14:48:48 +0100
Subject: [PATCH 5/6] add bug report issue template and improve feature request

---
 .gitlab/issue_templates/Bug Report.md      |  0
 .gitlab/issue_templates/Feature Request.md | 12 ++++++++----
 2 files changed, 8 insertions(+), 4 deletions(-)
 create mode 100644 .gitlab/issue_templates/Bug Report.md

diff --git a/.gitlab/issue_templates/Bug Report.md b/.gitlab/issue_templates/Bug Report.md
new file mode 100644
index 00000000..e69de29b
diff --git a/.gitlab/issue_templates/Feature Request.md b/.gitlab/issue_templates/Feature Request.md
index f60644d2..d6dbe30b 100644
--- a/.gitlab/issue_templates/Feature Request.md	
+++ b/.gitlab/issue_templates/Feature Request.md	
@@ -1,9 +1,13 @@
-# Description
+# Story
 
 As a …, I want … so that …
 
+# Description
+
+Please add a more detailed description of the feature here.
+
 # Acceptance criteria
 
-* Criterion 1
-* Criterion 2
-* …
+1. Criterion 1
+2. Criterion 2
+3. …

From f40efedcd376ef2fd24c34785f7924efb2d2bb88 Mon Sep 17 00:00:00 2001
From: Johannes Loher <johannes.loher@fg4f.de>
Date: Wed, 6 Jan 2021 14:56:50 +0100
Subject: [PATCH 6/6] add bug report template

---
 .gitlab/issue_templates/Bug Report.md | 29 +++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/.gitlab/issue_templates/Bug Report.md b/.gitlab/issue_templates/Bug Report.md
index e69de29b..c209b177 100644
--- a/.gitlab/issue_templates/Bug Report.md	
+++ b/.gitlab/issue_templates/Bug Report.md	
@@ -0,0 +1,29 @@
+# Description
+
+Please describe the issue.
+
+# Steps to Reproduce
+
+1. ...
+2. ...
+3. ...
+
+# Expected Behavior
+
+Please describe the expected behavior.
+
+# Actual Behavior
+
+Please describe the actual behavior.
+
+# Additional Details
+
+These are optional, please add them if it makes sense.
+
+- ![Screenshot]()
+- [Logfile]()
+- ...
+
+# Possible Solutions
+
+If you have any suggestions on how to solve the issue, please add them here.