diff --git a/tools/ci/annotate_dm.sh b/tools/ci/annotate_dm.sh
new file mode 100644
index 0000000000000..e43f930ba1acc
--- /dev/null
+++ b/tools/ci/annotate_dm.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+set -euo pipefail
+tools/bootstrap/python -m dm_annotator "$@"
diff --git a/tools/ci/build.ps1 b/tools/ci/build.ps1
new file mode 100644
index 0000000000000..4694cc3be58d4
--- /dev/null
+++ b/tools/ci/build.ps1
@@ -0,0 +1,10 @@
+if(!(Test-Path -Path "C:/byond")){
+ bash tools/ci/download_byond.sh
+ [System.IO.Compression.ZipFile]::ExtractToDirectory("C:/byond.zip", "C:/")
+ Remove-Item C:/byond.zip
+}
+
+bash tools/ci/install_node.sh
+bash tools/build/build -Werror
+
+exit $LASTEXITCODE
diff --git a/tools/ci/build_spaceman_dmm.sh b/tools/ci/build_spaceman_dmm.sh
new file mode 100644
index 0000000000000..d63aeac2cc850
--- /dev/null
+++ b/tools/ci/build_spaceman_dmm.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+set -euo pipefail
+
+source dependencies.sh
+
+cd $HOME/SpacemanDMM
+
+if [ ! -d .git ]
+then
+ git init
+ git remote add origin https://github.com/SpaceManiac/SpacemanDMM.git
+fi
+
+git fetch origin --depth=1 $SPACEMAN_DMM_COMMIT_HASH
+git reset --hard FETCH_HEAD
+
+cargo build --release --bin $1
+cp target/release/$1 ~
+~/$1 --version
diff --git a/tools/ci/check_filedirs copy.sh b/tools/ci/check_filedirs copy.sh
new file mode 100644
index 0000000000000..59f05d5f5be9d
--- /dev/null
+++ b/tools/ci/check_filedirs copy.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+if [ -n "$1" ]
+then
+ dme=$1
+else
+ echo "ERROR: Specify a DME to check"
+ exit 1
+fi
+
+if [[ $(awk '/BEGIN_FILE_DIR/{flag=1;next}/END_FILE_DIR/{flag=0}flag' $dme | wc -l) -ne 1 ]]
+then
+ echo "ERROR: File DIR was ticked, please untick it, see: https://tgstation13.org/phpBB/viewtopic.php?f=5&t=321 for more"
+ exit 1
+fi
+
diff --git a/tools/ci/check_misc.sh b/tools/ci/check_misc.sh
new file mode 100644
index 0000000000000..5cb1c860c3361
--- /dev/null
+++ b/tools/ci/check_misc.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -euo pipefail
+
+find . -name "*.php" -print0 | xargs -0 -n1 php -l
+find . -name "*.json" -not -path "*/node_modules/*" -print0 | xargs -0 python3 ./tools/json_verifier.py
diff --git a/tools/ci/ci_config.txt b/tools/ci/ci_config.txt
new file mode 100644
index 0000000000000..51e08e6328ba1
--- /dev/null
+++ b/tools/ci/ci_config.txt
@@ -0,0 +1,10 @@
+SQL_ENABLED
+ADDRESS 127.0.0.1
+PORT 3306
+FEEDBACK_DATABASE tg_ci
+FEEDBACK_TABLEPREFIX
+FEEDBACK_LOGIN root
+FEEDBACK_PASSWORD
+LAVALAND_BUDGET 0
+SPACE_BUDGET 0
+AUXTOOLS_ENABLED
diff --git a/tools/ci/ci_dependencies.sh b/tools/ci/ci_dependencies.sh
new file mode 100644
index 0000000000000..fd1bee5ea88a9
--- /dev/null
+++ b/tools/ci/ci_dependencies.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+#Project dependencies file
+#Contains versions of programs that we might need to install for CI purposes - do not add anything here that is REQUIRED to run the project, this is just for CI.
+
+export RIPGREP_VERSION=14.0.3
diff --git a/tools/ci/download_byond.sh b/tools/ci/download_byond.sh
new file mode 100644
index 0000000000000..19b7f2017078f
--- /dev/null
+++ b/tools/ci/download_byond.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e
+source dependencies.sh
+echo "Downloading BYOND version $BYOND_MAJOR.$BYOND_MINOR"
+curl "http://www.byond.com/download/build/$BYOND_MAJOR/$BYOND_MAJOR.${BYOND_MINOR}_byond.zip" -o C:/byond.zip
diff --git a/tools/ci/install_auxlua.sh b/tools/ci/install_auxlua.sh
new file mode 100644
index 0000000000000..c24ccb497d17f
--- /dev/null
+++ b/tools/ci/install_auxlua.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+source dependencies.sh
+
+mkdir -p ~/.byond/bin
+wget -nv -O ~/.byond/bin/libauxlua.so "https://github.com/$AUXLUA_REPO/releases/download/$AUXLUA_VERSION/libauxlua.so"
+chmod +x ~/.byond/bin/libauxlua.so
+ldd ~/.byond/bin/libauxlua.so
diff --git a/tools/ci/install_ripgrep.sh b/tools/ci/install_ripgrep.sh
new file mode 100644
index 0000000000000..455027d93a6eb
--- /dev/null
+++ b/tools/ci/install_ripgrep.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+source tools/ci/ci_dependencies.sh
+
+cargo install ripgrep --features pcre2 --version $RIPGREP_VERSION
diff --git a/tools/ci/run_server.sh b/tools/ci/run_server.sh
new file mode 100644
index 0000000000000..baf172d8ec3d8
--- /dev/null
+++ b/tools/ci/run_server.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+set -euo pipefail
+
+MAP=$1
+
+echo Testing $MAP
+
+tools/deploy.sh ci_test
+mkdir ci_test/config
+mkdir ci_test/data
+
+#test config
+cp tools/ci/ci_config.txt ci_test/config/config.txt
+
+#set the map
+cp _maps/$MAP.json ci_test/data/next_map.json
+
+cd ci_test
+DreamDaemon tgstation.dmb -close -trusted -verbose -params "log-directory=ci"
+
+cd ..
+
+mkdir -p data/screenshots_new
+cp -r ci_test/data/screenshots_new data/screenshots_new
+
+cat ci_test/data/logs/ci/clean_run.lk
diff --git a/tools/ci/show_screenshot_test_results.js b/tools/ci/show_screenshot_test_results.js
new file mode 100644
index 0000000000000..607ad9854d676
--- /dev/null
+++ b/tools/ci/show_screenshot_test_results.js
@@ -0,0 +1,236 @@
+import fetch, { FormData, fileFrom } from "node-fetch";
+import fs from "fs";
+import path from "path";
+import process from "process";
+
+const createComment = (screenshotFailures, zipFileUrl) => {
+ const formatScreenshotFailure = ({ directory, diffUrl, newUrl, oldUrl }) => {
+ const img = (url) => {
+ if (url) {
+ return `![](${url})`;
+ } else {
+ return "None produced.";
+ }
+ };
+
+ return `| ${directory} | ${img(oldUrl)} | ${img(newUrl)} | ${img(diffUrl)} |`;
+ };
+
+ return `
+ Screenshot tests failed!
+
+ ${zipFileUrl ? `[Download zip file of new screenshots.](${zipFileUrl})` : "No zip file could be produced, this is a bug!"}
+
+ ## Diffs
+
+ See snapshot diffs
+
+ | Name | Expected image | Produced image | Diff |
+ | :--: | :------------: | :------------: | :--: |
+ ${screenshotFailures.map(formatScreenshotFailure).join("\n")}
+
+
+ ## Help
+
+ What is this?
+
+ Screenshot tests make sure that specific icons look the same as they did before.
+ This is important for elements that often mistakenly change, such as alien species.
+
+ If the produced image looks broken, then it is possible your code caused a bug.
+ Make sure to test in game to see if you can fix it.
+
+
+
+ I am changing sprites, it's supposed to look different.
+
+ If the newly produced sprites are correct, then the tests should be updated.
+
+ You can either:
+
+ 1. Right-click the "produced image", and save it in \`code/modules/unit_tests/screenshots/NAME.png\`.
+ 2. Download and extract [this zip file](${zipFileUrl}) in the root of your repository, and commit.
+
+ If you need help, you can ask maintainers either on Discord or on this pull request.
+
+
+
+ This is a false positive.
+
+ If you are sure your code did not cause this failure, especially if it's inconsistent,
+ then you may have found a false positive.
+
+ Ask maintainers to rerun the test.
+
+ If you need help, you can ask maintainers either on Discord or on this pull request.
+
+ `.replace(/\t/g, ''); // If we keep tabs, it'll become a code block.
+};
+
+export async function showScreenshotTestResults({ github, context, exec }) {
+ const { FILE_HOUSE_KEY } = process.env;
+
+ // Check if bad-screenshots is in the artifacts
+ const { data: { artifacts } } = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: context.payload.workflow_run.id,
+ });
+
+ const badScreenshots = artifacts.find(({ name }) => name === 'bad-screenshots');
+ if (!badScreenshots) {
+ console.log("No bad screenshots found");
+ return;
+ }
+
+ // Download the screenshots from the artifacts
+ const download = await github.rest.actions.downloadArtifact({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ artifact_id: badScreenshots.id,
+ archive_format: "zip",
+ });
+
+ fs.writeFileSync("bad-screenshots.zip", Buffer.from(download.data));
+
+ await exec.exec("unzip bad-screenshots.zip -d bad-screenshots");
+
+ const prNumberFile = path.join("bad-screenshots", "pull_request_number.txt");
+
+ if (!fs.existsSync(prNumberFile)) {
+ console.log("No PR number found");
+ return;
+ }
+
+ const prNumber = parseInt(fs.readFileSync(prNumberFile, "utf8"), 10);
+ if (!prNumber) {
+ console.log("No PR number found");
+ return;
+ }
+
+ fs.rmSync(prNumberFile);
+
+ // Validate the PR
+ const result = await github.graphql(`query($owner:String!, $repo:String!, $prNumber:Int!) {
+ repository(owner: $owner, name: $repo) {
+ pullRequest(number: $prNumber) {
+ commits(last: 1) {
+ nodes {
+ commit {
+ checkSuites(first: 10) {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }`, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ prNumber,
+ });
+
+ const validPr = result
+ .repository
+ .pullRequest
+ .commits
+ .nodes[0]
+ .commit
+ .checkSuites
+ .nodes
+ .some(({ id }) => id === context.payload.workflow_run.check_suite_node_id);
+
+ if (!validPr) {
+ console.log(`PR #${prNumber} is not valid (expected check suite ID ${context.payload.workflow_run.check_suite_node_id})`);
+ return;
+ }
+
+ // Upload the screenshots
+ // 1. Loop over the bad-screenshots directory
+ // 2. Upload the screenshot
+ // 3. Save the URL
+ const uploadFile = async (filename) => {
+ if (!fs.existsSync(filename)) {
+ return;
+ }
+
+ const formData = new FormData();
+
+ formData.set("key", FILE_HOUSE_KEY);
+
+ formData.set("file", await fileFrom(filename), path.basename(filename));
+
+ return fetch("https://file.house/api/upload", {
+ method: "POST",
+ body: formData,
+ })
+ .then(response => response.json())
+ .then(response => {
+ console.log(response);
+ return response;
+ })
+ .then(({ url }) => url);
+ };
+
+ const screenshotFailures = [];
+
+ for (const directory of fs.readdirSync("bad-screenshots")) {
+ console.log(`Uploading screenshots for ${directory}`);
+
+ let diffUrl;
+ let newUrl;
+ let oldUrl;
+
+ await Promise.all([
+ uploadFile(path.join("bad-screenshots", directory, "new.png")).then(url => newUrl = url),
+ uploadFile(path.join("bad-screenshots", directory, "old.png")).then(url => oldUrl = url),
+ uploadFile(path.join("bad-screenshots", directory, "diff.png")).then(url => diffUrl = url),
+ ]);
+
+ console.log(`New URL (${directory}): ${newUrl}`);
+ console.log(`Old URL (${directory}): ${oldUrl}`);
+ console.log(`Diff URL (${directory}): ${diffUrl}`);
+
+ screenshotFailures.push({ directory, diffUrl, newUrl, oldUrl });
+ }
+
+ if (screenshotFailures.length === 0) {
+ console.log("No screenshot failures found");
+ return;
+ }
+
+ // Upload zip file for quick fixes
+ const zipFilePath = path.join("data", "screenshot-update");
+ const finalDestination = path.join(
+ zipFilePath,
+ "code", "modules", "unit_tests", "screenshots",
+ )
+
+ fs.mkdirSync(finalDestination, { recursive: true });
+
+ for (const { directory } of screenshotFailures) {
+ fs.copyFileSync(
+ path.join("bad-screenshots", directory, "new.png"),
+ path.join(finalDestination, `${directory}.png`),
+ )
+ }
+
+ await exec.exec("zip", ["-r", `../screenshot-update.zip`, "."], {
+ cwd: zipFilePath,
+ });
+
+ const zipUrl = await uploadFile(`${zipFilePath}.zip`);
+
+ // Post the comment
+ const comment = createComment(screenshotFailures, zipUrl);
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ body: comment,
+ });
+}